Skip to content

Commit

Permalink
Merge pull request #6 from heyitsanthony/restore-freelist
Browse files Browse the repository at this point in the history
rebuild freelist when opening with FreelistSync after NoFreelistSync
  • Loading branch information
heyitsanthony committed Jul 25, 2017
2 parents ad39960 + 05bfb3b commit c359265
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 6 deletions.
28 changes: 23 additions & 5 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}

Expand Down
68 changes: 68 additions & 0 deletions db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ func (tx *Tx) Commit() error {
if err != nil {
return err
}
} else {
tx.meta.freelist = pgidNoFreelist
}

// Write dirty pages to disk.
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit c359265

Please sign in to comment.