From 05bfb3bd9a086466d15e1d84cd681c48c28fc9bc Mon Sep 17 00:00:00 2001 From: Anthony Romano Date: Mon, 24 Jul 2017 18:42:20 -0700 Subject: [PATCH] rebuild freelist when opening with FreelistSync after NoFreelistSync Writes pgidNoFreelist to the meta freelist page to detect when freelists haven't been synced down. Fixes #5 --- db.go | 28 ++++++++++++++++++---- db_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tx.go | 6 ++++- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/db.go b/db.go index a6ab73c9d..c9d800fcc 100644 --- a/db.go +++ b/db.go @@ -24,6 +24,8 @@ const version = 2 // Represents a marker value to indicate that a file is a Bolt DB. const magic uint32 = 0xED0CDAED +const pgidNoFreelist pgid = 0xffffffffffffffff + // IgnoreNoSync specifies whether the NoSync field of a DB is ignored when // syncing changes to a file. This is required as some operating systems, // such as OpenBSD, do not have a unified buffer cache (UBC) and writes @@ -239,14 +241,29 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { return nil, err } - if db.NoFreelistSync { - db.freelist = newFreelist() + db.freelist = newFreelist() + noFreeList := db.meta().freelist == pgidNoFreelist + if noFreeList { + // Reconstruct free list by scanning the DB. db.freelist.readIDs(db.freepages()) } else { - // Read in the freelist. - db.freelist = newFreelist() + // Read free list from freelist page. db.freelist.read(db.page(db.meta().freelist)) } + db.stats.FreePageN = len(db.freelist.ids) + + // Flush freelist when transitioning from no sync to sync so + // NoFreelistSync unaware boltdb can open the db later. + if !db.NoFreelistSync && noFreeList && ((mode & 0222) != 0) { + tx, err := db.Begin(true) + if tx != nil { + err = tx.Commit() + } + if err != nil { + _ = db.close() + return nil, err + } + } // Mark the database as opened and return. return db, nil @@ -1065,7 +1082,8 @@ func (m *meta) copy(dest *meta) { func (m *meta) write(p *page) { if m.root.root >= m.pgid { panic(fmt.Sprintf("root bucket pgid (%d) above high water mark (%d)", m.root.root, m.pgid)) - } else if m.freelist >= m.pgid { + } else if m.freelist >= m.pgid && m.freelist != pgidNoFreelist { + // TODO: reject pgidNoFreeList if !NoFreelistSync panic(fmt.Sprintf("freelist pgid (%d) above high water mark (%d)", m.freelist, m.pgid)) } diff --git a/db_test.go b/db_test.go index 7a9afc276..f667eaef6 100644 --- a/db_test.go +++ b/db_test.go @@ -422,6 +422,74 @@ func TestDB_Open_InitialMmapSize(t *testing.T) { } } +// TestOpen_RecoverFreeList tests opening the DB with free-list +// write-out after no free list sync will recover the free list +// and write it out. +func TestOpen_RecoverFreeList(t *testing.T) { + db := MustOpenWithOption(&bolt.Options{NoFreelistSync: true}) + defer db.MustClose() + + // Write some pages. + tx, err := db.Begin(true) + if err != nil { + t.Fatal(err) + } + wbuf := make([]byte, 8192) + for i := 0; i < 100; i++ { + s := fmt.Sprintf("%d", i) + b, err := tx.CreateBucket([]byte(s)) + if err != nil { + t.Fatal(err) + } + if err = b.Put([]byte(s), wbuf); err != nil { + t.Fatal(err) + } + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + + // Generate free pages. + if tx, err = db.Begin(true); err != nil { + t.Fatal(err) + } + for i := 0; i < 50; i++ { + s := fmt.Sprintf("%d", i) + b := tx.Bucket([]byte(s)) + if b == nil { + t.Fatal(err) + } + if err := b.Delete([]byte(s)); err != nil { + t.Fatal(err) + } + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + if err := db.DB.Close(); err != nil { + t.Fatal(err) + } + + // Record freelist count from opening with NoFreelistSync. + db.MustReopen() + freepages := db.Stats().FreePageN + if freepages == 0 { + t.Fatalf("no free pages on NoFreelistSync reopen") + } + if err := db.DB.Close(); err != nil { + t.Fatal(err) + } + + // Check free page count is reconstructed when opened with freelist sync. + db.o = &bolt.Options{} + db.MustReopen() + // One less free page for syncing the free list on open. + freepages-- + if fp := db.Stats().FreePageN; fp < freepages { + t.Fatalf("closed with %d free pages, opened with %d", freepages, fp) + } +} + // Ensure that a database cannot open a transaction when it's not open. func TestDB_Begin_ErrDatabaseNotOpen(t *testing.T) { var db bolt.DB diff --git a/tx.go b/tx.go index 5370e5fe1..fa1fa5095 100644 --- a/tx.go +++ b/tx.go @@ -174,6 +174,8 @@ func (tx *Tx) Commit() error { if err != nil { return err } + } else { + tx.meta.freelist = pgidNoFreelist } // Write dirty pages to disk. @@ -223,7 +225,9 @@ func (tx *Tx) commitFreelist() error { // Free the freelist and allocate new pages for it. This will overestimate // the size of the freelist but not underestimate the size (which would be bad). - tx.db.freelist.free(tx.meta.txid, tx.db.page(tx.meta.freelist)) + if tx.meta.freelist != pgidNoFreelist { + tx.db.freelist.free(tx.meta.txid, tx.db.page(tx.meta.freelist)) + } p, err := tx.allocate((tx.db.freelist.size() / tx.db.pageSize) + 1) if err != nil { tx.rollback()