From 7a13798511f9d60d1179f25b9f3b10baf3b6cae6 Mon Sep 17 00:00:00 2001 From: James Blair Date: Thu, 30 Mar 2023 21:54:05 +1300 Subject: [PATCH 1/2] Backport perform unmap when failing to mlock or both meta pages corrupted. Signed-off-by: James Blair --- db.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/db.go b/db.go index 5f45d966e..cb7f47ad0 100644 --- a/db.go +++ b/db.go @@ -424,7 +424,7 @@ func (db *DB) hasSyncedFreelist() bool { // mmap opens the underlying memory-mapped file and initializes the meta references. // minsz is the minimum size that the new mmap can be. -func (db *DB) mmap(minsz int) error { +func (db *DB) mmap(minsz int) (err error) { db.mmaplock.Lock() defer db.mmaplock.Unlock() @@ -459,17 +459,27 @@ func (db *DB) mmap(minsz int) error { } // Unmap existing data before continuing. - if err := db.munmap(); err != nil { + if err = db.munmap(); err != nil { return err } // Memory-map the data file as a byte slice. // gofail: var mapError string // return errors.New(mapError) - if err := mmap(db, size); err != nil { + if err = mmap(db, size); err != nil { return err } + // Perform unmmap on any error to reset all data fields: + // dataref, data, datasz, meta0 and meta1. + defer func() { + if err != nil { + if unmapErr := db.munmap(); unmapErr != nil { + err = fmt.Errorf("%w; rollback unmap also failed: %v", err, unmapErr) + } + } + }() + if db.Mlock { // Don't allow swapping of data file if err := db.mlock(fileSize); err != nil { @@ -553,6 +563,8 @@ func (db *DB) mmapSize(size int) (int, error) { } func (db *DB) munlock(fileSize int) error { + // gofail: var munlockError string + // return errors.New(munlockError) if err := munlock(db, fileSize); err != nil { return fmt.Errorf("munlock error: " + err.Error()) } @@ -560,6 +572,8 @@ func (db *DB) munlock(fileSize int) error { } func (db *DB) mlock(fileSize int) error { + // gofail: var mlockError string + // return errors.New(mlockError) if err := mlock(db, fileSize); err != nil { return fmt.Errorf("mlock error: " + err.Error()) } From 95acc509210bda4a3102ecc08308298ed9c753a1 Mon Sep 17 00:00:00 2001 From: James Blair Date: Thu, 30 Mar 2023 21:54:14 +1300 Subject: [PATCH 2/2] Backport add test cases to simulate mlock failure. Signed-off-by: James Blair --- tests/failpoint/db_failpoint_test.go | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/failpoint/db_failpoint_test.go b/tests/failpoint/db_failpoint_test.go index ae900b229..ef7d7ca63 100644 --- a/tests/failpoint/db_failpoint_test.go +++ b/tests/failpoint/db_failpoint_test.go @@ -1,6 +1,7 @@ package failpoint import ( + "fmt" "path/filepath" "testing" "time" @@ -8,6 +9,7 @@ import ( "github.com/stretchr/testify/require" bolt "go.etcd.io/bbolt" + "go.etcd.io/bbolt/internal/btesting" gofail "go.etcd.io/gofail/runtime" ) @@ -46,3 +48,49 @@ func TestFailpoint_UnmapFail_DbClose(t *testing.T) { err = db.Close() require.NoError(t, err) } + +func TestFailpoint_mLockFail(t *testing.T) { + err := gofail.Enable("mlockError", `return("mlock somehow failed")`) + require.NoError(t, err) + + f := filepath.Join(t.TempDir(), "db") + _, err = bolt.Open(f, 0666, &bolt.Options{Mlock: true}) + require.Error(t, err) + require.ErrorContains(t, err, "mlock somehow failed") + + // It should work after disabling the failpoint. + err = gofail.Disable("mlockError") + require.NoError(t, err) + + _, err = bolt.Open(f, 0666, &bolt.Options{Mlock: true}) + require.NoError(t, err) +} + +func TestFailpoint_mLockFail_When_remap(t *testing.T) { + db := btesting.MustCreateDB(t) + db.Mlock = true + + err := gofail.Enable("mlockError", `return("mlock somehow failed in allocate")`) + require.NoError(t, err) + + err = db.Fill([]byte("data"), 1, 10000, + func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) }, + func(tx int, k int) []byte { return make([]byte, 100) }, + ) + + require.Error(t, err) + require.ErrorContains(t, err, "mlock somehow failed in allocate") + + // It should work after disabling the failpoint. + err = gofail.Disable("mlockError") + require.NoError(t, err) + db.MustClose() + db.MustReopen() + + err = db.Fill([]byte("data"), 1, 10000, + func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) }, + func(tx int, k int) []byte { return make([]byte, 100) }, + ) + + require.NoError(t, err) +}