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/db.go b/db.go index c1c63e685..bb2991efb 100644 --- a/db.go +++ b/db.go @@ -116,15 +116,16 @@ type DB struct { // `dataref` isn't used at all on Windows, and the golangci-lint // always fails on Windows platform. //nolint - dataref []byte // mmap'ed readonly, write throws SEGV - data *[maxMapSize]byte - datasz int - meta0 *common.Meta - meta1 *common.Meta - pageSize int - opened bool - rwtx *Tx - txs []*Tx + dataref []byte // mmap'ed readonly, write throws SEGV + data *[maxMapSize]byte + datasz int + meta0 *common.Meta + meta1 *common.Meta + pageSize int + opened bool + mmapFallback bool + rwtx *Tx + txs []*Tx freelist *freelist freelistLoad sync.Once @@ -450,7 +451,13 @@ func (db *DB) mmap(minsz int) (err error) { // gofail: var mapError string // return errors.New(mapError) if err = mmap(db, size); err != nil { - return err + if !isMmapUnsupported(err) { + return err + } + mmapErr := err + if err = mmapFallback(db, size); err != nil { + return fmt.Errorf("mmap unsupported: %v; fallback error: %w", mmapErr, err) + } } // Perform unmmap on any error to reset all data fields: @@ -490,6 +497,7 @@ func (db *DB) invalidate() { db.dataref = nil db.data = nil db.datasz = 0 + db.mmapFallback = false db.meta0 = nil db.meta1 = nil @@ -501,13 +509,31 @@ 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 { return fmt.Errorf("unmap error: " + err.Error()) } return nil } +// writeAt writes bytes to the backing file and, in fallback mode, mirrors the +// write into the heap buffer so the in-memory page view stays consistent. +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. @@ -607,7 +633,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 { return err } if err := fdatasync(db); err != nil { diff --git a/mmap_error_unix.go b/mmap_error_unix.go new file mode 100644 index 000000000..85e4c68c2 --- /dev/null +++ b/mmap_error_unix.go @@ -0,0 +1,19 @@ +//go:build !windows && !plan9 + +package bbolt + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +// isMmapUnsupported reports whether err indicates the underlying file system +// does not support mmap (e.g. jffs2 returns EINVAL for MAP_SHARED). +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..3fa28e10c --- /dev/null +++ b/mmap_fallback.go @@ -0,0 +1,51 @@ +package bbolt + +import ( + "fmt" + "io" + "unsafe" +) + +// mmapFallback provides a heap-backed mirror of the data file as a substitute +// for mmap on file systems that do not support memory mapping (e.g. jffs2). +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 = (*[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 +} + +// copyToMmapFallback copies written bytes into the heap mirror so the in-memory +// page view stays consistent with the file in fallback mode. +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 d60225950..74ecac294 100644 --- a/tx.go +++ b/tx.go @@ -421,7 +421,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 { return err } @@ -472,7 +472,7 @@ func (tx *Tx) writeMeta() error { tx.meta.Write(p) // Write the meta page to file. - 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 { return err } if !tx.db.NoSync || common.IgnoreNoSync {