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
25 changes: 25 additions & 0 deletions bolt_plan9.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 33 additions & 7 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -567,14 +577,30 @@ 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)
}

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.
Expand Down Expand Up @@ -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
}
Expand Down
29 changes: 29 additions & 0 deletions db_whitebox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions mlock_plan9.go
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 1 addition & 1 deletion mlock_unix.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !plan9

package bbolt

Expand Down
7 changes: 7 additions & 0 deletions mmap_error_plan9.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build plan9

package bbolt

func isMmapUnsupported(error) bool {
return false
}
17 changes: 17 additions & 0 deletions mmap_error_unix.go
Original file line number Diff line number Diff line change
@@ -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)
}
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)
}
49 changes: 49 additions & 0 deletions mmap_fallback.go
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down