diff --git a/bolt_openbsd.go b/bolt_openbsd.go index bf47aa1a6..5bbf80bfe 100644 --- a/bolt_openbsd.go +++ b/bolt_openbsd.go @@ -9,7 +9,7 @@ func msync(db *DB) error { } func fdatasync(db *DB) error { - if db.data != nil { + if db.data != nil && !db.mmapFallback { return msync(db) } return db.file.Sync() diff --git a/bolt_plan9.go b/bolt_plan9.go new file mode 100644 index 000000000..a97e505e3 --- /dev/null +++ b/bolt_plan9.go @@ -0,0 +1,25 @@ +//go:build plan9 + +package bbolt + +import "time" + +func fdatasync(db *DB) error { + return db.file.Sync() +} + +func flock(_ *DB, _ bool, _ time.Duration) error { + return nil +} + +func funlock(_ *DB) error { + return nil +} + +func mmap(db *DB, sz int) error { + return mmapFallback(db, sz) +} + +func munmap(db *DB) error { + return munmapFallback(db) +} diff --git a/db.go b/db.go index 2ab120394..ec0e33155 100644 --- a/db.go +++ b/db.go @@ -121,10 +121,9 @@ type DB struct { path string openFile func(string, int, os.FileMode) (*os.File, error) file *os.File - // `dataref` isn't used at all on Windows, and the golangci-lint - // always fails on Windows platform. + // dataref keeps the mapped byte slice on Unix, or the heap mirror used by mmap fallback. //nolint - dataref []byte // mmap'ed readonly, write throws SEGV + dataref []byte data *[common.MaxMapSize]byte datasz int meta0 *common.Meta @@ -154,6 +153,8 @@ type DB struct { // Read only mode. // When true, Update() and Begin(true) return ErrDatabaseReadOnly immediately. readOnly bool + + mmapFallback bool } // Path returns the path to currently open database file. @@ -514,8 +515,16 @@ func (db *DB) mmap(minsz int) (err error) { // gofail: var mapError string // return errors.New(mapError) if err = mmap(db, size); err != nil { - lg.Errorf("[GOOS: %s, GOARCH: %s] mmap failed, size: %d, error: %v", runtime.GOOS, runtime.GOARCH, size, err) - return err + if !isMmapUnsupported(err) { + lg.Errorf("[GOOS: %s, GOARCH: %s] mmap failed, size: %d, error: %v", runtime.GOOS, runtime.GOARCH, size, err) + return err + } + mmapErr := err + lg.Warningf("[GOOS: %s, GOARCH: %s] mmap unsupported, size: %d, falling back to heap mirror: %v", runtime.GOOS, runtime.GOARCH, size, mmapErr) + if err = mmapFallback(db, size); err != nil { + lg.Errorf("[GOOS: %s, GOARCH: %s] mmap fallback failed, size: %d, mmap error: %v, fallback error: %v", runtime.GOOS, runtime.GOARCH, size, mmapErr, err) + return fmt.Errorf("mmap unsupported: %v; fallback error: %w", mmapErr, err) + } } // Perform unmmap on any error to reset all data fields: @@ -556,6 +565,7 @@ func (db *DB) invalidate() { db.dataref = nil db.data = nil db.datasz = 0 + db.mmapFallback = false db.meta0 = nil db.meta1 = nil @@ -567,7 +577,13 @@ func (db *DB) munmap() error { // gofail: var unmapError string // return errors.New(unmapError) - if err := munmap(db); err != nil { + var err error + if db.mmapFallback { + err = munmapFallback(db) + } else { + err = munmap(db) + } + if err != nil { db.Logger().Errorf("[GOOS: %s, GOARCH: %s] munmap failed, db.datasz: %d, error: %v", runtime.GOOS, runtime.GOARCH, db.datasz, err) return fmt.Errorf("unmap error: %w", err) } @@ -575,6 +591,16 @@ func (db *DB) munmap() error { return nil } +func (db *DB) writeAt(b []byte, off int64) (int, error) { + n, err := db.ops.writeAt(b, off) + if n > 0 && db.mmapFallback { + db.mmaplock.Lock() + db.copyToMmapFallback(b[:n], off) + db.mmaplock.Unlock() + } + return n, err +} + // mmapSize determines the appropriate size for the mmap given the current size // of the database. The minimum size is 32KB and doubles until it reaches 1GB. // Returns an error if the new mmap size is greater than the max allowed. @@ -676,7 +702,7 @@ func (db *DB) init() error { p.SetCount(0) // Write the buffer to our data file. - if _, err := db.ops.writeAt(buf, 0); err != nil { + if _, err := db.writeAt(buf, 0); err != nil { db.Logger().Errorf("writeAt failed: %w", err) return err } diff --git a/db_whitebox_test.go b/db_whitebox_test.go index 8b517fc0a..f4bb9f974 100644 --- a/db_whitebox_test.go +++ b/db_whitebox_test.go @@ -55,6 +55,35 @@ func TestOpenWithPreLoadFreelist(t *testing.T) { } } +func TestMmapFallbackReadWrite(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + db, err := Open(path, 0600, nil) + require.NoError(t, err) + defer db.Close() + + sz := db.datasz + require.NoError(t, db.munmap()) + require.NoError(t, mmapFallback(db, sz)) + db.meta0 = db.page(0).Meta() + db.meta1 = db.page(1).Meta() + + require.True(t, db.mmapFallback) + require.NoError(t, db.Update(func(tx *Tx) error { + bucket, err := tx.CreateBucket([]byte("widgets")) + if err != nil { + return err + } + return bucket.Put([]byte("key"), []byte("value")) + })) + + require.NoError(t, db.View(func(tx *Tx) error { + bucket := tx.Bucket([]byte("widgets")) + require.NotNil(t, bucket) + require.Equal(t, []byte("value"), bucket.Get([]byte("key"))) + return nil + })) +} + func TestMethodPage(t *testing.T) { testCases := []struct { name string diff --git a/mlock_plan9.go b/mlock_plan9.go new file mode 100644 index 000000000..2b2eb730e --- /dev/null +++ b/mlock_plan9.go @@ -0,0 +1,15 @@ +//go:build plan9 + +package bbolt + +import "errors" + +// mlock locks memory of db file +func mlock(_ *DB, _ int) error { + return errors.New("mlock is not supported on plan9") +} + +// munlock unlocks memory of db file +func munlock(_ *DB, _ int) error { + return errors.New("munlock is not supported on plan9") +} diff --git a/mlock_unix.go b/mlock_unix.go index 9a0fd332c..4e208427e 100644 --- a/mlock_unix.go +++ b/mlock_unix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !plan9 package bbolt diff --git a/mmap_error_plan9.go b/mmap_error_plan9.go new file mode 100644 index 000000000..c0103d1fd --- /dev/null +++ b/mmap_error_plan9.go @@ -0,0 +1,7 @@ +//go:build plan9 + +package bbolt + +func isMmapUnsupported(error) bool { + return false +} diff --git a/mmap_error_unix.go b/mmap_error_unix.go new file mode 100644 index 000000000..0a7ef3c49 --- /dev/null +++ b/mmap_error_unix.go @@ -0,0 +1,17 @@ +//go:build !windows && !plan9 + +package bbolt + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +func isMmapUnsupported(err error) bool { + return errors.Is(err, unix.ENOSYS) || + errors.Is(err, unix.ENODEV) || + errors.Is(err, unix.EOPNOTSUPP) || + errors.Is(err, unix.ENOTSUP) || + errors.Is(err, unix.EINVAL) +} diff --git a/mmap_error_windows.go b/mmap_error_windows.go new file mode 100644 index 000000000..5884aed66 --- /dev/null +++ b/mmap_error_windows.go @@ -0,0 +1,12 @@ +package bbolt + +import ( + "errors" + + "golang.org/x/sys/windows" +) + +func isMmapUnsupported(err error) bool { + return errors.Is(err, windows.ERROR_INVALID_FUNCTION) || + errors.Is(err, windows.ERROR_NOT_SUPPORTED) +} diff --git a/mmap_fallback.go b/mmap_fallback.go new file mode 100644 index 000000000..d525bd7be --- /dev/null +++ b/mmap_fallback.go @@ -0,0 +1,49 @@ +package bbolt + +import ( + "fmt" + "io" + "unsafe" + + "github.com/sagernet/bbolt/internal/common" +) + +func mmapFallback(db *DB, sz int) error { + b := make([]byte, sz) + + info, err := db.file.Stat() + if err != nil { + return fmt.Errorf("file stat: %w", err) + } + + readSize := int64(sz) + if info.Size() < readSize { + readSize = info.Size() + } + if readSize > 0 { + if _, err := db.file.ReadAt(b[:readSize], 0); err != nil && err != io.EOF { + return fmt.Errorf("file read: %w", err) + } + } + + db.dataref = b + db.data = (*[common.MaxMapSize]byte)(unsafe.Pointer(&b[0])) + db.datasz = sz + db.mmapFallback = true + return nil +} + +func munmapFallback(db *DB) error { + db.dataref = nil + db.data = nil + db.datasz = 0 + db.mmapFallback = false + return nil +} + +func (db *DB) copyToMmapFallback(b []byte, off int64) { + if off < 0 || off >= int64(len(db.dataref)) { + return + } + copy(db.dataref[off:], b) +} diff --git a/tx.go b/tx.go index fa2453432..6a124fb7e 100644 --- a/tx.go +++ b/tx.go @@ -542,7 +542,7 @@ func (tx *Tx) write() error { } buf := common.UnsafeByteSlice(unsafe.Pointer(p), written, 0, int(sz)) - if _, err := tx.db.ops.writeAt(buf, offset); err != nil { + if _, err := tx.db.writeAt(buf, offset); err != nil { lg.Errorf("writeAt failed, offset: %d: %w", offset, err) return err } @@ -604,7 +604,7 @@ func (tx *Tx) writeMeta() error { // Write the meta page to file. tx.db.metalock.Lock() - if _, err := tx.db.ops.writeAt(buf, int64(p.Id())*int64(tx.db.pageSize)); err != nil { + if _, err := tx.db.writeAt(buf, int64(p.Id())*int64(tx.db.pageSize)); err != nil { tx.db.metalock.Unlock() lg.Errorf("writeAt failed, pgid: %d, pageSize: %d, error: %v", p.Id(), tx.db.pageSize, err) return err