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