Merge branch 'ro'

master
Ben Johnson 2015-05-18 13:45:09 -06:00
commit d4363a9208
5 changed files with 124 additions and 15 deletions

View File

@ -11,7 +11,7 @@ import (
) )
// flock acquires an advisory lock on a file descriptor. // flock acquires an advisory lock on a file descriptor.
func flock(f *os.File, timeout time.Duration) error { func flock(f *os.File, exclusive bool, timeout time.Duration) error {
var t time.Time var t time.Time
for { for {
// If we're beyond our timeout then return an error. // If we're beyond our timeout then return an error.
@ -21,9 +21,13 @@ func flock(f *os.File, timeout time.Duration) error {
} else if timeout > 0 && time.Since(t) > timeout { } else if timeout > 0 && time.Since(t) > timeout {
return ErrTimeout return ErrTimeout
} }
flag := syscall.LOCK_SH
if exclusive {
flag = syscall.LOCK_EX
}
// Otherwise attempt to obtain an exclusive lock. // Otherwise attempt to obtain an exclusive lock.
err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) err := syscall.Flock(int(f.Fd()), flag|syscall.LOCK_NB)
if err == nil { if err == nil {
return nil return nil
} else if err != syscall.EWOULDBLOCK { } else if err != syscall.EWOULDBLOCK {
@ -44,7 +48,7 @@ func funlock(f *os.File) error {
func mmap(db *DB, sz int) error { func mmap(db *DB, sz int) error {
// Truncate and fsync to ensure file size metadata is flushed. // Truncate and fsync to ensure file size metadata is flushed.
// https://github.com/boltdb/bolt/issues/284 // https://github.com/boltdb/bolt/issues/284
if !db.NoGrowSync { if !db.NoGrowSync && !db.readOnly {
if err := db.file.Truncate(int64(sz)); err != nil { if err := db.file.Truncate(int64(sz)); err != nil {
return fmt.Errorf("file resize error: %s", err) return fmt.Errorf("file resize error: %s", err)
} }

View File

@ -16,7 +16,7 @@ func fdatasync(db *DB) error {
} }
// flock acquires an advisory lock on a file descriptor. // flock acquires an advisory lock on a file descriptor.
func flock(f *os.File, _ time.Duration) error { func flock(f *os.File, _ bool, _ time.Duration) error {
return nil return nil
} }
@ -28,9 +28,11 @@ func funlock(f *os.File) error {
// mmap memory maps a DB's data file. // mmap memory maps a DB's data file.
// Based on: https://github.com/edsrzf/mmap-go // Based on: https://github.com/edsrzf/mmap-go
func mmap(db *DB, sz int) error { func mmap(db *DB, sz int) error {
// Truncate the database to the size of the mmap. if !db.readOnly {
if err := db.file.Truncate(int64(sz)); err != nil { // Truncate the database to the size of the mmap.
return fmt.Errorf("truncate: %s", err) if err := db.file.Truncate(int64(sz)); err != nil {
return fmt.Errorf("truncate: %s", err)
}
} }
// Open a file mapping handle. // Open a file mapping handle.

45
db.go
View File

@ -104,6 +104,10 @@ type DB struct {
ops struct { ops struct {
writeAt func(b []byte, off int64) (n int, err error) writeAt func(b []byte, off int64) (n int, err error)
} }
// Read only mode.
// When true, Update() and Begin(true) return ErrDatabaseReadOnly immediately.
readOnly bool
} }
// Path returns the path to currently open database file. // Path returns the path to currently open database file.
@ -137,19 +141,28 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
db.MaxBatchSize = DefaultMaxBatchSize db.MaxBatchSize = DefaultMaxBatchSize
db.MaxBatchDelay = DefaultMaxBatchDelay db.MaxBatchDelay = DefaultMaxBatchDelay
flag := os.O_RDWR
if options.ReadOnly {
flag = os.O_RDONLY
db.readOnly = true
}
// Open data file and separate sync handler for metadata writes. // Open data file and separate sync handler for metadata writes.
db.path = path db.path = path
var err error var err error
if db.file, err = os.OpenFile(db.path, os.O_RDWR|os.O_CREATE, mode); err != nil { if db.file, err = os.OpenFile(db.path, flag|os.O_CREATE, mode); err != nil {
_ = db.close() _ = db.close()
return nil, err return nil, err
} }
// Lock file so that other processes using Bolt cannot use the database // Lock file so that other processes using Bolt in read-write mode cannot
// at the same time. This would cause corruption since the two processes // use the database at the same time. This would cause corruption since
// would write meta pages and free pages separately. // the two processes would write meta pages and free pages separately.
if err := flock(db.file, options.Timeout); err != nil { // The database file is locked exclusively (only one process can grab the lock)
// if !options.ReadOnly.
// The database file is locked using the shared lock (more than one process may
// hold a lock at the same time) otherwise (options.ReadOnly is set).
if err := flock(db.file, !db.readOnly, options.Timeout); err != nil {
_ = db.close() _ = db.close()
return nil, err return nil, err
} }
@ -359,8 +372,11 @@ func (db *DB) close() error {
// Close file handles. // Close file handles.
if db.file != nil { if db.file != nil {
// Unlock the file. // No need to unlock read-only file.
_ = funlock(db.file) if !db.readOnly {
// Unlock the file.
_ = funlock(db.file)
}
// Close the file descriptor. // Close the file descriptor.
if err := db.file.Close(); err != nil { if err := db.file.Close(); err != nil {
@ -426,6 +442,11 @@ func (db *DB) beginTx() (*Tx, error) {
} }
func (db *DB) beginRWTx() (*Tx, error) { func (db *DB) beginRWTx() (*Tx, error) {
// If the database was opened with Options.ReadOnly, return an error.
if db.readOnly {
return nil, ErrDatabaseReadOnly
}
// Obtain writer lock. This is released by the transaction when it closes. // Obtain writer lock. This is released by the transaction when it closes.
// This enforces only one writer transaction at a time. // This enforces only one writer transaction at a time.
db.rwlock.Lock() db.rwlock.Lock()
@ -622,6 +643,10 @@ func (db *DB) allocate(count int) (*page, error) {
return p, nil return p, nil
} }
func (db *DB) IsReadOnly() bool {
return db.readOnly
}
// Options represents the options that can be set when opening a database. // Options represents the options that can be set when opening a database.
type Options struct { type Options struct {
// Timeout is the amount of time to wait to obtain a file lock. // Timeout is the amount of time to wait to obtain a file lock.
@ -631,6 +656,10 @@ type Options struct {
// Sets the DB.NoGrowSync flag before memory mapping the file. // Sets the DB.NoGrowSync flag before memory mapping the file.
NoGrowSync bool NoGrowSync bool
// Open database in read-only mode. Uses flock(..., LOCK_SH |LOCK_NB) to
// grab a shared lock (UNIX).
ReadOnly bool
} }
// DefaultOptions represent the options used if nil options are passed into Open(). // DefaultOptions represent the options used if nil options are passed into Open().

View File

@ -224,6 +224,76 @@ func TestDB_Open_FileTooSmall(t *testing.T) {
equals(t, errors.New("file size too small"), err) equals(t, errors.New("file size too small"), err)
} }
// Ensure that a database can be opened in read-only mode by multiple processes
// and that a database can not be opened in read-write mode and in read-only
// mode at the same time.
func TestOpen_ReadOnly(t *testing.T) {
bucket, key, value := []byte(`bucket`), []byte(`key`), []byte(`value`)
path := tempfile()
defer os.Remove(path)
// Open in read-write mode.
db, err := bolt.Open(path, 0666, nil)
ok(t, db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucket(bucket)
if err != nil {
return err
}
return b.Put(key, value)
}))
assert(t, db != nil, "")
assert(t, !db.IsReadOnly(), "")
ok(t, err)
ok(t, db.Close())
// Open in read-only mode.
db0, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true})
ok(t, err)
defer db0.Close()
// Opening in read-write mode should return an error.
_, err = bolt.Open(path, 0666, &bolt.Options{Timeout: time.Millisecond * 100})
assert(t, err != nil, "")
// And again (in read-only mode).
db1, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true})
ok(t, err)
defer db1.Close()
// Verify both read-only databases are accessible.
for _, db := range []*bolt.DB{db0, db1} {
// Verify is is in read only mode indeed.
assert(t, db.IsReadOnly(), "")
// Read-only databases should not allow updates.
assert(t,
bolt.ErrDatabaseReadOnly == db.Update(func(*bolt.Tx) error {
panic(`should never get here`)
}),
"")
// Read-only databases should not allow beginning writable txns.
_, err = db.Begin(true)
assert(t, bolt.ErrDatabaseReadOnly == err, "")
// Verify the data.
ok(t, db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return fmt.Errorf("expected bucket `%s`", string(bucket))
}
got := string(b.Get(key))
expected := string(value)
if got != expected {
return fmt.Errorf("expected `%s`, got `%s`", expected, got)
}
return nil
}))
}
}
// TODO(benbjohnson): Test corruption at every byte of the first two pages. // TODO(benbjohnson): Test corruption at every byte of the first two pages.
// Ensure that a database cannot open a transaction when it's not open. // Ensure that a database cannot open a transaction when it's not open.

View File

@ -36,6 +36,10 @@ var (
// ErrTxClosed is returned when committing or rolling back a transaction // ErrTxClosed is returned when committing or rolling back a transaction
// that has already been committed or rolled back. // that has already been committed or rolled back.
ErrTxClosed = errors.New("tx closed") ErrTxClosed = errors.New("tx closed")
// ErrDatabaseReadOnly is returned when a mutating transaction is started on a
// read-only database.
ErrDatabaseReadOnly = errors.New("database is in read-only mode")
) )
// These errors can occur when putting or deleting a value or a bucket. // These errors can occur when putting or deleting a value or a bucket.