diff --git a/Makefile b/Makefile index 1ff13c133..9ee21cae4 100644 --- a/Makefile +++ b/Makefile @@ -42,14 +42,14 @@ lint: .PHONY: test test: @echo "hashmap freelist test" - TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} -timeout ${TESTFLAGS_TIMEOUT} - TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} ./internal/... - TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} ./cmd/bbolt + BBOLT_VERIFY=all TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} -timeout ${TESTFLAGS_TIMEOUT} + BBOLT_VERIFY=all TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} ./internal/... + BBOLT_VERIFY=all TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} ./cmd/bbolt @echo "array freelist test" - TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} -timeout ${TESTFLAGS_TIMEOUT} - TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} ./internal/... - TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} ./cmd/bbolt + BBOLT_VERIFY=all TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} -timeout ${TESTFLAGS_TIMEOUT} + BBOLT_VERIFY=all TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} ./internal/... + BBOLT_VERIFY=all TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} ./cmd/bbolt .PHONY: coverage coverage: @@ -76,8 +76,8 @@ install-gofail: .PHONY: test-failpoint test-failpoint: @echo "[failpoint] hashmap freelist test" - TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} -timeout 30m ./tests/failpoint + BBOLT_VERIFY=all TEST_FREELIST_TYPE=hashmap go test -v ${TESTFLAGS} -timeout 30m ./tests/failpoint @echo "[failpoint] array freelist test" - TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} -timeout 30m ./tests/failpoint + BBOLT_VERIFY=all TEST_FREELIST_TYPE=array go test -v ${TESTFLAGS} -timeout 30m ./tests/failpoint diff --git a/freelist.go b/freelist.go index 29ac16c72..731d75c46 100644 --- a/freelist.go +++ b/freelist.go @@ -30,6 +30,7 @@ type freelist struct { freemaps map[uint64]pidSet // key is the size of continuous pages(span), value is a set which contains the starting pgids of same size forwardMap map[common.Pgid]uint64 // key is start pgid, value is its span size backwardMap map[common.Pgid]uint64 // key is end pgid, value is its span size + freePagesCount uint64 // count of free pages(hashmap version) allocate func(txid common.Txid, n int) common.Pgid // the freelist allocate func free_count func() int // the function which gives you free page number mergeSpans func(ids common.Pgids) // the mergeSpan func diff --git a/freelist_hmap.go b/freelist_hmap.go index 57e1e950b..0d38976a1 100644 --- a/freelist_hmap.go +++ b/freelist_hmap.go @@ -8,7 +8,15 @@ import ( // hashmapFreeCount returns count of free pages(hashmap version) func (f *freelist) hashmapFreeCount() int { - // use the forwardMap to get the total count + common.Verify(func() { + expectedFreePageCount := f.hashmapFreeCountSlow() + common.Assert(int(f.freePagesCount) == expectedFreePageCount, + "freePagesCount (%d) is out of sync with free pages map (%d)", f.freePagesCount, expectedFreePageCount) + }) + return int(f.freePagesCount) +} + +func (f *freelist) hashmapFreeCountSlow() int { count := 0 for _, size := range f.forwardMap { count += int(size) @@ -142,6 +150,7 @@ func (f *freelist) addSpan(start common.Pgid, size uint64) { } f.freemaps[size][start] = struct{}{} + f.freePagesCount += size } func (f *freelist) delSpan(start common.Pgid, size uint64) { @@ -151,6 +160,7 @@ func (f *freelist) delSpan(start common.Pgid, size uint64) { if len(f.freemaps[size]) == 0 { delete(f.freemaps, size) } + f.freePagesCount -= size } // initial from pgids using when use hashmap version @@ -162,6 +172,8 @@ func (f *freelist) init(pgids []common.Pgid) { size := uint64(1) start := pgids[0] + // reset the counter when freelist init + f.freePagesCount = 0 if !sort.SliceIsSorted([]common.Pgid(pgids), func(i, j int) bool { return pgids[i] < pgids[j] }) { panic("pgids not sorted") diff --git a/freelist_test.go b/freelist_test.go index 7297055b4..5cf40bd1c 100644 --- a/freelist_test.go +++ b/freelist_test.go @@ -448,6 +448,7 @@ func Test_freelist_hashmapGetFreePageIDs(t *testing.T) { val = rand.Int31n(1000) fm[common.Pgid(i)] = uint64(val) i += val + f.freePagesCount += uint64(val) } f.forwardMap = fm diff --git a/internal/common/utils.go b/internal/common/utils.go index c94e5c6bf..bdf82a7b0 100644 --- a/internal/common/utils.go +++ b/internal/common/utils.go @@ -7,13 +7,6 @@ import ( "unsafe" ) -// Assert will panic with a given formatted message if the given condition is false. -func Assert(condition bool, msg string, v ...interface{}) { - if !condition { - panic(fmt.Sprintf("assertion failed: "+msg, v...)) - } -} - func LoadBucket(buf []byte) *InBucket { return (*InBucket)(unsafe.Pointer(&buf[0])) } diff --git a/internal/common/verify.go b/internal/common/verify.go new file mode 100644 index 000000000..eac95e263 --- /dev/null +++ b/internal/common/verify.go @@ -0,0 +1,67 @@ +// Copied from https://github.com/etcd-io/etcd/blob/main/client/pkg/verify/verify.go +package common + +import ( + "fmt" + "os" + "strings" +) + +const ENV_VERIFY = "BBOLT_VERIFY" + +type VerificationType string + +const ( + ENV_VERIFY_VALUE_ALL VerificationType = "all" + ENV_VERIFY_VALUE_ASSERT VerificationType = "assert" +) + +func getEnvVerify() string { + return strings.ToLower(os.Getenv(ENV_VERIFY)) +} + +func IsVerificationEnabled(verification VerificationType) bool { + env := getEnvVerify() + return env == string(ENV_VERIFY_VALUE_ALL) || env == strings.ToLower(string(verification)) +} + +// EnableVerifications sets `ENV_VERIFY` and returns a function that +// can be used to bring the original settings. +func EnableVerifications(verification VerificationType) func() { + previousEnv := getEnvVerify() + os.Setenv(ENV_VERIFY, string(verification)) + return func() { + os.Setenv(ENV_VERIFY, previousEnv) + } +} + +// EnableAllVerifications enables verification and returns a function +// that can be used to bring the original settings. +func EnableAllVerifications() func() { + return EnableVerifications(ENV_VERIFY_VALUE_ALL) +} + +// DisableVerifications unsets `ENV_VERIFY` and returns a function that +// can be used to bring the original settings. +func DisableVerifications() func() { + previousEnv := getEnvVerify() + os.Unsetenv(ENV_VERIFY) + return func() { + os.Setenv(ENV_VERIFY, previousEnv) + } +} + +// Verify performs verification if the assertions are enabled. +// In the default setup running in tests and skipped in the production code. +func Verify(f func()) { + if IsVerificationEnabled(ENV_VERIFY_VALUE_ASSERT) { + f() + } +} + +// Assert will panic with a given formatted message if the given condition is false. +func Assert(condition bool, msg string, v ...any) { + if !condition { + panic(fmt.Sprintf("assertion failed: "+msg, v...)) + } +}