Add Open() options, flock timeout.

This commit changes Open() to provide an additional Options argument. The options
argument currently only has a Timeout which will cause the Open() to return
ErrTimeout if a file lock cannot be obtained in time.

Fixes #207.
master
Ben Johnson 2014-06-21 14:44:22 -06:00
parent 0a59a75472
commit 00ee0da528
20 changed files with 172 additions and 132 deletions

View File

@ -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
}

View File

@ -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
}

69
bolt_unix.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -1021,7 +1021,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()
@ -1048,7 +1048,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()
@ -1088,7 +1088,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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")))

View File

@ -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

View File

@ -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

View File

@ -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())
}

View File

@ -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

View File

@ -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

23
db.go
View File

@ -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

View File

@ -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())
}

View File

@ -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.