Skip to content

Commit

Permalink
Merge pull request #381 from ahrtr/loadfreepage_20230111
Browse files Browse the repository at this point in the history
add PreLoadFreelist to support loading free pages in readonly mode
  • Loading branch information
ahrtr authored Jan 11, 2023
2 parents 51c763c + d2fae80 commit ad85400
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 4 deletions.
10 changes: 8 additions & 2 deletions cmd/bbolt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 31 additions & 0 deletions cmd/bbolt/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down
124 changes: 124 additions & 0 deletions db_whitebox_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package bbolt

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestOpenWithPreLoadFreelist(t *testing.T) {
testCases := []struct {
name string
readonly bool
preLoadFreePage bool
expectedFreePagesLoaded bool
}{
{
name: "write mode always load free pages",
readonly: false,
preLoadFreePage: false,
expectedFreePagesLoaded: true,
},
{
name: "readonly mode load free pages when flag set",
readonly: true,
preLoadFreePage: true,
expectedFreePagesLoaded: true,
},
{
name: "readonly mode doesn't load free pages when flag not set",
readonly: true,
preLoadFreePage: false,
expectedFreePagesLoaded: false,
},
}

fileName, err := prepareData(t)
require.NoError(t, err)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
db, err := Open(fileName, 0666, &Options{
ReadOnly: tc.readonly,
PreLoadFreelist: tc.preLoadFreePage,
})
require.NoError(t, err)

assert.Equal(t, tc.expectedFreePagesLoaded, db.freelist != nil)

assert.NoError(t, db.Close())
})
}
}

func TestMethodPage(t *testing.T) {
testCases := []struct {
name string
readonly bool
preLoadFreePage bool
expectedError error
}{
{
name: "write mode",
readonly: false,
preLoadFreePage: false,
expectedError: nil,
},
{
name: "readonly mode with preloading free pages",
readonly: true,
preLoadFreePage: true,
expectedError: nil,
},
{
name: "readonly mode without preloading free pages",
readonly: true,
preLoadFreePage: false,
expectedError: ErrFreePagesNotLoaded,
},
}

fileName, err := prepareData(t)
require.NoError(t, err)

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
db, err := Open(fileName, 0666, &Options{
ReadOnly: tc.readonly,
PreLoadFreelist: tc.preLoadFreePage,
})
require.NoError(t, err)
defer db.Close()

tx, err := db.Begin(!tc.readonly)
require.NoError(t, err)

_, err = tx.Page(0)
require.Equal(t, tc.expectedError, err)

if tc.readonly {
require.NoError(t, tx.Rollback())
} else {
require.NoError(t, tx.Commit())
}

require.NoError(t, db.Close())
})
}
}

func prepareData(t *testing.T) (string, error) {
fileName := filepath.Join(t.TempDir(), "db")
db, err := Open(fileName, 0666, nil)
if err != nil {
return "", err
}
if err := db.Close(); err != nil {
return "", err
}

return fileName, nil
}
4 changes: 4 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ var (
// ErrDatabaseReadOnly is returned when a mutating transaction is started on a
// read-only database.
ErrDatabaseReadOnly = errors.New("database is in read-only mode")

// ErrFreePagesNotLoaded is returned when a readonly transaction without
// preloading the free pages is trying to access the free pages.
ErrFreePagesNotLoaded = errors.New("free pages are not pre-loaded")
)

// These errors can occur when putting or deleting a value or a bucket.
Expand Down
4 changes: 4 additions & 0 deletions tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,10 @@ func (tx *Tx) Page(id int) (*PageInfo, error) {
return nil, nil
}

if tx.db.freelist == nil {
return nil, ErrFreePagesNotLoaded
}

// Build the page info.
p := tx.db.page(pgid(id))
info := &PageInfo{
Expand Down

0 comments on commit ad85400

Please sign in to comment.