aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Johnson <benbjohnson@yahoo.com>2015-05-18 13:45:09 -0600
committerBen Johnson <benbjohnson@yahoo.com>2015-05-18 13:45:09 -0600
commitd4363a920813e181d1a65a7e4c2ac7bdbcbd94a1 (patch)
tree2fce56e5ba9f85b2dd8cdc2fd2255b7675cca6f7
parentMerge pull request #375 from benbjohnson/min-mmap-size (diff)
parentAdd test case inline documentation. (diff)
downloaddedo-d4363a920813e181d1a65a7e4c2ac7bdbcbd94a1.tar.gz
dedo-d4363a920813e181d1a65a7e4c2ac7bdbcbd94a1.tar.xz
Merge branch 'ro'
-rw-r--r--bolt_unix.go10
-rw-r--r--bolt_windows.go10
-rw-r--r--db.go45
-rw-r--r--db_test.go70
-rw-r--r--errors.go4
5 files changed, 124 insertions, 15 deletions
diff --git a/bolt_unix.go b/bolt_unix.go
index 35dce08..266222a 100644
--- a/bolt_unix.go
+++ b/bolt_unix.go
@@ -11,7 +11,7 @@ import (
)
// 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
for {
// 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 {
return ErrTimeout
}
+ flag := syscall.LOCK_SH
+ if exclusive {
+ flag = syscall.LOCK_EX
+ }
// 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 {
return nil
} else if err != syscall.EWOULDBLOCK {
@@ -44,7 +48,7 @@ func funlock(f *os.File) error {
func mmap(db *DB, sz int) error {
// Truncate and fsync to ensure file size metadata is flushed.
// https://github.com/boltdb/bolt/issues/284
- if !db.NoGrowSync {
+ if !db.NoGrowSync && !db.readOnly {
if err := db.file.Truncate(int64(sz)); err != nil {
return fmt.Errorf("file resize error: %s", err)
}
diff --git a/bolt_windows.go b/bolt_windows.go
index c8539d4..8b782be 100644
--- a/bolt_windows.go
+++ b/bolt_windows.go
@@ -16,7 +16,7 @@ func fdatasync(db *DB) error {
}
// 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
}
@@ -28,9 +28,11 @@ func funlock(f *os.File) error {
// mmap memory maps a DB's data file.
// Based on: https://github.com/edsrzf/mmap-go
func mmap(db *DB, sz int) error {
- // Truncate the database to the size of the mmap.
- if err := db.file.Truncate(int64(sz)); err != nil {
- return fmt.Errorf("truncate: %s", err)
+ if !db.readOnly {
+ // Truncate the database to the size of the mmap.
+ if err := db.file.Truncate(int64(sz)); err != nil {
+ return fmt.Errorf("truncate: %s", err)
+ }
}
// Open a file mapping handle.
diff --git a/db.go b/db.go
index 5b905be..c415c1b 100644
--- a/db.go
+++ b/db.go
@@ -104,6 +104,10 @@ type DB struct {
ops struct {
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.
@@ -137,19 +141,28 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
db.MaxBatchSize = DefaultMaxBatchSize
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.
db.path = path
-
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()
return nil, err
}
- // 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, options.Timeout); err != nil {
+ // Lock file so that other processes using Bolt in read-write mode cannot
+ // use the database at the same time. This would cause corruption since
+ // the two processes would write meta pages and free pages separately.
+ // 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()
return nil, err
}
@@ -359,8 +372,11 @@ func (db *DB) close() error {
// Close file handles.
if db.file != nil {
- // Unlock the file.
- _ = funlock(db.file)
+ // No need to unlock read-only file.
+ if !db.readOnly {
+ // Unlock the file.
+ _ = funlock(db.file)
+ }
// Close the file descriptor.
if err := db.file.Close(); err != nil {
@@ -426,6 +442,11 @@ func (db *DB) beginTx() (*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.
// This enforces only one writer transaction at a time.
db.rwlock.Lock()
@@ -622,6 +643,10 @@ func (db *DB) allocate(count int) (*page, error) {
return p, nil
}
+func (db *DB) IsReadOnly() bool {
+ return db.readOnly
+}
+
// 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.
@@ -631,6 +656,10 @@ type Options struct {
// Sets the DB.NoGrowSync flag before memory mapping the file.
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().
diff --git a/db_test.go b/db_test.go
index ad17e87..6af6423 100644
--- a/db_test.go
+++ b/db_test.go
@@ -224,6 +224,76 @@ func TestDB_Open_FileTooSmall(t *testing.T) {
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.
// Ensure that a database cannot open a transaction when it's not open.
diff --git a/errors.go b/errors.go
index aa504f1..6883786 100644
--- a/errors.go
+++ b/errors.go
@@ -36,6 +36,10 @@ var (
// ErrTxClosed is returned when committing or rolling back a transaction
// that has already been committed or rolled back.
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.