From 019bf5b010caa9b546c17add2ddd68d58c58fc10 Mon Sep 17 00:00:00 2001 From: sasha-s Date: Thu, 14 May 2015 15:43:13 -0700 Subject: [PATCH 1/4] open read-only databases in read-only mode --- bolt_unix.go | 2 +- bolt_windows.go | 2 +- db.go | 53 ++++++++++++++++++++++++++++++++++++----------- db_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ errors.go | 4 ++++ 5 files changed, 102 insertions(+), 14 deletions(-) diff --git a/bolt_unix.go b/bolt_unix.go index 35dce08..cc958d4 100644 --- a/bolt_unix.go +++ b/bolt_unix.go @@ -45,7 +45,7 @@ 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 err := db.file.Truncate(int64(sz)); err != nil { + if err := db.ops.Truncate(int64(sz)); err != nil { return fmt.Errorf("file resize error: %s", err) } if err := db.file.Sync(); err != nil { diff --git a/bolt_windows.go b/bolt_windows.go index c8539d4..cfece39 100644 --- a/bolt_windows.go +++ b/bolt_windows.go @@ -29,7 +29,7 @@ func funlock(f *os.File) error { // 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 { + if err := db.ops.Truncate(int64(sz)); err != nil { return fmt.Errorf("truncate: %s", err) } diff --git a/db.go b/db.go index b78640f..5cf0533 100644 --- a/db.go +++ b/db.go @@ -102,8 +102,11 @@ type DB struct { statlock sync.RWMutex // Protects stats access. ops struct { - writeAt func(b []byte, off int64) (n int, err error) + writeAt func(b []byte, off int64) (n int, err error) + Truncate func(size int64) error } + + readOnly bool // Read only mode. Update()/Begin(true) would return ErrDatabaseReadOnly immediately. } // Path returns the path to currently open database file. @@ -137,21 +140,37 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { db.MaxBatchSize = DefaultMaxBatchSize db.MaxBatchDelay = DefaultMaxBatchDelay + // Get file stats. + s, err := os.Stat(path) + flag := os.O_RDWR + if err != nil && !os.IsNotExist(err) { + return nil, err + } else if err == nil && (s.Mode().Perm()&0222) == 0 { + // remove www from mode as well. + mode ^= (mode & 0222) + flag = os.O_RDONLY + db.readOnly = true + // Ignore truncations. + db.ops.Truncate = func(int64) error { return nil } + } + // 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 { - _ = db.close() - return nil, err + // No need to lock read-only file. + if !db.readOnly { + db.ops.Truncate = db.file.Truncate + // 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 { + _ = db.close() + return nil, err + } } // Default values for test hooks @@ -359,8 +378,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 +448,9 @@ func (db *DB) beginTx() (*Tx, error) { } func (db *DB) beginRWTx() (*Tx, 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 +647,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. diff --git a/db_test.go b/db_test.go index ad17e87..b3d41f3 100644 --- a/db_test.go +++ b/db_test.go @@ -224,6 +224,61 @@ 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. +func TestOpen_ReadOnly(t *testing.T) { + var bucket = []byte(`bucket`) + var key = []byte(`key`) + var value = []byte(`value`) + path := tempfile() + defer os.Remove(path) + 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()) + // Make it read-only. + ok(t, os.Chmod(path, 0444)) + // Open again. + db0, err := bolt.Open(path, 0666, nil) + ok(t, err) + defer db0.Close() + // And again. + db1, err := bolt.Open(path, 0666, nil) + ok(t, err) + defer db1.Close() + for _, db := range []*bolt.DB{db0, db1} { + // Verify is is in read only mode indeed. + assert(t, db.IsReadOnly(), "") + assert(t, + bolt.ErrDatabaseReadOnly == db.Update(func(*bolt.Tx) error { + panic(`should never get here`) + }), + "") + _, 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. From fda75748b51d0dade234b5f6438bc8a2f8d71d23 Mon Sep 17 00:00:00 2001 From: sasha-s Date: Mon, 18 May 2015 11:07:12 -0700 Subject: [PATCH 2/4] use a shared lock in read-only mode https://github.com/boltdb/bolt/pull/371#issuecomment-103119486 --- bolt_unix.go | 8 ++++++-- bolt_windows.go | 2 +- db.go | 33 ++++++++++++++++++--------------- db_test.go | 17 ++++++++++------- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/bolt_unix.go b/bolt_unix.go index cc958d4..8107f4a 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 { diff --git a/bolt_windows.go b/bolt_windows.go index cfece39..783b633 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 } diff --git a/db.go b/db.go index 5cf0533..5ae35cc 100644 --- a/db.go +++ b/db.go @@ -140,14 +140,8 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { db.MaxBatchSize = DefaultMaxBatchSize db.MaxBatchDelay = DefaultMaxBatchDelay - // Get file stats. - s, err := os.Stat(path) flag := os.O_RDWR - if err != nil && !os.IsNotExist(err) { - return nil, err - } else if err == nil && (s.Mode().Perm()&0222) == 0 { - // remove www from mode as well. - mode ^= (mode & 0222) + if options.ReadOnly { flag = os.O_RDONLY db.readOnly = true // Ignore truncations. @@ -156,21 +150,26 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { // Open data file and separate sync handler for metadata writes. db.path = path + var err error if db.file, err = os.OpenFile(db.path, flag|os.O_CREATE, mode); err != nil { _ = db.close() return nil, err } - // No need to lock read-only file. if !db.readOnly { db.ops.Truncate = db.file.Truncate - // 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 { - _ = db.close() - return nil, err - } + } + + // 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 } // Default values for test hooks @@ -660,6 +659,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 b3d41f3..64eb923 100644 --- a/db_test.go +++ b/db_test.go @@ -224,7 +224,9 @@ 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. +// 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) { var bucket = []byte(`bucket`) var key = []byte(`key`) @@ -243,14 +245,15 @@ func TestOpen_ReadOnly(t *testing.T) { assert(t, !db.IsReadOnly(), "") ok(t, err) ok(t, db.Close()) - // Make it read-only. - ok(t, os.Chmod(path, 0444)) - // Open again. - db0, err := bolt.Open(path, 0666, nil) + // Open in read-only mode. + db0, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true}) ok(t, err) defer db0.Close() - // And again. - db1, err := bolt.Open(path, 0666, nil) + // Try opening in regular mode. + _, 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() for _, db := range []*bolt.DB{db0, db1} { From aa13f7f94f882fa0964b801c062217c0b9a45436 Mon Sep 17 00:00:00 2001 From: sasha-s Date: Mon, 18 May 2015 12:00:40 -0700 Subject: [PATCH 3/4] make ignoring Truncate() explicit https://github.com/boltdb/bolt/pull/371#issuecomment-103176330 --- bolt_unix.go | 4 ++-- bolt_windows.go | 8 +++++--- db.go | 7 ------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/bolt_unix.go b/bolt_unix.go index 8107f4a..266222a 100644 --- a/bolt_unix.go +++ b/bolt_unix.go @@ -48,8 +48,8 @@ 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 err := db.ops.Truncate(int64(sz)); err != nil { + if !db.NoGrowSync && !db.readOnly { + if err := db.file.Truncate(int64(sz)); err != nil { return fmt.Errorf("file resize error: %s", err) } if err := db.file.Sync(); err != nil { diff --git a/bolt_windows.go b/bolt_windows.go index 783b633..8b782be 100644 --- a/bolt_windows.go +++ b/bolt_windows.go @@ -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.ops.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 5ae35cc..d1b722a 100644 --- a/db.go +++ b/db.go @@ -103,7 +103,6 @@ type DB struct { ops struct { writeAt func(b []byte, off int64) (n int, err error) - Truncate func(size int64) error } readOnly bool // Read only mode. Update()/Begin(true) would return ErrDatabaseReadOnly immediately. @@ -144,8 +143,6 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { if options.ReadOnly { flag = os.O_RDONLY db.readOnly = true - // Ignore truncations. - db.ops.Truncate = func(int64) error { return nil } } // Open data file and separate sync handler for metadata writes. @@ -156,10 +153,6 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { return nil, err } - if !db.readOnly { - db.ops.Truncate = db.file.Truncate - } - // 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. From df52bd0803e5062c92eb947ef63fdf0dd74c5753 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Mon, 18 May 2015 13:45:02 -0600 Subject: [PATCH 4/4] Add test case inline documentation. --- db.go | 8 ++++++-- db_test.go | 20 ++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/db.go b/db.go index d1b722a..6592b38 100644 --- a/db.go +++ b/db.go @@ -102,10 +102,12 @@ type DB struct { statlock sync.RWMutex // Protects stats access. ops struct { - writeAt func(b []byte, off int64) (n int, err error) + writeAt func(b []byte, off int64) (n int, err error) } - readOnly bool // Read only mode. Update()/Begin(true) would return ErrDatabaseReadOnly immediately. + // Read only mode. + // When true, Update() and Begin(true) return ErrDatabaseReadOnly immediately. + readOnly bool } // Path returns the path to currently open database file. @@ -440,9 +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() diff --git a/db_test.go b/db_test.go index 64eb923..6af6423 100644 --- a/db_test.go +++ b/db_test.go @@ -228,11 +228,12 @@ func TestDB_Open_FileTooSmall(t *testing.T) { // 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) { - var bucket = []byte(`bucket`) - var key = []byte(`key`) - var value = []byte(`value`) + 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) @@ -245,33 +246,44 @@ func TestOpen_ReadOnly(t *testing.T) { 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() - // Try opening in regular mode. + + // 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 {