Skip to content

Commit d3de7ff

Browse files
kvstorage: delete raft state when deleting destroy/subsume events
When deleting a WAG node that has the events destroy or subsume, we also need to delete the raft log (up until and including the addr.index), and any sideloaded files that are part of that raft log prefix. We do this because when a replica delete or subsume is applied, we cannot just yet delete the replica state (log and sideloaded files) because we might crash before the state engine has flushed, and we might need the raft log to replay the event. Release note: None Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
1 parent 9adb88d commit d3de7ff

3 files changed

Lines changed: 212 additions & 31 deletions

File tree

pkg/kv/kvserver/kvstorage/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ go_test(
6969
"//pkg/roachpb",
7070
"//pkg/settings/cluster",
7171
"//pkg/storage",
72+
"//pkg/storage/fs",
7273
"//pkg/testutils",
7374
"//pkg/testutils/datapathutils",
7475
"//pkg/testutils/dd",
@@ -82,5 +83,6 @@ go_test(
8283
"//pkg/util/uuid",
8384
"@com_github_cockroachdb_datadriven//:datadriven",
8485
"@com_github_stretchr_testify//require",
86+
"@org_golang_x_time//rate",
8587
],
8688
)

pkg/kv/kvserver/kvstorage/wag_truncator.go

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ package kvstorage
88
import (
99
"context"
1010

11+
"github.com/cockroachdb/cockroach/pkg/keys"
1112
"github.com/cockroachdb/cockroach/pkg/kv/kvpb"
1213
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvstorage/wag"
1314
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvstorage/wag/wagpb"
1415
"github.com/cockroachdb/cockroach/pkg/roachpb"
16+
"github.com/cockroachdb/cockroach/pkg/storage"
1517
"github.com/cockroachdb/errors"
1618
)
1719

@@ -102,33 +104,71 @@ func isNodeApplied(ctx context.Context, stateRO StateRO, node wagpb.Node) (bool,
102104
return true, nil
103105
}
104106

105-
// truncateAppliedNode checks the first WAG node and deletes it if all of its
106-
// events have been applied to the state engine. Returns true if a node was
107-
// deleted.
107+
// SideloadClearer truncates sideloaded files for a given range up to and
108+
// including the specified raft log index. Files beyond this index may belong
109+
// to a newer replica and must be preserved.
110+
// The SideloadClearer must also sync the sideloaded storage after truncation to
111+
// avoid leaking the files in case of a node crash.
112+
type SideloadClearer func(ctx context.Context, rangeID roachpb.RangeID, lastIndex kvpb.RaftIndex) error
113+
114+
// clearReplicaRaftLog clears raft log entries at or below the given index for
115+
// a destroyed or subsumed replica.
116+
func clearReplicaRaftLog(
117+
ctx context.Context, raft Raft, rangeID roachpb.RangeID, lastIndex kvpb.RaftIndex,
118+
) error {
119+
return storage.ClearRangeWithHeuristic(
120+
ctx, raft.RO, raft.WO,
121+
keys.RaftLogPrefix(rangeID), /* start */
122+
keys.RaftLogKey(rangeID, lastIndex+1), /* end */
123+
ClearRangeThresholdPointKeys(),
124+
)
125+
}
126+
127+
// truncateAppliedWAGNodeAndClearRaftState checks the first WAG node and
128+
// deletes it if all of its events have been applied to the state engine. For
129+
// nodes containing EventDestroy or EventSubsume events, it also clears the
130+
// raft log entries and sideloaded files for those replicas.
108131
//
109132
// The caller must provide a stateRO reader with GuaranteedDurability so that
110133
// only state confirmed flushed to persistent storage is visible. This ensures
111134
// we never delete a WAG node whose mutations aren't flushed yet.
112-
func truncateAppliedNode(ctx context.Context, raft Raft, stateRO StateRO) (bool, error) {
135+
// TODO(ibrahim): Support deleting multiple WAG nodes within the same batch.
136+
func truncateAppliedWAGNodeAndClearRaftState(
137+
ctx context.Context, raft Raft, stateRO StateRO, clearSideloaded SideloadClearer,
138+
) (bool, error) {
113139
var iter wag.Iterator
114140
for index, node := range iter.Iter(ctx, raft.RO) {
115141
applied, err := isNodeApplied(ctx, stateRO, node)
116-
if err != nil {
142+
if err != nil || !applied {
117143
return false, err
118144
}
119-
if !applied {
120-
return false, nil
121-
}
122145
if err := wag.Delete(raft.WO, index); err != nil {
123146
return false, err
124147
}
125-
// TODO(Ibrahim): Add logic to clear raft state (log entries, HardState,
126-
// TruncatedState) for destroyed/subsumed replicas.
127-
// TODO(ibrahim): Support deleting multiple WAG nodes within the same batch.
148+
149+
// Clean up the raft log prefix of a destroyed/subsumed replica.
150+
for _, event := range node.Events {
151+
if event.Type != wagpb.EventDestroy && event.Type != wagpb.EventSubsume {
152+
continue
153+
}
154+
if err := clearReplicaRaftLog(ctx, raft, event.Addr.RangeID, event.Addr.Index); err != nil {
155+
return false, errors.Wrapf(err, "clearing raft log for r%d", event.Addr.RangeID)
156+
}
157+
if clearSideloaded != nil {
158+
// In general, we shouldn't delete sideloaded files before committing the
159+
// batch that deletes the Raft log entries. However, in this case, we
160+
// know that the destroy/subsume event has already been flushed to disk,
161+
// and the replica is destroyed. We will not need to reference the
162+
// sideloaded files anymore. If we were to delay deleting them till
163+
// after the batch is committed, we risk leaking the sideloaded files
164+
// if a node crash happens after the batch is committed (and synced) and
165+
// before the sideloaded files are deleted.
166+
if err := clearSideloaded(ctx, event.Addr.RangeID, event.Addr.Index); err != nil {
167+
return false, errors.Wrapf(err, "clearing sideloaded files for r%d", event.Addr.RangeID)
168+
}
169+
}
170+
}
128171
return true, nil
129172
}
130-
if err := iter.Error(); err != nil {
131-
return false, err
132-
}
133-
return false, nil
173+
return false, iter.Error()
134174
}

pkg/kv/kvserver/kvstorage/wag_truncator_test.go

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ package kvstorage
77

88
import (
99
"context"
10+
"math"
1011
"testing"
1112

13+
"github.com/cockroachdb/cockroach/pkg/keys"
14+
"github.com/cockroachdb/cockroach/pkg/kv/kvpb"
1215
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvserverpb"
1316
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvstorage/wag"
1417
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvstorage/wag/wagpb"
18+
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/logstore"
1519
"github.com/cockroachdb/cockroach/pkg/roachpb"
20+
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
1621
"github.com/cockroachdb/cockroach/pkg/storage"
22+
"github.com/cockroachdb/cockroach/pkg/storage/fs"
1723
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
1824
"github.com/cockroachdb/cockroach/pkg/util/log"
1925
"github.com/stretchr/testify/require"
26+
"golang.org/x/time/rate"
2027
)
2128

2229
// testEngines holds the separated log and state engines used by truncator
@@ -45,25 +52,44 @@ func (e *testEngines) writeWAGNode(t *testing.T, event wagpb.Event) {
4552
require.NoError(t, wag.Write(e.LogEngine(), index, wagpb.Node{Events: []wagpb.Event{event}}))
4653
}
4754

48-
// truncateWAGNodes repeatedly truncates applied WAG nodes until none remain
49-
// applied.
50-
func (e *testEngines) truncateWAGNodes(t *testing.T) {
55+
// writeRaftLogEntry writes a dummy raft log entry to the log engine.
56+
func (e *testEngines) writeRaftLogEntry(
57+
t *testing.T, rangeID roachpb.RangeID, index kvpb.RaftIndex,
58+
) {
5159
t.Helper()
52-
ctx := context.Background()
60+
sl := MakeStateLoader(rangeID)
61+
require.NoError(t, e.LogEngine().PutUnversioned(sl.RaftLogKey(index), []byte("entry")))
62+
}
63+
64+
// raftLogIndices returns the indices of all raft log entries for the given range
65+
// in the log engine.
66+
func (e *testEngines) raftLogIndices(t *testing.T, rangeID roachpb.RangeID) []kvpb.RaftIndex {
67+
t.Helper()
68+
sl := MakeStateLoader(rangeID)
69+
prefix := sl.RaftLogPrefix()
70+
var indices []kvpb.RaftIndex
71+
iter, err := e.LogEngine().NewMVCCIterator(
72+
context.Background(), storage.MVCCKeyIterKind, storage.IterOptions{
73+
LowerBound: prefix,
74+
UpperBound: prefix.PrefixEnd(),
75+
ReadCategory: fs.ReplicationReadCategory,
76+
})
77+
require.NoError(t, err)
78+
defer iter.Close()
79+
iter.SeekGE(storage.MakeMVCCMetadataKey(prefix))
5380
for {
54-
b := e.LogEngine().NewWriteBatch()
55-
truncated, err := truncateAppliedNode(
56-
ctx, Raft{RO: e.LogEngine(), WO: b}, e.StateEngine(),
57-
)
58-
if err == nil && truncated {
59-
err = b.Commit(false /* sync */)
60-
}
61-
b.Close()
62-
require.NoError(t, err)
63-
if !truncated {
64-
return
81+
ok, iterErr := iter.Valid()
82+
require.NoError(t, iterErr)
83+
if !ok {
84+
break
6585
}
86+
suffix := iter.UnsafeKey().Key[len(prefix):]
87+
index, decErr := keys.DecodeRaftLogKeyFromSuffix(suffix)
88+
require.NoError(t, decErr)
89+
indices = append(indices, index)
90+
iter.Next()
6691
}
92+
return indices
6793
}
6894

6995
// listWAGNodes returns the indices of all WAG nodes in the log engine.
@@ -78,6 +104,39 @@ func (e *testEngines) listWAGNodes(t *testing.T) []uint64 {
78104
return indices
79105
}
80106

107+
// makeSideloadClearer returns a SideloadClearer that truncates sideloaded
108+
// files for a given range using disk-based sideload storage.
109+
func (e *testEngines) makeSideloadClearer(
110+
st *cluster.Settings, baseDir string, limiter *rate.Limiter, env *fs.Env,
111+
) SideloadClearer {
112+
return func(ctx context.Context, rangeID roachpb.RangeID, lastIndex kvpb.RaftIndex) error {
113+
ss := logstore.NewDiskSideloadStorage(st, rangeID, baseDir, limiter, env)
114+
if err := ss.TruncateTo(ctx, lastIndex); err != nil {
115+
return err
116+
}
117+
return ss.Sync()
118+
}
119+
}
120+
121+
// truncateWAGNodes repeatedly calls truncateAppliedWAGNodeAndClearRaftState
122+
// until no more nodes can be deleted.
123+
func (e *testEngines) truncateWAGNodes(t *testing.T, clearSideloaded SideloadClearer) {
124+
t.Helper()
125+
require.NoError(t, e.StateEngine().Flush())
126+
stateReader := e.StateEngine().NewReader(storage.GuaranteedDurability)
127+
defer stateReader.Close()
128+
for {
129+
b := e.LogEngine().NewBatch()
130+
ok, err := truncateAppliedWAGNodeAndClearRaftState(context.Background(),
131+
Raft{RO: e.LogEngine(), WO: b}, stateReader, clearSideloaded)
132+
require.NoError(t, err)
133+
require.NoError(t, b.Commit(false /* sync */))
134+
if !ok {
135+
return
136+
}
137+
}
138+
}
139+
81140
func TestTruncateApplied(t *testing.T) {
82141
defer leaktest.AfterTest(t)()
83142
defer log.Scope(t).Close(t)
@@ -215,8 +274,88 @@ func TestTruncateApplied(t *testing.T) {
215274
e := makeTestEngines()
216275
defer e.Close()
217276
tc.setup(t, &e)
218-
e.truncateWAGNodes(t)
277+
e.truncateWAGNodes(t, nil /* clearSideloaded */)
219278
require.Equal(t, tc.wantWAGIndices, e.listWAGNodes(t))
220279
})
221280
}
222281
}
282+
283+
// TestTruncateAndClearRaftState verifies that
284+
// truncateAppliedWAGNodeAndClearRaftState only clears raft log entries and
285+
// sideloaded files up to the destroyed/subsumed replica's last index. Entries
286+
// and files beyond that index may belong to a newer replica and must be
287+
// preserved.
288+
func TestTruncateAndClearRaftState(t *testing.T) {
289+
defer leaktest.AfterTest(t)()
290+
defer log.Scope(t).Close(t)
291+
292+
r1 := roachpb.FullReplicaID{RangeID: 1, ReplicaID: 1}
293+
r2 := roachpb.FullReplicaID{RangeID: 1, ReplicaID: 2}
294+
sl := MakeStateLoader(1 /* rangeID */)
295+
296+
for _, eventType := range []wagpb.EventType{wagpb.EventDestroy, wagpb.EventSubsume} {
297+
t.Run(eventType.String(), func(t *testing.T) {
298+
e := makeTestEngines()
299+
defer e.Close()
300+
ctx := context.Background()
301+
302+
// Write WAG nodes: init then destroy/subsume at index 20.
303+
e.writeWAGNode(t, wagpb.Event{
304+
Addr: wagpb.MakeAddr(r1, 10), Type: wagpb.EventInit,
305+
})
306+
e.writeWAGNode(t, wagpb.Event{
307+
Addr: wagpb.MakeAddr(r1, 20), Type: eventType,
308+
})
309+
310+
// Create a WAG node for a newer replica for the same range.
311+
e.writeWAGNode(t, wagpb.Event{
312+
Addr: wagpb.MakeAddr(r2, 0), Type: wagpb.EventCreate,
313+
})
314+
315+
// Tombstone confirms destruction/subsumption.
316+
require.NoError(t, sl.SetRangeTombstone(ctx, e.StateEngine(),
317+
kvserverpb.RangeTombstone{NextReplicaID: 2}))
318+
319+
// Raft log entries at indices 10-25. (Indices at or below lastIndex=20)
320+
// belong to the old replica. Entries at 21-25 (above lastIndex=20)
321+
// belong to a newer replica reusing the same RangeID.
322+
for idx := 10; idx <= 25; idx++ {
323+
e.writeRaftLogEntry(t, 1 /* rangeID */, kvpb.RaftIndex(idx))
324+
}
325+
326+
// Create sideloaded files using the log engine's VFS.
327+
st := cluster.MakeTestingClusterSettings()
328+
baseDir := e.LogEngine().GetAuxiliaryDir()
329+
limiter := rate.NewLimiter(rate.Inf, math.MaxInt64)
330+
env := e.LogEngine().Env()
331+
ss := logstore.NewDiskSideloadStorage(st, 1, baseDir, limiter, env)
332+
for _, idx := range []kvpb.RaftIndex{10, 15, 20, 21, 25} {
333+
require.NoError(t, ss.Put(ctx, idx, 1 /* term */, []byte("sst-data")))
334+
}
335+
clearer := e.makeSideloadClearer(st, baseDir, limiter, env)
336+
337+
e.truncateWAGNodes(t, clearer)
338+
// Raft log entries 10-20 (at or below lastIndex=20) must be deleted. The
339+
// rest shouldn't be deleted by the WAG truncator.
340+
require.Equal(t,
341+
[]kvpb.RaftIndex{21, 22, 23, 24, 25},
342+
e.raftLogIndices(t, 1 /* rangeID */),
343+
)
344+
// Sideloaded files at indices 10, 15, and 20 (at or below lastIndex=20)
345+
// must be deleted.
346+
for _, idx := range []kvpb.RaftIndex{10, 15, 20} {
347+
_, err := ss.Get(ctx, idx, 1)
348+
require.Errorf(t, err, "sideloaded file at index %d should have been deleted", idx)
349+
}
350+
// Sideloaded files at indices 21 and 25 (above lastIndex=20) must be
351+
// preserved.
352+
for _, idx := range []kvpb.RaftIndex{21, 25} {
353+
data, err := ss.Get(ctx, idx, 1)
354+
require.NoErrorf(t, err, "sideloaded file at index %d should be preserved", idx)
355+
require.Equal(t, []byte("sst-data"), data)
356+
}
357+
// Only the WAG node for the newer replica should be left.
358+
require.Equal(t, []uint64{3}, e.listWAGNodes(t))
359+
})
360+
}
361+
}

0 commit comments

Comments
 (0)