From 6bc57389f0adca87ca761c1e8a252bbd706e1fc1 Mon Sep 17 00:00:00 2001 From: Benjamin Wang Date: Wed, 11 Jan 2023 10:42:53 +0800 Subject: [PATCH] add PreLoadFreelist to support loading free pages in readonly mode Signed-off-by: Benjamin Wang --- cmd/bbolt/main.go | 10 ++++++++-- cmd/bbolt/main_test.go | 31 +++++++++++++++++++++++++++++++ db.go | 20 ++++++++++++++++++-- tx.go | 3 +++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/cmd/bbolt/main.go b/cmd/bbolt/main.go index b92832e..ff24df0 100644 --- a/cmd/bbolt/main.go +++ b/cmd/bbolt/main.go @@ -195,7 +195,10 @@ func (cmd *CheckCommand) Run(args ...string) error { } // Open database. - db, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true}) + db, err := bolt.Open(path, 0666, &bolt.Options{ + ReadOnly: true, + PreLoadFreelist: true, + }) if err != nil { return err } @@ -644,7 +647,10 @@ func (cmd *PagesCommand) Run(args ...string) error { } // Open database. - db, err := bolt.Open(path, 0666, &bolt.Options{ReadOnly: true}) + db, err := bolt.Open(path, 0666, &bolt.Options{ + ReadOnly: true, + PreLoadFreelist: true, + }) if err != nil { return err } diff --git a/cmd/bbolt/main_test.go b/cmd/bbolt/main_test.go index 382b7fa..7d0cfd2 100644 --- a/cmd/bbolt/main_test.go +++ b/cmd/bbolt/main_test.go @@ -255,6 +255,37 @@ func TestGetCommand_Run(t *testing.T) { } } +// Ensure the "pages" command neither panic, nor change the db file. +func TestPagesCommand_Run(t *testing.T) { + db := btesting.MustCreateDB(t) + + err := db.Update(func(tx *bolt.Tx) error { + for _, name := range []string{"foo", "bar"} { + b, err := tx.CreateBucket([]byte(name)) + if err != nil { + return err + } + for i := 0; i < 3; i++ { + key := fmt.Sprintf("%s-%d", name, i) + val := fmt.Sprintf("val-%s-%d", name, i) + if err := b.Put([]byte(key), []byte(val)); err != nil { + return err + } + } + } + return nil + }) + require.NoError(t, err) + db.Close() + + defer requireDBNoChange(t, dbData(t, db.Path()), db.Path()) + + // Run the command. + m := NewMain() + err = m.Run("pages", db.Path()) + require.NoError(t, err) +} + // Main represents a test wrapper for main.Main that records output. type Main struct { *main.Main diff --git a/db.go b/db.go index a74c2b6..d9749f4 100644 --- a/db.go +++ b/db.go @@ -95,6 +95,11 @@ type DB struct { // https://github.com/boltdb/bolt/issues/284 NoGrowSync bool + // When `true`, bbolt will always load the free pages when opening the DB. + // When opening db in write mode, this flag will always automatically + // set to `true`. + PreLoadFreelist bool + // If you want to read the entire database fast, you can set MmapFlag to // syscall.MAP_POPULATE on Linux 2.6.23+ for sequential read-ahead. MmapFlags int @@ -196,6 +201,7 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { db.NoGrowSync = options.NoGrowSync db.MmapFlags = options.MmapFlags db.NoFreelistSync = options.NoFreelistSync + db.PreLoadFreelist = options.PreLoadFreelist db.FreelistType = options.FreelistType db.Mlock = options.Mlock @@ -208,6 +214,9 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { if options.ReadOnly { flag = os.O_RDONLY db.readOnly = true + } else { + // always load free pages in write mode + db.PreLoadFreelist = true } db.openFile = options.OpenFile @@ -277,12 +286,14 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { return nil, err } + if db.PreLoadFreelist { + db.loadFreelist() + } + if db.readOnly { return db, nil } - db.loadFreelist() - // Flush freelist when transitioning from no sync to sync so // NoFreelistSync unaware boltdb can open the db later. if !db.NoFreelistSync && !db.hasSyncedFreelist() { @@ -1163,6 +1174,11 @@ type Options struct { // under normal operation, but requires a full database re-sync during recovery. NoFreelistSync bool + // PreLoadFreelist sets whether to load the free pages when opening + // the db file. Note when opening db in write mode, bbolt will always + // load the free pages. + PreLoadFreelist bool + // FreelistType sets the backend freelist type. There are two options. Array which is simple but endures // dramatic performance degradation if database is large and framentation in freelist is common. // The alternative one is using hashmap, it is faster in almost all circumstances diff --git a/tx.go b/tx.go index f8119a0..3f3eda7 100644 --- a/tx.go +++ b/tx.go @@ -652,6 +652,9 @@ func (tx *Tx) Page(id int) (*PageInfo, error) { return nil, nil } + // Force loading free list if opened in ReadOnly mode. + tx.db.loadFreelist() + // Build the page info. p := tx.db.page(pgid(id)) info := &PageInfo{