Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bolt_openbsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
50 changes: 38 additions & 12 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions mmap_error_unix.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions mmap_error_windows.go
Original file line number Diff line number Diff line change
@@ -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)
}
51 changes: 51 additions & 0 deletions mmap_fallback.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down