mirror of https://github.com/etcd-io/bbolt.git
369 lines
9.6 KiB
Go
369 lines
9.6 KiB
Go
package failpoint
|
|
|
|
import (
|
|
crand "crypto/rand"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
"go.etcd.io/bbolt/errors"
|
|
"go.etcd.io/bbolt/internal/btesting"
|
|
"go.etcd.io/bbolt/internal/common"
|
|
"go.etcd.io/bbolt/internal/guts_cli"
|
|
gofail "go.etcd.io/gofail/runtime"
|
|
)
|
|
|
|
func TestFailpoint_MapFail(t *testing.T) {
|
|
err := gofail.Enable("mapError", `return("map somehow failed")`)
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
err = gofail.Disable("mapError")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
f := filepath.Join(t.TempDir(), "db")
|
|
_, err = bolt.Open(f, 0600, nil)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "map somehow failed")
|
|
}
|
|
|
|
// ensures when munmap fails, the flock is unlocked
|
|
func TestFailpoint_UnmapFail_DbClose(t *testing.T) {
|
|
//unmap error on db close
|
|
//we need to open the db first, and then enable the error.
|
|
//otherwise the db cannot be opened.
|
|
f := filepath.Join(t.TempDir(), "db")
|
|
|
|
err := gofail.Enable("unmapError", `return("unmap somehow failed")`)
|
|
require.NoError(t, err)
|
|
_, err = bolt.Open(f, 0600, nil)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "unmap somehow failed")
|
|
//disable the error, and try to reopen the db
|
|
err = gofail.Disable("unmapError")
|
|
require.NoError(t, err)
|
|
|
|
db, err := bolt.Open(f, 0600, &bolt.Options{Timeout: 30 * time.Second})
|
|
require.NoError(t, err)
|
|
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, 0600, &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, 0600, &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)
|
|
}
|
|
|
|
func TestFailpoint_ResizeFileFail(t *testing.T) {
|
|
db := btesting.MustCreateDB(t)
|
|
|
|
err := gofail.Enable("resizeFileError", `return("resizeFile somehow failed")`)
|
|
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, "resizeFile somehow failed")
|
|
|
|
// It should work after disabling the failpoint.
|
|
err = gofail.Disable("resizeFileError")
|
|
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)
|
|
}
|
|
|
|
func TestFailpoint_LackOfDiskSpace(t *testing.T) {
|
|
db := btesting.MustCreateDB(t)
|
|
|
|
err := gofail.Enable("lackOfDiskSpace", `return("grow somehow failed")`)
|
|
require.NoError(t, err)
|
|
|
|
tx, err := db.Begin(true)
|
|
require.NoError(t, err)
|
|
|
|
err = tx.Commit()
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "grow somehow failed")
|
|
|
|
err = tx.Rollback()
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, errors.ErrTxClosed)
|
|
|
|
// It should work after disabling the failpoint.
|
|
err = gofail.Disable("lackOfDiskSpace")
|
|
require.NoError(t, err)
|
|
|
|
tx, err = db.Begin(true)
|
|
require.NoError(t, err)
|
|
|
|
err = tx.Commit()
|
|
require.NoError(t, err)
|
|
|
|
err = tx.Rollback()
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, errors.ErrTxClosed)
|
|
}
|
|
|
|
// TestIssue72 reproduces issue 72.
|
|
//
|
|
// When bbolt is processing a `Put` invocation, the key might be concurrently
|
|
// updated by the application which calls the `Put` API (although it shouldn't).
|
|
// It might lead to a situation that bbolt use an old key to find a proper
|
|
// position to insert the key/value pair, but actually inserts a new key.
|
|
// Eventually it might break the rule that all keys should be sorted. In a
|
|
// worse case, it might cause page elements to point to already freed pages.
|
|
//
|
|
// REF: https://github.com/etcd-io/bbolt/issues/72
|
|
func TestIssue72(t *testing.T) {
|
|
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: 4096})
|
|
|
|
bucketName := []byte(t.Name())
|
|
err := db.Update(func(tx *bolt.Tx) error {
|
|
_, txerr := tx.CreateBucket(bucketName)
|
|
return txerr
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// The layout is like:
|
|
//
|
|
// +--+--+--+
|
|
// +------+1 |3 |10+---+
|
|
// | +-++--+--+ |
|
|
// | | |
|
|
// | | |
|
|
// +v-+--+ +v-+--+ +-v+--+--+
|
|
// |1 |2 | |3 |4 | |10|11|12|
|
|
// +--+--+ +--+--+ +--+--+--+
|
|
//
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
bk := tx.Bucket(bucketName)
|
|
|
|
for _, id := range []int{1, 2, 3, 4, 10, 11, 12} {
|
|
if txerr := bk.Put(idToBytes(id), make([]byte, 1000)); txerr != nil {
|
|
return txerr
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, gofail.Enable("beforeBucketPut", `sleep(5000)`))
|
|
|
|
// +--+--+--+
|
|
// +------+1 |3 |1 +---+
|
|
// | +-++--+--+ |
|
|
// | | |
|
|
// | | |
|
|
// +v-+--+ +v-+--+ +-v+--+--+--+
|
|
// |1 |2 | |3 |4 | |1 |10|11|12|
|
|
// +--+--+ +--+--+ +--+--+--+--+
|
|
//
|
|
key := idToBytes(13)
|
|
updatedKey := idToBytes(1)
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
bk := tx.Bucket(bucketName)
|
|
|
|
go func() {
|
|
time.Sleep(3 * time.Second)
|
|
copy(key, updatedKey)
|
|
}()
|
|
return bk.Put(key, make([]byte, 100))
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, gofail.Disable("beforeBucketPut"))
|
|
|
|
// bbolt inserts 100 into last branch page. Since there are two `1`
|
|
// keys in branch, spill operation will update first `1` pointer and
|
|
// then last one won't be updated and continues to point to freed page.
|
|
//
|
|
//
|
|
// +--+--+--+
|
|
// +---------------+1 |3 |1 +---------+
|
|
// | +--++-+--+ |
|
|
// | | |
|
|
// | | |
|
|
// | +--+--+ +v-+--+ +-----v-----+
|
|
// | |1 |2 | |3 |4 | |freed page |
|
|
// | +--+--+ +--+--+ +-----------+
|
|
// |
|
|
// +v-+--+--+--+---+
|
|
// |1 |10|11|12|100|
|
|
// +--+--+--+--+---+
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
return tx.Bucket(bucketName).Put(idToBytes(100), make([]byte, 100))
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("panic info:\n %v", r)
|
|
}
|
|
}()
|
|
|
|
// Add more keys to ensure branch node to spill.
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
bk := tx.Bucket(bucketName)
|
|
|
|
for _, id := range []int{101, 102, 103, 104, 105} {
|
|
if txerr := bk.Put(idToBytes(id), make([]byte, 1000)); txerr != nil {
|
|
return txerr
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTx_Rollback_Freelist(t *testing.T) {
|
|
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: 4096})
|
|
|
|
bucketName := []byte("data")
|
|
|
|
t.Log("Populate some data to have at least 5 leaf pages.")
|
|
var keys []string
|
|
err := db.Update(func(tx *bolt.Tx) error {
|
|
b, terr := tx.CreateBucket(bucketName)
|
|
if terr != nil {
|
|
return terr
|
|
}
|
|
for i := 0; i <= 10; i++ {
|
|
k := fmt.Sprintf("t1_k%02d", i)
|
|
keys = append(keys, k)
|
|
|
|
v := make([]byte, 1500)
|
|
if _, terr := crand.Read(v); terr != nil {
|
|
return terr
|
|
}
|
|
|
|
if terr := b.Put([]byte(k), v); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Log("Remove some keys to have at least 3 more free pages.")
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketName)
|
|
for i := 0; i < 6; i++ {
|
|
if terr := b.Delete([]byte(keys[i])); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Log("Close and then reopen the db to release all pending free pages.")
|
|
db.MustClose()
|
|
db.MustReopen()
|
|
|
|
t.Log("Enable the `beforeWriteMetaError` failpoint.")
|
|
require.NoError(t, gofail.Enable("beforeWriteMetaError", `return("writeMeta somehow failed")`))
|
|
defer func() {
|
|
t.Log("Disable the `beforeWriteMetaError` failpoint.")
|
|
require.NoError(t, gofail.Disable("beforeWriteMetaError"))
|
|
}()
|
|
|
|
beforeFreelistPgids, err := readFreelistPageIds(db.Path())
|
|
require.NoError(t, err)
|
|
require.Greater(t, len(beforeFreelistPgids), 0)
|
|
|
|
t.Log("Simulate TXN rollback")
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(bucketName)
|
|
for i := 6; i < len(keys); i++ {
|
|
v := make([]byte, 1500)
|
|
if _, terr := crand.Read(v); terr != nil {
|
|
return terr
|
|
}
|
|
// update the keys
|
|
if terr := b.Put([]byte(keys[i]), v); terr != nil {
|
|
return terr
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
require.Error(t, err)
|
|
|
|
afterFreelistPgids, err := readFreelistPageIds(db.Path())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, beforeFreelistPgids, afterFreelistPgids)
|
|
}
|
|
|
|
func idToBytes(id int) []byte {
|
|
return []byte(fmt.Sprintf("%010d", id))
|
|
}
|
|
|
|
func readFreelistPageIds(path string) ([]common.Pgid, error) {
|
|
m, _, err := guts_cli.GetActiveMetaPage(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p, _, err := guts_cli.ReadPage(path, uint64(m.Freelist()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.FreelistPageIds(), nil
|
|
}
|