Skip to content

Commit ba2d7d5

Browse files
committed
fix replay segments
1 parent 4fd9bcd commit ba2d7d5

2 files changed

Lines changed: 152 additions & 6 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package wal
2+
3+
import (
4+
"os"
5+
"path"
6+
"testing"
7+
8+
"github.com/rs/zerolog"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/onflow/flow-go/ledger"
12+
"github.com/onflow/flow-go/ledger/complete/mtrie"
13+
"github.com/onflow/flow-go/ledger/complete/payloadless"
14+
"github.com/onflow/flow-go/model/bootstrap"
15+
"github.com/onflow/flow-go/module/metrics"
16+
"github.com/onflow/flow-go/utils/unittest"
17+
)
18+
19+
// TestReplayOnPayloadlessForest_IgnoresV6RootCheckpoint is a regression test for
20+
// the case where a payloadless node boots with both a V7 root checkpoint (the
21+
// real seed) and a V6 root.checkpoint present in the trie dir. The forest must
22+
// be seeded from the V7 checkpoint, and the V6 root.checkpoint must NOT be read.
23+
//
24+
// To prove the V6 file is never touched, a corrupt root.checkpoint is placed
25+
// alongside the V7 checkpoint: the previous implementation routed payloadless
26+
// segment replay through [DiskWAL.replay], which falls back to loading the V6
27+
// root checkpoint when replaying from segment 0 — that fallback would fail on
28+
// the corrupt file. With the fix, the V6 file is ignored and replay succeeds.
29+
func TestReplayOnPayloadlessForest_IgnoresV6RootCheckpoint(t *testing.T) {
30+
unittest.RunWithTempDir(t, func(dir string) {
31+
logger := zerolog.Nop()
32+
33+
// Build a V7 root checkpoint from a simple trie and write it as the
34+
// payloadless root checkpoint (root.checkpoint.v7).
35+
v6Tries := createSimpleTrie(t)
36+
rootHash := v6Tries[0].RootHash()
37+
v7Tries, err := FromV6Tries(v6Tries)
38+
require.NoError(t, err)
39+
require.NoError(t, StoreCheckpointV7Concurrently(v7Tries, dir, RootCheckpointFilenameV7(), logger))
40+
41+
// Place a corrupt V6 root checkpoint next to the V7 one. If the
42+
// payloadless replay path attempts to load it, the load fails — which is
43+
// exactly the regression this test guards against.
44+
junkPath := path.Join(dir, bootstrap.FilenameWALRootCheckpoint)
45+
require.NoError(t, os.WriteFile(junkPath, []byte("not a valid v6 checkpoint"), 0644))
46+
47+
w, err := NewDiskWAL(logger, nil, metrics.NewNoopCollector(), dir, 10, pathByteSize, segmentSize)
48+
require.NoError(t, err)
49+
defer func() { <-w.Done() }()
50+
51+
forest, err := payloadless.NewForest(100, &metrics.NoopCollector{}, nil)
52+
require.NoError(t, err)
53+
54+
err = w.ReplayOnPayloadlessForest(forest)
55+
require.NoError(t, err, "replay must seed from V7 and must not load the V6 root checkpoint")
56+
57+
require.True(t, forest.HasTrie(rootHash), "forest must be seeded from the V7 root checkpoint")
58+
})
59+
}
60+
61+
// TestReplayOnPayloadlessForest_ReplaysWALSegments verifies that after seeding
62+
// the forest from the V7 root checkpoint, WAL segment records that are newer
63+
// than the checkpoint are still replayed onto the payloadless forest. This
64+
// guards against the segment-replay refactor accidentally skipping segments.
65+
func TestReplayOnPayloadlessForest_ReplaysWALSegments(t *testing.T) {
66+
unittest.RunWithTempDir(t, func(dir string) {
67+
logger := zerolog.Nop()
68+
69+
// Seed state: a full forest with an initial update, captured as the V7
70+
// root checkpoint.
71+
fullForest, err := mtrie.NewForest(100, &metrics.NoopCollector{}, nil)
72+
require.NoError(t, err)
73+
74+
paths0, payloads0 := randNPathPayloads(10)
75+
seed := &ledger.TrieUpdate{
76+
RootHash: fullForest.GetEmptyRootHash(),
77+
Paths: paths0,
78+
Payloads: toPayloadPtrs(payloads0),
79+
}
80+
root0, err := fullForest.Update(seed)
81+
require.NoError(t, err)
82+
83+
v6Tries, err := fullForest.GetTries()
84+
require.NoError(t, err)
85+
v7Tries, err := FromV6Tries(v6Tries)
86+
require.NoError(t, err)
87+
require.NoError(t, StoreCheckpointV7Concurrently(v7Tries, dir, RootCheckpointFilenameV7(), logger))
88+
89+
// A second update, built on root0, recorded into the WAL but NOT in the
90+
// checkpoint. Replay must apply it to reach root1.
91+
paths1, payloads1 := randNPathPayloads(10)
92+
update1 := &ledger.TrieUpdate{
93+
RootHash: root0,
94+
Paths: paths1,
95+
Payloads: toPayloadPtrs(payloads1),
96+
}
97+
root1, err := fullForest.Update(update1)
98+
require.NoError(t, err)
99+
100+
// Record update1 into the WAL, then close to flush the segment to disk.
101+
recordWAL, err := NewDiskWAL(logger, nil, metrics.NewNoopCollector(), dir, 10, pathByteSize, segmentSize)
102+
require.NoError(t, err)
103+
_, _, err = recordWAL.RecordUpdate(update1)
104+
require.NoError(t, err)
105+
<-recordWAL.Done()
106+
107+
// Replay on a fresh WAL: seed from V7 (root0), then replay the WAL
108+
// segment carrying update1 to reach root1.
109+
w, err := NewDiskWAL(logger, nil, metrics.NewNoopCollector(), dir, 10, pathByteSize, segmentSize)
110+
require.NoError(t, err)
111+
defer func() { <-w.Done() }()
112+
113+
forest, err := payloadless.NewForest(100, &metrics.NoopCollector{}, nil)
114+
require.NoError(t, err)
115+
116+
require.NoError(t, w.ReplayOnPayloadlessForest(forest))
117+
118+
require.True(t, forest.HasTrie(root0), "forest must contain the V7 checkpoint root")
119+
require.True(t, forest.HasTrie(root1), "forest must contain the root produced by replaying the WAL segment")
120+
})
121+
}

ledger/complete/wal/wal.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,19 @@ func (w *DiskWAL) replaySegmentsForPayloadlessForest(
196196
// V7 checkpoint already covers everything on disk.
197197
return nil
198198
}
199-
err = w.replay(from, lastSeg,
200-
func(tries []*trie.MTrie) error { return nil }, // unused when useCheckpoints=false
199+
// Replay only the WAL segment records onto the forest. Unlike
200+
// [DiskWAL.replay], this deliberately does NOT fall back to loading the V6
201+
// root checkpoint when `from` is 0: the payloadless forest is already seeded
202+
// from the V7 checkpoint by the caller ([DiskWAL.ReplayOnPayloadlessForest]),
203+
// and the V6 root checkpoint is not loadable into a payloadless forest.
204+
// Routing through replay would read (and immediately discard) the entire V6
205+
// root checkpoint, a wasteful full-forest load at boot.
206+
err = w.replaySegments(from, lastSeg,
201207
func(update *ledger.TrieUpdate) error {
202208
_, err := forest.Update(update)
203209
return err
204210
},
205211
func(rootHash ledger.RootHash) error { return nil },
206-
false, // useCheckpoints
207212
)
208213
if err != nil {
209214
return fmt.Errorf("could not replay WAL segments [%v:%v] for payloadless forest: %w", from, lastSeg, err)
@@ -374,9 +379,31 @@ func (w *DiskWAL) replay(
374379
Int("loaded_checkpoint", loadedCheckpoint).
375380
Msgf("replaying segments from %d to %d", startSegment, to)
376381

382+
err = w.replaySegments(startSegment, to, updateFn, deleteFn)
383+
if err != nil {
384+
return err
385+
}
386+
387+
w.log.Info().Msgf("finished loading checkpoint and replaying WAL from %d to %d", from, to)
388+
389+
return nil
390+
}
391+
392+
// replaySegments reads the WAL segment records in the range [from, to] and
393+
// applies each record to the provided handlers, dispatching WALUpdate records
394+
// to `updateFn` and WALDelete records to `deleteFn`. It performs NO checkpoint
395+
// loading: the caller is responsible for seeding any starting state before
396+
// calling this.
397+
//
398+
// No error returns are expected during normal operation.
399+
func (w *DiskWAL) replaySegments(
400+
from, to int,
401+
updateFn func(update *ledger.TrieUpdate) error,
402+
deleteFn func(rootHash ledger.RootHash) error,
403+
) error {
377404
sr, err := prometheusWAL.NewSegmentsRangeReader(w.log, prometheusWAL.SegmentRange{
378405
Dir: w.wal.Dir(),
379-
First: startSegment,
406+
First: from,
380407
Last: to,
381408
})
382409
if err != nil {
@@ -413,8 +440,6 @@ func (w *DiskWAL) replay(
413440
}
414441
}
415442

416-
w.log.Info().Msgf("finished loading checkpoint and replaying WAL from %d to %d", from, to)
417-
418443
return nil
419444
}
420445

0 commit comments

Comments
 (0)