aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bolt_darwin.go41
-rw-r--r--bolt_linux.go40
-rw-r--r--bolt_unix.go69
-rw-r--r--bolt_windows.go3
-rw-r--r--bucket_test.go6
-rw-r--r--cmd/bolt/bench.go2
-rw-r--r--cmd/bolt/buckets.go2
-rw-r--r--cmd/bolt/check.go2
-rw-r--r--cmd/bolt/export.go2
-rw-r--r--cmd/bolt/get.go2
-rw-r--r--cmd/bolt/import.go2
-rw-r--r--cmd/bolt/import_test.go2
-rw-r--r--cmd/bolt/info.go2
-rw-r--r--cmd/bolt/keys.go2
-rw-r--r--cmd/bolt/main_test.go2
-rw-r--r--cmd/bolt/pages.go2
-rw-r--r--cmd/bolt/stats.go2
-rw-r--r--db.go23
-rw-r--r--db_test.go90
-rw-r--r--tx_test.go8
20 files changed, 172 insertions, 132 deletions
diff --git a/bolt_darwin.go b/bolt_darwin.go
index 2bffad3..6528f72 100644
--- a/bolt_darwin.go
+++ b/bolt_darwin.go
@@ -2,8 +2,6 @@ package bolt
import (
"os"
- "syscall"
- "unsafe"
)
var odirect int
@@ -12,42 +10,3 @@ var odirect int
func fdatasync(f *os.File) error {
return f.Sync()
}
-
-// flock acquires an advisory lock on a file descriptor.
-func flock(f *os.File) error {
- return syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
-}
-
-// funlock releases an advisory lock on a file descriptor.
-func funlock(f *os.File) error {
- return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
-}
-
-// mmap memory maps a DB's data file.
-func mmap(db *DB, sz int) error {
- b, err := syscall.Mmap(int(db.file.Fd()), 0, sz, syscall.PROT_READ, syscall.MAP_SHARED)
- if err != nil {
- return err
- }
-
- // Save the original byte slice and convert to a byte array pointer.
- db.dataref = b
- db.data = (*[maxMapSize]byte)(unsafe.Pointer(&b[0]))
- db.datasz = sz
- return nil
-}
-
-// munmap unmaps a DB's data file from memory.
-func munmap(db *DB) error {
- // Ignore the unmap if we have no mapped data.
- if db.dataref == nil {
- return nil
- }
-
- // Unmap using the original byte slice.
- err := syscall.Munmap(db.dataref)
- db.dataref = nil
- db.data = nil
- db.datasz = 0
- return err
-}
diff --git a/bolt_linux.go b/bolt_linux.go
index 761a83e..7e3e539 100644
--- a/bolt_linux.go
+++ b/bolt_linux.go
@@ -3,7 +3,6 @@ package bolt
import (
"os"
"syscall"
- "unsafe"
)
var odirect = syscall.O_DIRECT
@@ -12,42 +11,3 @@ var odirect = syscall.O_DIRECT
func fdatasync(f *os.File) error {
return syscall.Fdatasync(int(f.Fd()))
}
-
-// flock acquires an advisory lock on a file descriptor.
-func flock(f *os.File) error {
- return syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
-}
-
-// funlock releases an advisory lock on a file descriptor.
-func funlock(f *os.File) error {
- return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
-}
-
-// mmap memory maps a DB's data file.
-func mmap(db *DB, sz int) error {
- b, err := syscall.Mmap(int(db.file.Fd()), 0, sz, syscall.PROT_READ, syscall.MAP_SHARED)
- if err != nil {
- return err
- }
-
- // Save the original byte slice and convert to a byte array pointer.
- db.dataref = b
- db.data = (*[maxMapSize]byte)(unsafe.Pointer(&b[0]))
- db.datasz = sz
- return nil
-}
-
-// munmap unmaps a DB's data file from memory.
-func munmap(db *DB) error {
- // Ignore the unmap if we have no mapped data.
- if db.dataref == nil {
- return nil
- }
-
- // Unmap using the original byte slice.
- err := syscall.Munmap(db.dataref)
- db.dataref = nil
- db.data = nil
- db.datasz = 0
- return err
-}
diff --git a/bolt_unix.go b/bolt_unix.go
new file mode 100644
index 0000000..f7d2fe5
--- /dev/null
+++ b/bolt_unix.go
@@ -0,0 +1,69 @@
+// +build linux darwin
+
+package bolt
+
+import (
+ "os"
+ "syscall"
+ "time"
+ "unsafe"
+)
+
+// flock acquires an advisory lock on a file descriptor.
+func flock(f *os.File, timeout time.Duration) error {
+ var t time.Time
+ for {
+ // If we're beyond our timeout then return an error.
+ // This can only occur after we've attempted a flock once.
+ if t.IsZero() {
+ t = time.Now()
+ } else if timeout > 0 && time.Since(t) > timeout {
+ return ErrTimeout
+ }
+
+ // Otherwise attempt to obtain an exclusive lock.
+ err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
+ if err == nil {
+ return nil
+ } else if err != syscall.EWOULDBLOCK {
+ return err
+ }
+
+ // Wait for a bit and try again.
+ time.Sleep(50 * time.Millisecond)
+ }
+}
+
+// funlock releases an advisory lock on a file descriptor.
+func funlock(f *os.File) error {
+ return syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
+}
+
+// mmap memory maps a DB's data file.
+func mmap(db *DB, sz int) error {
+ b, err := syscall.Mmap(int(db.file.Fd()), 0, sz, syscall.PROT_READ, syscall.MAP_SHARED)
+ if err != nil {
+ return err
+ }
+
+ // Save the original byte slice and convert to a byte array pointer.
+ db.dataref = b
+ db.data = (*[maxMapSize]byte)(unsafe.Pointer(&b[0]))
+ db.datasz = sz
+ return nil
+}
+
+// munmap unmaps a DB's data file from memory.
+func munmap(db *DB) error {
+ // Ignore the unmap if we have no mapped data.
+ if db.dataref == nil {
+ return nil
+ }
+
+ // Unmap using the original byte slice.
+ err := syscall.Munmap(db.dataref)
+ db.dataref = nil
+ db.data = nil
+ db.datasz = 0
+ return err
+}
diff --git a/bolt_windows.go b/bolt_windows.go
index 8300d40..fc77968 100644
--- a/bolt_windows.go
+++ b/bolt_windows.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"syscall"
+ "time"
"unsafe"
)
@@ -15,7 +16,7 @@ func fdatasync(f *os.File) error {
}
// flock acquires an advisory lock on a file descriptor.
-func flock(f *os.File) error {
+func flock(f *os.File, _ time.Duration) error {
return nil
}
diff --git a/bucket_test.go b/bucket_test.go
index fbaaeb6..0785753 100644
--- a/bucket_test.go
+++ b/bucket_test.go
@@ -1003,7 +1003,7 @@ func TestBucket_Delete_Quick(t *testing.T) {
func ExampleBucket_Put() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -1030,7 +1030,7 @@ func ExampleBucket_Put() {
func ExampleBucket_Delete() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -1070,7 +1070,7 @@ func ExampleBucket_Delete() {
func ExampleBucket_ForEach() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
diff --git a/cmd/bolt/bench.go b/cmd/bolt/bench.go
index 5d0dbec..7eb503d 100644
--- a/cmd/bolt/bench.go
+++ b/cmd/bolt/bench.go
@@ -41,7 +41,7 @@ func Bench(options *BenchOptions) {
}
// Create database.
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/buckets.go b/cmd/bolt/buckets.go
index 48395d8..68e7dde 100644
--- a/cmd/bolt/buckets.go
+++ b/cmd/bolt/buckets.go
@@ -13,7 +13,7 @@ func Buckets(path string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/check.go b/cmd/bolt/check.go
index 7555ec8..125f2b8 100644
--- a/cmd/bolt/check.go
+++ b/cmd/bolt/check.go
@@ -13,7 +13,7 @@ func Check(path string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/export.go b/cmd/bolt/export.go
index 2dcbc1f..2689f32 100644
--- a/cmd/bolt/export.go
+++ b/cmd/bolt/export.go
@@ -16,7 +16,7 @@ func Export(path string) {
}
// Open the database.
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/get.go b/cmd/bolt/get.go
index 6ea7f04..90e0c1d 100644
--- a/cmd/bolt/get.go
+++ b/cmd/bolt/get.go
@@ -13,7 +13,7 @@ func Get(path, name, key string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/import.go b/cmd/bolt/import.go
index 0988f32..258a7da 100644
--- a/cmd/bolt/import.go
+++ b/cmd/bolt/import.go
@@ -29,7 +29,7 @@ func Import(path string, input string) {
func importBuckets(path string, root []*rawMessage) {
// Open the database.
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/import_test.go b/cmd/bolt/import_test.go
index 263f561..3d4f275 100644
--- a/cmd/bolt/import_test.go
+++ b/cmd/bolt/import_test.go
@@ -23,7 +23,7 @@ func TestImport(t *testing.T) {
assert.Equal(t, ``, output)
// Open database and verify contents.
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
assert.NoError(t, err)
db.View(func(tx *bolt.Tx) error {
assert.NotNil(t, tx.Bucket([]byte("empty")))
diff --git a/cmd/bolt/info.go b/cmd/bolt/info.go
index 1e9e0d8..5606e68 100644
--- a/cmd/bolt/info.go
+++ b/cmd/bolt/info.go
@@ -13,7 +13,7 @@ func Info(path string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/keys.go b/cmd/bolt/keys.go
index 6affefe..d4bb3c3 100644
--- a/cmd/bolt/keys.go
+++ b/cmd/bolt/keys.go
@@ -13,7 +13,7 @@ func Keys(path, name string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/main_test.go b/cmd/bolt/main_test.go
index 9b32cc8..0614d43 100644
--- a/cmd/bolt/main_test.go
+++ b/cmd/bolt/main_test.go
@@ -14,7 +14,7 @@ func open(fn func(*bolt.DB, string)) {
path := tempfile()
defer os.RemoveAll(path)
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
panic("db open error: " + err.Error())
}
diff --git a/cmd/bolt/pages.go b/cmd/bolt/pages.go
index 33f6886..ec1c4b4 100644
--- a/cmd/bolt/pages.go
+++ b/cmd/bolt/pages.go
@@ -14,7 +14,7 @@ func Pages(path string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/cmd/bolt/stats.go b/cmd/bolt/stats.go
index 54a0f44..b5d0083 100644
--- a/cmd/bolt/stats.go
+++ b/cmd/bolt/stats.go
@@ -14,7 +14,7 @@ func Stats(path, prefix string) {
return
}
- db, err := bolt.Open(path, 0600)
+ db, err := bolt.Open(path, 0600, nil)
if err != nil {
fatal(err)
return
diff --git a/db.go b/db.go
index d7037f9..56a3ef3 100644
--- a/db.go
+++ b/db.go
@@ -8,6 +8,7 @@ import (
"runtime/debug"
"strings"
"sync"
+ "time"
"unsafe"
)
@@ -50,6 +51,10 @@ var (
// ErrChecksum is returned when either meta page checksum does not match.
ErrChecksum = errors.New("checksum error")
+
+ // ErrTimeout is returned when a database cannot obtain an exclusive lock
+ // on the data file after the timeout passed to Open().
+ ErrTimeout = errors.New("timeout")
)
// DB represents a collection of buckets persisted to a file on disk.
@@ -108,9 +113,15 @@ func (db *DB) String() string {
// Open creates and opens a database at the given path.
// If the file does not exist then it will be created automatically.
-func Open(path string, mode os.FileMode) (*DB, error) {
+// Passing in nil options will cause Bolt to open the database with the default options.
+func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
var db = &DB{opened: true, FillPercent: DefaultFillPercent}
+ // Set default options.
+ if options == nil {
+ options = &Options{}
+ }
+
// Open data file and separate sync handler for metadata writes.
db.path = path
@@ -123,7 +134,7 @@ func Open(path string, mode os.FileMode) (*DB, error) {
// Lock file so that other processes using Bolt cannot use the database
// at the same time. This would cause corruption since the two processes
// would write meta pages and free pages separately.
- if err := flock(db.file); err != nil {
+ if err := flock(db.file, options.Timeout); err != nil {
_ = db.close()
return nil, err
}
@@ -556,6 +567,14 @@ func (db *DB) allocate(count int) (*page, error) {
return p, nil
}
+// Options represents the options that can be set when opening a database.
+type Options struct {
+ // Timeout is the amount of time to wait to obtain a file lock.
+ // When set to zero it will wait indefinitely. This option is only
+ // available on Darwin and Linux.
+ Timeout time.Duration
+}
+
// Stats represents statistics about the database.
type Stats struct {
// Freelist stats
diff --git a/db_test.go b/db_test.go
index 0fc286b..310dfad 100644
--- a/db_test.go
+++ b/db_test.go
@@ -7,6 +7,7 @@ import (
"io/ioutil"
"os"
"regexp"
+ "runtime"
"sort"
"strings"
"testing"
@@ -18,31 +19,17 @@ import (
var statsFlag = flag.Bool("stats", false, "show performance stats")
-// Ensure that a database can be opened without error.
-func TestOpen(t *testing.T) {
- f, _ := ioutil.TempFile("", "bolt-")
- path := f.Name()
- f.Close()
- os.Remove(path)
- defer os.RemoveAll(path)
-
- db, err := Open(path, 0666)
- assert.NoError(t, err)
- assert.NotNil(t, db)
- db.Close()
-}
-
// Ensure that opening a database with a bad path returns an error.
func TestOpen_BadPath(t *testing.T) {
- db, err := Open("/../bad-path", 0666)
+ db, err := Open("/../bad-path", 0666, nil)
assert.Error(t, err)
assert.Nil(t, db)
}
// Ensure that a database can be opened without error.
-func TestDB_Open(t *testing.T) {
+func TestOpen(t *testing.T) {
withTempPath(func(path string) {
- db, err := Open(path, 0666)
+ db, err := Open(path, 0666, nil)
assert.NotNil(t, db)
assert.NoError(t, err)
assert.Equal(t, db.Path(), path)
@@ -50,15 +37,60 @@ func TestDB_Open(t *testing.T) {
})
}
+// Ensure that opening an already open database file will timeout.
+func TestOpen_Timeout(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("timeout not supported on windows")
+ }
+ withTempPath(func(path string) {
+ // Open a data file.
+ db0, err := Open(path, 0666, nil)
+ assert.NotNil(t, db0)
+ assert.NoError(t, err)
+
+ // Attempt to open the database again.
+ start := time.Now()
+ db1, err := Open(path, 0666, &Options{Timeout: 100 * time.Millisecond})
+ assert.Nil(t, db1)
+ assert.Equal(t, ErrTimeout, err)
+ assert.True(t, time.Since(start) > 100*time.Millisecond)
+
+ db0.Close()
+ })
+}
+
+// Ensure that opening an already open database file will wait until its closed.
+func TestOpen_Wait(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("timeout not supported on windows")
+ }
+ withTempPath(func(path string) {
+ // Open a data file.
+ db0, err := Open(path, 0666, nil)
+ assert.NotNil(t, db0)
+ assert.NoError(t, err)
+
+ // Close it in just a bit.
+ time.AfterFunc(100*time.Millisecond, func() { db0.Close() })
+
+ // Attempt to open the database again.
+ start := time.Now()
+ db1, err := Open(path, 0666, &Options{Timeout: 200 * time.Millisecond})
+ assert.NotNil(t, db1)
+ assert.NoError(t, err)
+ assert.True(t, time.Since(start) > 100*time.Millisecond)
+ })
+}
+
// Ensure that a re-opened database is consistent.
func TestOpen_Check(t *testing.T) {
withTempPath(func(path string) {
- db, err := Open(path, 0666)
+ db, err := Open(path, 0666, nil)
assert.NoError(t, err)
assert.NoError(t, db.View(func(tx *Tx) error { return <-tx.Check() }))
db.Close()
- db, err = Open(path, 0666)
+ db, err = Open(path, 0666, nil)
assert.NoError(t, err)
assert.NoError(t, db.View(func(tx *Tx) error { return <-tx.Check() }))
db.Close()
@@ -68,7 +100,7 @@ func TestOpen_Check(t *testing.T) {
// Ensure that the database returns an error if the file handle cannot be open.
func TestDB_Open_FileError(t *testing.T) {
withTempPath(func(path string) {
- _, err := Open(path+"/youre-not-my-real-parent", 0666)
+ _, err := Open(path+"/youre-not-my-real-parent", 0666, nil)
if err, _ := err.(*os.PathError); assert.Error(t, err) {
assert.Equal(t, path+"/youre-not-my-real-parent", err.Path)
assert.Equal(t, "open", err.Op)
@@ -84,14 +116,14 @@ func TestDB_Open_MetaInitWriteError(t *testing.T) {
// Ensure that a database that is too small returns an error.
func TestDB_Open_FileTooSmall(t *testing.T) {
withTempPath(func(path string) {
- db, err := Open(path, 0666)
+ db, err := Open(path, 0666, nil)
assert.NoError(t, err)
db.Close()
// corrupt the database
assert.NoError(t, os.Truncate(path, int64(os.Getpagesize())))
- db, err = Open(path, 0666)
+ db, err = Open(path, 0666, nil)
assert.Equal(t, errors.New("file size too small"), err)
})
}
@@ -115,7 +147,7 @@ func TestDB_Open_CorruptMeta0(t *testing.T) {
assert.NoError(t, err)
// Open the database.
- _, err = Open(path, 0666)
+ _, err = Open(path, 0666, nil)
assert.Equal(t, err, errors.New("meta0 error: invalid database"))
})
}
@@ -124,7 +156,7 @@ func TestDB_Open_CorruptMeta0(t *testing.T) {
func TestDB_Open_MetaChecksumError(t *testing.T) {
for i := 0; i < 2; i++ {
withTempPath(func(path string) {
- db, err := Open(path, 0600)
+ db, err := Open(path, 0600, nil)
pageSize := db.pageSize
db.Update(func(tx *Tx) error {
_, err := tx.CreateBucket([]byte("widgets"))
@@ -143,7 +175,7 @@ func TestDB_Open_MetaChecksumError(t *testing.T) {
f.Close()
// Reopen the database.
- _, err = Open(path, 0600)
+ _, err = Open(path, 0600, nil)
if assert.Error(t, err) {
if i == 0 {
assert.Equal(t, "meta0 error: checksum error", err.Error())
@@ -387,7 +419,7 @@ func TestDB_DoubleFree(t *testing.T) {
func ExampleDB_Update() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -418,7 +450,7 @@ func ExampleDB_Update() {
func ExampleDB_View() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -444,7 +476,7 @@ func ExampleDB_View() {
func ExampleDB_Begin_ReadOnly() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -494,7 +526,7 @@ func withTempPath(fn func(string)) {
// withOpenDB executes a function with an already opened database.
func withOpenDB(fn func(*DB, string)) {
withTempPath(func(path string) {
- db, err := Open(path, 0666)
+ db, err := Open(path, 0666, nil)
if err != nil {
panic("cannot open db: " + err.Error())
}
diff --git a/tx_test.go b/tx_test.go
index 2fe4723..0528c0d 100644
--- a/tx_test.go
+++ b/tx_test.go
@@ -301,7 +301,7 @@ func TestTx_CopyFile(t *testing.T) {
assert.NoError(t, db.View(func(tx *Tx) error { return tx.CopyFile(dest, 0600) }))
- db2, err := Open(dest, 0600)
+ db2, err := Open(dest, 0600, nil)
assert.NoError(t, err)
defer db2.Close()
@@ -366,7 +366,7 @@ func TestTx_CopyFile_Error_Normal(t *testing.T) {
func ExampleTx_Rollback() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -400,7 +400,7 @@ func ExampleTx_Rollback() {
func ExampleTx_CopyFile() {
// Open the database.
- db, _ := Open(tempfile(), 0666)
+ db, _ := Open(tempfile(), 0666, nil)
defer os.Remove(db.Path())
defer db.Close()
@@ -417,7 +417,7 @@ func ExampleTx_CopyFile() {
defer os.Remove(toFile)
// Open the cloned database.
- db2, _ := Open(toFile, 0666)
+ db2, _ := Open(toFile, 0666, nil)
defer db2.Close()
// Ensure that the key exists in the copy.