mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-24 23:46:17 +00:00
Implement crash-safe persistence for the Bitbox hash table: - wal.go: WAL format with START/CLEAR/UPDATE/END entries, builder/reader - sync.go: 3-phase sync protocol (BeginSync → WriteWAL → CommitSync) - recover.go: WAL replay for crash recovery The WAL records page diffs (not full pages) for compact logging. The 3-phase protocol ensures: WAL fsynced before HT modification, HT fsynced before WAL truncation, providing at-least-once delivery of page updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
217 lines
5 KiB
Go
217 lines
5 KiB
Go
package bitbox
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/ethereum/go-ethereum/nomt/core"
|
|
"github.com/ethereum/go-ethereum/nomt/merkle"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- WAL Builder/Reader Tests ---
|
|
|
|
func TestWALEmptyRoundTrip(t *testing.T) {
|
|
b := NewWALBuilder()
|
|
data := b.Finish(42)
|
|
|
|
// Should be padded to page boundary.
|
|
assert.Equal(t, 0, len(data)%pageSize)
|
|
|
|
seqn, entries, err := ReadWAL(data)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(42), seqn)
|
|
assert.Empty(t, entries)
|
|
}
|
|
|
|
func TestWALClearEntryRoundTrip(t *testing.T) {
|
|
b := NewWALBuilder()
|
|
b.AddClear(123)
|
|
b.AddClear(456)
|
|
data := b.Finish(1)
|
|
|
|
seqn, entries, err := ReadWAL(data)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(1), seqn)
|
|
require.Len(t, entries, 2)
|
|
|
|
assert.Equal(t, WALEntryClear, entries[0].Kind)
|
|
assert.Equal(t, uint64(123), entries[0].ClearBucket)
|
|
assert.Equal(t, uint64(456), entries[1].ClearBucket)
|
|
}
|
|
|
|
func TestWALUpdateEntryRoundTrip(t *testing.T) {
|
|
var pageID [32]byte
|
|
pageID[0] = 0xAB
|
|
|
|
var diff core.PageDiff
|
|
diff.SetChanged(5)
|
|
diff.SetChanged(70)
|
|
|
|
nodes := []core.Node{{0x01}, {0x02}}
|
|
|
|
b := NewWALBuilder()
|
|
b.AddUpdate(pageID, diff, nodes, 0xFF, 99)
|
|
data := b.Finish(7)
|
|
|
|
seqn, entries, err := ReadWAL(data)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(7), seqn)
|
|
require.Len(t, entries, 1)
|
|
|
|
e := entries[0]
|
|
assert.Equal(t, WALEntryUpdate, e.Kind)
|
|
assert.Equal(t, pageID, e.PageID)
|
|
assert.True(t, e.Diff.IsChanged(5))
|
|
assert.True(t, e.Diff.IsChanged(70))
|
|
require.Len(t, e.ChangedNodes, 2)
|
|
assert.Equal(t, core.Node{0x01}, e.ChangedNodes[0])
|
|
assert.Equal(t, core.Node{0x02}, e.ChangedNodes[1])
|
|
assert.Equal(t, uint64(0xFF), e.ElidedChildren)
|
|
assert.Equal(t, uint64(99), e.UpdateBucket)
|
|
}
|
|
|
|
func TestWALMixedEntries(t *testing.T) {
|
|
b := NewWALBuilder()
|
|
b.AddClear(10)
|
|
|
|
var pid [32]byte
|
|
var diff core.PageDiff
|
|
diff.SetChanged(0)
|
|
b.AddUpdate(pid, diff, []core.Node{{0xAA}}, 0, 20)
|
|
|
|
b.AddClear(30)
|
|
data := b.Finish(100)
|
|
|
|
_, entries, err := ReadWAL(data)
|
|
require.NoError(t, err)
|
|
require.Len(t, entries, 3)
|
|
assert.Equal(t, WALEntryClear, entries[0].Kind)
|
|
assert.Equal(t, WALEntryUpdate, entries[1].Kind)
|
|
assert.Equal(t, WALEntryClear, entries[2].Kind)
|
|
}
|
|
|
|
func TestReadWALEmpty(t *testing.T) {
|
|
seqn, entries, err := ReadWAL(nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(0), seqn)
|
|
assert.Nil(t, entries)
|
|
}
|
|
|
|
func TestWALFilePersistence(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.wal")
|
|
|
|
b := NewWALBuilder()
|
|
b.AddClear(42)
|
|
data := b.Finish(5)
|
|
|
|
require.NoError(t, WriteWALFile(path, data))
|
|
|
|
loaded, err := ReadWALFile(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, data, loaded)
|
|
|
|
require.NoError(t, TruncateWALFile(path))
|
|
loaded2, err := ReadWALFile(path)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, loaded2)
|
|
}
|
|
|
|
// --- Sync Controller Tests ---
|
|
|
|
func TestFullSyncCycle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
htPath := filepath.Join(dir, "test.bitbox")
|
|
walPath := filepath.Join(dir, "test.wal")
|
|
|
|
seed := HashSeedFromUint64(1, 2)
|
|
db, err := Create(htPath, 1024, seed)
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
|
|
rootID := core.RootPageID()
|
|
page := new(core.RawPage)
|
|
page.SetNodeAt(0, core.Node{0xAA})
|
|
|
|
var diff core.PageDiff
|
|
diff.SetChanged(0)
|
|
|
|
updates := []merkle.UpdatedPage{{
|
|
PageID: rootID,
|
|
Page: page,
|
|
Diff: diff,
|
|
}}
|
|
|
|
require.NoError(t, db.FullSync(walPath, 1, updates))
|
|
|
|
// Verify page is persisted.
|
|
loaded, _, found, err := db.LoadPage(rootID)
|
|
require.NoError(t, err)
|
|
assert.True(t, found)
|
|
assert.Equal(t, core.Node{0xAA}, loaded.NodeAt(0))
|
|
}
|
|
|
|
// --- Recovery Tests ---
|
|
|
|
func TestRecoverFromWAL(t *testing.T) {
|
|
dir := t.TempDir()
|
|
htPath := filepath.Join(dir, "test.bitbox")
|
|
walPath := filepath.Join(dir, "test.wal")
|
|
|
|
seed := HashSeedFromUint64(1, 2)
|
|
|
|
// Create DB and write a WAL but don't commit Phase 3.
|
|
db, err := Create(htPath, 1024, seed)
|
|
require.NoError(t, err)
|
|
|
|
rootID := core.RootPageID()
|
|
page := new(core.RawPage)
|
|
page.SetNodeAt(0, core.Node{0xBB})
|
|
|
|
var diff core.PageDiff
|
|
diff.SetChanged(0)
|
|
|
|
updates := []merkle.UpdatedPage{{
|
|
PageID: rootID,
|
|
Page: page,
|
|
Diff: diff,
|
|
}}
|
|
|
|
// Phase 1 + 2 only (simulate crash before Phase 3).
|
|
plan, err := db.BeginSync(walPath, 5, updates)
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.WriteWAL(walPath, plan))
|
|
db.Close()
|
|
|
|
// Reopen and recover.
|
|
db2, err := Open(htPath)
|
|
require.NoError(t, err)
|
|
defer db2.Close()
|
|
|
|
seqn, err := db2.Recover(walPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(5), seqn)
|
|
|
|
// Verify the page was recovered.
|
|
loaded, _, found, err := db2.LoadPage(rootID)
|
|
require.NoError(t, err)
|
|
assert.True(t, found)
|
|
assert.Equal(t, core.Node{0xBB}, loaded.NodeAt(0))
|
|
}
|
|
|
|
func TestRecoverNoWAL(t *testing.T) {
|
|
dir := t.TempDir()
|
|
htPath := filepath.Join(dir, "test.bitbox")
|
|
walPath := filepath.Join(dir, "test.wal")
|
|
|
|
seed := HashSeedFromUint64(1, 2)
|
|
db, err := Create(htPath, 1024, seed)
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
|
|
seqn, err := db.Recover(walPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint32(0), seqn, "no recovery needed")
|
|
}
|