Skip to content

Commit 8108b23

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 8108b23

3 files changed

Lines changed: 220 additions & 34 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: 61 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,77 @@ 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+
// corresponding raft log prefix from the engine and the sideloaded entries
131+
// storage.
108132
//
109133
// The caller must provide a stateRO reader with GuaranteedDurability so that
110134
// only state confirmed flushed to persistent storage is visible. This ensures
111135
// we never delete a WAG node whose mutations aren't flushed yet.
112-
func truncateAppliedNode(ctx context.Context, raft Raft, stateRO StateRO) (bool, error) {
136+
//
137+
// Returns a boolean indicating whether a node was successfully truncated or
138+
// not. If the return value is false, it means that either there are no WAG
139+
// nodes left, or that the WAG node has not been applied to the state engine.
140+
// Also, an error is returned if the WAG node could not be fetched or deleted.
141+
// TODO(ibrahim): Support deleting multiple WAG nodes within the same batch.
142+
func truncateAppliedWAGNodeAndClearRaftState(
143+
ctx context.Context, raft Raft, stateRO StateRO, clearSideloaded SideloadClearer,
144+
) (bool, error) {
113145
var iter wag.Iterator
114146
for index, node := range iter.Iter(ctx, raft.RO) {
115147
applied, err := isNodeApplied(ctx, stateRO, node)
116-
if err != nil {
148+
if err != nil || !applied {
117149
return false, err
118150
}
119-
if !applied {
120-
return false, nil
121-
}
122151
if err := wag.Delete(raft.WO, index); err != nil {
123152
return false, err
124153
}
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.
154+
155+
// Clean up the raft log prefix of a destroyed/subsumed replica.
156+
for _, event := range node.Events {
157+
if event.Type != wagpb.EventDestroy && event.Type != wagpb.EventSubsume {
158+
continue
159+
}
160+
if err := clearReplicaRaftLog(ctx, raft, event.Addr.RangeID, event.Addr.Index); err != nil {
161+
return false, errors.Wrapf(err, "clearing raft log for r%d", event.Addr.RangeID)
162+
}
163+
if clearSideloaded != nil {
164+
// In general, we shouldn't delete sideloaded files before committing the
165+
// batch that deletes the Raft log entries. However, in this case, we
166+
// know that the destroy/subsume event has already been flushed to disk,
167+
// and the replica is destroyed. We will not need to reference the
168+
// sideloaded files anymore. If we were to delay deleting them till
169+
// after the batch is committed, we risk leaking the sideloaded files
170+
// if a node crash happens after the batch is committed (and synced) and
171+
// before the sideloaded files are deleted.
172+
if err := clearSideloaded(ctx, event.Addr.RangeID, event.Addr.Index); err != nil {
173+
return false, errors.Wrapf(err, "clearing sideloaded files for r%d", event.Addr.RangeID)
174+
}
175+
}
176+
}
128177
return true, nil
129178
}
130-
if err := iter.Error(); err != nil {
131-
return false, err
132-
}
133-
return false, nil
179+
return false, iter.Error()
134180
}

pkg/kv/kvserver/kvstorage/wag_truncator_test.go

Lines changed: 157 additions & 19 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,43 @@ 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()
53-
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()
60+
require.NoError(t, e.LogEngine().PutUnversioned(
61+
keys.RaftLogKeyFromPrefix(keys.RaftLogPrefix(rangeID), 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))
80+
for ; ; iter.Next() {
81+
ok, err := iter.Valid()
6282
require.NoError(t, err)
63-
if !truncated {
64-
return
83+
if !ok {
84+
break
6585
}
86+
suffix := iter.UnsafeKey().Key[len(prefix):]
87+
index, err := keys.DecodeRaftLogKeyFromSuffix(suffix)
88+
require.NoError(t, err)
89+
indices = append(indices, index)
6690
}
91+
return indices
6792
}
6893

6994
// listWAGNodes returns the indices of all WAG nodes in the log engine.
@@ -78,6 +103,41 @@ func (e *testEngines) listWAGNodes(t *testing.T) []uint64 {
78103
return indices
79104
}
80105

106+
// makeSideloadClearer returns a SideloadClearer that truncates sideloaded
107+
// files for a given range using disk-based sideload storage.
108+
func (e *testEngines) makeSideloadClearer(
109+
st *cluster.Settings, baseDir string, env *fs.Env,
110+
) SideloadClearer {
111+
return func(ctx context.Context, rangeID roachpb.RangeID, lastIndex kvpb.RaftIndex) error {
112+
limiter := rate.NewLimiter(rate.Inf, math.MaxInt64)
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+
b.Close()
135+
if !ok {
136+
return
137+
}
138+
}
139+
}
140+
81141
func TestTruncateApplied(t *testing.T) {
82142
defer leaktest.AfterTest(t)()
83143
defer log.Scope(t).Close(t)
@@ -110,7 +170,7 @@ func TestTruncateApplied(t *testing.T) {
110170
&kvserverpb.RangeAppliedState{RaftAppliedIndex: 9}))
111171
},
112172
// No WAG node has been applied yet because the current
113-
// replica id is < the event replica id.
173+
// replicaID is < the event replicaID.
114174
wantWAGIndices: []uint64{1, 2, 3},
115175
},
116176
{
@@ -189,7 +249,7 @@ func TestTruncateApplied(t *testing.T) {
189249
e.writeWAGNode(t, wagpb.Event{Addr: wagpb.MakeAddr(r1, 0), Type: wagpb.EventCreate})
190250
e.writeWAGNode(t, wagpb.Event{Addr: wagpb.MakeAddr(r1, 20), Type: wagpb.EventDestroy})
191251
require.NoError(t, sl.SetRaftReplicaID(ctx, e.StateEngine(), r1.ReplicaID))
192-
// EventDestroy won't be deleted if the replica id still matches the
252+
// EventDestroy won't be deleted if the replicaID still matches the
193253
// WAG node's replica ID because it means that the event hasn't applied
194254
// yet. In this case, we won't even read the applied index.
195255
require.NoError(t, sl.SetRangeAppliedState(ctx, e.StateEngine(),
@@ -202,7 +262,7 @@ func TestTruncateApplied(t *testing.T) {
202262
e.writeWAGNode(t, wagpb.Event{Addr: wagpb.MakeAddr(r1, 0), Type: wagpb.EventCreate})
203263
e.writeWAGNode(t, wagpb.Event{Addr: wagpb.MakeAddr(r1, 20), Type: wagpb.EventSubsume})
204264
require.NoError(t, sl.SetRaftReplicaID(ctx, e.StateEngine(), r1.ReplicaID))
205-
// EventSubsume won't be deleted if the replica id still matches the
265+
// EventSubsume won't be deleted if the replicaID still matches the
206266
// WAG node's replica ID because it means that the event hasn't applied
207267
// yet. In this case, we won't even read the applied index.
208268
require.NoError(t, sl.SetRangeAppliedState(ctx, e.StateEngine(),
@@ -215,8 +275,86 @@ func TestTruncateApplied(t *testing.T) {
215275
e := makeTestEngines()
216276
defer e.Close()
217277
tc.setup(t, &e)
218-
e.truncateWAGNodes(t)
278+
e.truncateWAGNodes(t, nil /* clearSideloaded */)
219279
require.Equal(t, tc.wantWAGIndices, e.listWAGNodes(t))
220280
})
221281
}
222282
}
283+
284+
// TestTruncateAndClearRaftState verifies that
285+
// truncateAppliedWAGNodeAndClearRaftState only clears raft log entries and
286+
// sideloaded files up to the destroyed/subsumed replica's last index. Entries
287+
// and files beyond that index may belong to a newer replica and must be
288+
// preserved.
289+
func TestTruncateAndClearRaftState(t *testing.T) {
290+
defer leaktest.AfterTest(t)()
291+
defer log.Scope(t).Close(t)
292+
293+
r1 := roachpb.FullReplicaID{RangeID: 1, ReplicaID: 1}
294+
r2 := roachpb.FullReplicaID{RangeID: 1, ReplicaID: 2}
295+
sl := MakeStateLoader(1 /* rangeID */)
296+
297+
for _, eventType := range []wagpb.EventType{wagpb.EventDestroy, wagpb.EventSubsume} {
298+
t.Run(eventType.String(), func(t *testing.T) {
299+
e := makeTestEngines()
300+
defer e.Close()
301+
ctx := context.Background()
302+
303+
// Write WAG nodes: init then destroy/subsume at index 20.
304+
e.writeWAGNode(t, wagpb.Event{
305+
Addr: wagpb.MakeAddr(r1, 10), Type: wagpb.EventInit,
306+
})
307+
e.writeWAGNode(t, wagpb.Event{
308+
Addr: wagpb.MakeAddr(r1, 20), Type: eventType,
309+
})
310+
311+
// Create a WAG node for a newer replica for the same range.
312+
e.writeWAGNode(t, wagpb.Event{
313+
Addr: wagpb.MakeAddr(r2, 0), Type: wagpb.EventCreate,
314+
})
315+
316+
// Tombstone confirms destruction/subsumption.
317+
require.NoError(t, sl.SetRangeTombstone(ctx, e.StateEngine(),
318+
kvserverpb.RangeTombstone{NextReplicaID: 2}))
319+
320+
// Write raft log entries 10-25. Entries <= 20 belong to the old replica.
321+
// Entries >= 21 belong to the new replica using 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+
env := e.LogEngine().Env()
330+
limiter := rate.NewLimiter(rate.Inf, math.MaxInt64)
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, env)
336+
337+
e.truncateWAGNodes(t, clearer)
338+
// Raft entries <= 20 belong to the old replica and 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 entries 10, 15, and 20 belong to the old replica
345+
// and must be deleted. The rest shouldn't be deleted by the WAG
346+
// truncator.
347+
for _, idx := range []kvpb.RaftIndex{10, 15, 20} {
348+
_, err := ss.Get(ctx, idx, 1)
349+
require.Errorf(t, err, "sideloaded file at index %d should have been deleted", idx)
350+
}
351+
for _, idx := range []kvpb.RaftIndex{21, 25} {
352+
data, err := ss.Get(ctx, idx, 1)
353+
require.NoErrorf(t, err, "sideloaded file at index %d should be preserved", idx)
354+
require.Equal(t, []byte("sst-data"), data)
355+
}
356+
// Only the WAG node for the newer replica should be left.
357+
require.Equal(t, []uint64{3}, e.listWAGNodes(t))
358+
})
359+
}
360+
}

0 commit comments

Comments
 (0)