Files
zeekayandHanzo Dev db33c93d2a Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:46:54 -07:00

314 lines
9.2 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wal_test
import (
"crypto/rand"
"encoding/binary"
"testing"
"github.com/luxfi/bft/common"
"github.com/luxfi/bft/testutil"
wal "github.com/luxfi/bft/wal"
"github.com/stretchr/testify/require"
)
type mockWALReader struct{}
func (m *mockWALReader) RetentionTerm(entry []byte) (uint64, error) {
return binary.BigEndian.Uint64(entry[:8]), nil
}
// makeRecord builds a payload of exactly `size` bytes whose first 8 bytes
// encode the retention term (as read back by mockWALReader).
func makeRecord(t *testing.T, retentionTerm uint64, size int) []byte {
require.GreaterOrEqual(t, size, 8)
buff := make([]byte, size)
binary.BigEndian.PutUint64(buff[:8], retentionTerm)
_, err := rand.Read(buff[8:])
require.NoError(t, err)
return buff
}
// countingCreator returns a Creator that records how many WALs it has created
// and hands back a pointer to the running count.
func countingCreator(t *testing.T) (wal.Creator, *int) {
count := new(int)
return func() (wal.DeletableWAL, error) {
*count++
return testutil.NewTestWAL(t), nil
}, count
}
func TestGarbageCollectedWAL(t *testing.T) {
var testWALs []wal.DeletableWAL
records := make([][]byte, 10)
for i := range uint64(10) {
buff := make([]byte, 16)
binary.BigEndian.PutUint64(buff[:8], i)
_, err := rand.Read(buff[8:])
require.NoError(t, err)
testWAL := testutil.NewTestWAL(t)
require.NoError(t, testWAL.Append(buff))
testWALs = append(testWALs, testWAL)
records[i] = buff
}
var newWALCreatedCount int
gcw, err := wal.NewGarbageCollectedWAL(testWALs, func() (wal.DeletableWAL, error) {
newWALCreatedCount++
return testutil.NewTestWAL(t), nil
}, &mockWALReader{}, 100)
walRecords, err := gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records, walRecords)
require.NoError(t, gcw.GarbageCollect(6))
walRecords, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records[6:], walRecords)
require.NoError(t, gcw.GarbageCollect(10))
walRecords, err = gcw.ReadAll()
require.NoError(t, err)
require.Empty(t, walRecords)
buff := make([]byte, 16)
binary.BigEndian.PutUint64(buff[:8], 100)
_, err = rand.Read(buff[8:])
require.NoError(t, err)
require.Zero(t, newWALCreatedCount)
require.NoError(t, gcw.Append(buff))
require.Equal(t, 1, newWALCreatedCount)
walRecords, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, [][]byte{buff}, walRecords)
buff1 := buff
buff = make([]byte, 1024)
binary.BigEndian.PutUint64(buff[:8], 100)
_, err = rand.Read(buff[8:])
require.NoError(t, err)
buffs := make([][]byte, 2)
buffs[0] = buff1
buffs[1] = buff
require.Equal(t, 1, newWALCreatedCount)
require.NoError(t, gcw.Append(buff))
walRecords, err = gcw.ReadAll()
require.Equal(t, 2, newWALCreatedCount)
require.Equal(t, buffs, walRecords)
}
func TestGarbageCollectedWALRotatesBySize(t *testing.T) {
// WAL rotates when the total number of records exceed maxWALSize.
// Each WAL is expected to contain 2 records, so 6 records should result in 3 WALs.
const (
maxWALSize = 100
recordSize = 40
numRecords = 6
)
creator, newWALCount := countingCreator(t)
gcw, err := wal.NewGarbageCollectedWAL(nil, creator, &mockWALReader{}, maxWALSize)
require.NoError(t, err)
records := make([][]byte, numRecords)
for i := range numRecords {
records[i] = makeRecord(t, uint64(i), recordSize)
require.NoError(t, gcw.Append(records[i]))
}
// Each WAL fits two 40-byte records: 40+40=80 <= 100, but 80+40=120 > 100
// forces a rotation. So 6 records land in 3 WALs.
require.Equal(t, 3, *newWALCount)
// Every record must still be readable, in the original append order,
// across the rotated WALs.
got, err := gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records, got)
}
func TestGarbageCollectedWALRotatesOnOversizedPayload(t *testing.T) {
// WAL rotates if a really big record is inserted.
const maxWALSize = 100
creator, newWALCount := countingCreator(t)
gcw, err := wal.NewGarbageCollectedWAL(nil, creator, &mockWALReader{}, maxWALSize)
require.NoError(t, err)
small := makeRecord(t, 0, 16)
require.NoError(t, gcw.Append(small))
require.Equal(t, 1, *newWALCount)
// A single record bigger than maxWALSize rotates into its own WAL.
big := makeRecord(t, 1, maxWALSize+50)
require.NoError(t, gcw.Append(big))
require.Equal(t, 2, *newWALCount)
got, err := gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, [][]byte{small, big}, got)
}
func TestGarbageCollectedWALGarbageCollectsRotatedWALs(t *testing.T) {
// Verifies a WAL is removed only once its highest retention term drops below the cutoff.
const (
maxWALSize = 100
recordSize = 40
numRecords = 6
)
creator, newWALCount := countingCreator(t)
gcw, err := wal.NewGarbageCollectedWAL(nil, creator, &mockWALReader{}, maxWALSize)
require.NoError(t, err)
// Retention terms 0..5, two per WAL:
// WAL0 -> {0,1} (max 1), WAL1 -> {2,3} (max 3), WAL2 -> {4,5} (max 5)
records := make([][]byte, numRecords)
for rt := range numRecords {
records[rt] = makeRecord(t, uint64(rt), recordSize)
require.NoError(t, gcw.Append(records[rt]))
}
require.Equal(t, 3, *newWALCount) // 6 records, two per WAL -> 3 WALs.
// Does not drop any WALs because the cutoff is below the max retention term of WAL0.
require.NoError(t, gcw.GarbageCollect(1))
got, err := gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records, got)
// Drop WAL 0 (max retention 1 < 2) but keeps WAL1 and WAL2.
require.NoError(t, gcw.GarbageCollect(2))
got, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records[2:], got)
// Does not drop WAL1 because the cutoff is equal to the max retention term of WAL1.
require.NoError(t, gcw.GarbageCollect(3))
got, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records[2:], got)
// Cutoff 4 drops WAL1 (max retention 3 < 4).
require.NoError(t, gcw.GarbageCollect(4))
got, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records[4:], got)
// Cutoff 6 drops the last WAL.
require.NoError(t, gcw.GarbageCollect(6))
got, err = gcw.ReadAll()
require.NoError(t, err)
require.Empty(t, got)
// No new WALs were created by GC or ReadAll.
require.Equal(t, 3, *newWALCount)
}
func TestGarbageCollectedWALAppendOutOfOrder(t *testing.T) {
walRecordBlock := func(round uint64) *testutil.TestBlock {
return testutil.NewTestBlock(common.ProtocolMetadata{Round: round, Seq: round}, common.Blacklist{})
}
nodes := []common.NodeID{{1}, {2}, {3}, {4}}
sigAggr := &testutil.TestSignatureAggregator{N: len(nodes)}
blockRecord := func(round uint64) []byte {
b := walRecordBlock(round)
raw := b.Bytes()
bytes, err := common.BlockRecord(b.BlockHeader(), raw)
require.NoError(t, err)
return bytes
}
notarizationRecord := func(round uint64) []byte {
rec, err := testutil.NewNotarizationRecord(testutil.MakeLogger(t), sigAggr, walRecordBlock(round), nodes)
require.NoError(t, err)
return rec
}
finalizationRecord := func(round uint64) []byte {
_, rec := testutil.NewFinalizationRecord(t, sigAggr, walRecordBlock(round), nodes)
return rec
}
// We create a block record for 4 and a finalization record for 3 out of order.
// The WALRetentionReader must be able to read the retention term from every
// record type, or GarbageCollectedWAL.Append will fail on the finalization.
records := [][]byte{
blockRecord(1), notarizationRecord(1),
blockRecord(2), notarizationRecord(2),
blockRecord(3), notarizationRecord(3),
blockRecord(4),
finalizationRecord(3),
}
// A small maxWALSize forces the stream across several WAL segments so ReadAll must stitch every segment back together.
creator, newWALCount := countingCreator(t)
walSize := 512
gcw, err := wal.NewGarbageCollectedWAL(nil, creator, &common.WALRetentionReader{}, walSize)
require.NoError(t, err)
var sum int
for _, rec := range records {
sum += len(rec)
require.NoError(t, gcw.Append(rec))
}
require.Equal(t, *newWALCount, sum/walSize+1)
// Every record — including the out-of-order Finalization — reads back in the
// order it was appended.
got, err := gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, records, got)
}
func TestGarbageCollectedWALAppendAfterRotationAndGC(t *testing.T) {
// Verifies that appends resume correctly (and keep rotating)
// after the WAL has been fully garbage collected down to empty.
const (
maxWALSize = 100
recordSize = 40
)
creator, newWALCount := countingCreator(t)
gcw, err := wal.NewGarbageCollectedWAL(nil, creator, &mockWALReader{}, maxWALSize)
require.NoError(t, err)
for i := range 4 {
require.NoError(t, gcw.Append(makeRecord(t, uint64(i), recordSize)))
}
// 4 records, two per WAL -> 2 WALs.
require.Equal(t, 2, *newWALCount)
// GC everything away.
require.NoError(t, gcw.GarbageCollect(100))
got, err := gcw.ReadAll()
require.NoError(t, err)
require.Empty(t, got)
// Appending again must create a fresh WAL and continue rotating.
next := make([][]byte, 3)
for i := range 3 {
next[i] = makeRecord(t, uint64(200+i), recordSize)
require.NoError(t, gcw.Append(next[i]))
}
// First append starts a new WAL; the third crosses maxWALSize -> 2 more WALs.
require.Equal(t, 4, *newWALCount)
got, err = gcw.ReadAll()
require.NoError(t, err)
require.Equal(t, next, got)
}