go-ethereum/nomt/bitbox/sync.go
weiihann 859312d1f5 nomt/bitbox: add Phase 4 WAL, sync controller, and crash recovery
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>
2026-02-12 17:16:29 +08:00

131 lines
3 KiB
Go

package bitbox
import (
"fmt"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/ethereum/go-ethereum/nomt/merkle"
)
// SyncPlan holds the pre-computed work for a sync operation.
type SyncPlan struct {
walData []byte
dataWrites []dataWrite
syncSeqn uint32
}
type dataWrite struct {
bucket uint64
page *core.RawPage
}
// BeginSync prepares a sync plan from a set of updated pages. It allocates
// or reuses buckets, builds the WAL, and returns a SyncPlan.
//
// This is Phase 1 of the 3-phase sync protocol.
func (db *DB) BeginSync(
walPath string,
syncSeqn uint32,
updates []merkle.UpdatedPage,
) (*SyncPlan, error) {
wal := NewWALBuilder()
writes := make([]dataWrite, 0, len(updates))
for _, up := range updates {
if up.Diff.IsCleared() {
// Page was cleared — tombstone its bucket.
_, bucket, found, err := db.LoadPage(up.PageID)
if err != nil {
return nil, fmt.Errorf("bitbox/sync: load for clear: %w", err)
}
if found {
db.metaMap.Set(bucket, MetaTombstone)
db.occupied.Add(-1)
wal.AddClear(bucket)
}
continue
}
// Encode the PageID into the page data.
encodedID := up.PageID.Encode()
up.Page.SetPageIDBytes(encodedID)
up.Page.SetElidedChildren(up.Page.ElidedChildren())
// Allocate or reuse a bucket.
bucket, err := db.StorePage(up.PageID, up.Page)
if err != nil {
return nil, fmt.Errorf("bitbox/sync: store page: %w", err)
}
// Pack changed nodes from diff.
changedNodes := up.Diff.PackChangedNodes(up.Page)
wal.AddUpdate(
encodedID,
up.Diff,
changedNodes,
up.Page.ElidedChildren(),
bucket,
)
writes = append(writes, dataWrite{bucket: bucket, page: up.Page})
}
walData := wal.Finish(syncSeqn)
return &SyncPlan{
walData: walData,
dataWrites: writes,
syncSeqn: syncSeqn,
}, nil
}
// WriteWAL writes the WAL to disk and fsyncs it.
//
// This is Phase 2 of the 3-phase sync protocol.
func (db *DB) WriteWAL(walPath string, plan *SyncPlan) error {
return WriteWALFile(walPath, plan.walData)
}
// CommitSync writes dirty HT data + meta pages, fsyncs the HT file, and
// truncates the WAL.
//
// This is Phase 3 of the 3-phase sync protocol.
func (db *DB) CommitSync(walPath string, plan *SyncPlan) error {
// Write data pages.
for _, dw := range plan.dataWrites {
if err := db.writeDataPage(dw.bucket, dw.page); err != nil {
return fmt.Errorf("bitbox/sync: write data: %w", err)
}
}
// Write dirty meta pages.
if err := db.FlushMeta(); err != nil {
return fmt.Errorf("bitbox/sync: flush meta: %w", err)
}
// fsync the HT file.
if err := db.file.Sync(); err != nil {
return fmt.Errorf("bitbox/sync: fsync HT: %w", err)
}
// Truncate WAL — no fsync needed.
return TruncateWALFile(walPath)
}
// FullSync runs all three phases of the sync protocol.
func (db *DB) FullSync(
walPath string,
syncSeqn uint32,
updates []merkle.UpdatedPage,
) error {
plan, err := db.BeginSync(walPath, syncSeqn, updates)
if err != nil {
return err
}
if err := db.WriteWAL(walPath, plan); err != nil {
return err
}
return db.CommitSync(walPath, plan)
}