mirror of https://github.com/hak5/bolt.git
Merge branch 'ro'
commit
d4363a9208
10
bolt_unix.go
10
bolt_unix.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
45
db.go
|
@ -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().
|
||||||
|
|
70
db_test.go
70
db_test.go
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue