Skip to content

Commit 22b383e

Browse files
committed
kvserver: add split trigger support to TestReplicaLifecycleDataDriven
This patch adds a few directives to construct and evaluate split triggers. We then use these directives to demonstrate that leases are correctly copied over from the LHS to the RHS. The interesting case is around leader leases, where if the LHS has a leader lease, the RHS gets an expiration based lease. For the other two lease types, the RHS's lease type stays the same. Epic: none Release note: None
1 parent ee20eae commit 22b383e

File tree

4 files changed

+322
-17
lines changed

4 files changed

+322
-17
lines changed

pkg/kv/kvserver/batcheval/cmd_end_transaction.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,9 @@ func splitTriggerHelper(
14071407
// This avoids running the consistency checker on the RHS immediately after
14081408
// the split.
14091409
lastTS := hlc.Timestamp{}
1410+
// TODO(arul): instead of fetching the consistency checker timestamp here
1411+
// like this, we should instead pass it using the SplitTriggerHelperInput to
1412+
// make it easier to test.
14101413
if _, err := storage.MVCCGetProto(ctx, batch,
14111414
keys.QueueLastProcessedKey(split.LeftDesc.StartKey, "consistencyChecker"),
14121415
hlc.Timestamp{}, &lastTS, storage.MVCCGetOptions{}); err != nil {

pkg/kv/kvserver/print/debug_print.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ func tryTxn(kv storage.MVCCKeyValue) (string, error) {
397397
if err := maybeUnmarshalInline(kv.Value, &txn); err != nil {
398398
return "", err
399399
}
400+
// TODO(arul): investigate why we need this new line here.
400401
return txn.String() + "\n", nil
401402
}
402403

pkg/kv/kvserver/replica_lifecycle_datadriven_test.go

Lines changed: 220 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,28 @@
99
package kvserver
1010

1111
import (
12+
"cmp"
1213
"context"
1314
"fmt"
1415
"strings"
1516
"testing"
17+
"time"
1618

19+
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/abortspan"
20+
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/batcheval"
1721
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvserverpb"
1822
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/kvstorage"
1923
"github.com/cockroachdb/cockroach/pkg/kv/kvserver/print"
2024
"github.com/cockroachdb/cockroach/pkg/raft/raftpb"
2125
"github.com/cockroachdb/cockroach/pkg/roachpb"
2226
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
2327
"github.com/cockroachdb/cockroach/pkg/storage"
28+
"github.com/cockroachdb/cockroach/pkg/storage/enginepb"
2429
"github.com/cockroachdb/cockroach/pkg/testutils/dd"
30+
"github.com/cockroachdb/cockroach/pkg/util/hlc"
2531
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
2632
"github.com/cockroachdb/cockroach/pkg/util/log"
33+
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
2734
"github.com/cockroachdb/datadriven"
2835
"github.com/cockroachdb/errors"
2936
"github.com/stretchr/testify/require"
@@ -50,10 +57,36 @@ import (
5057
// Creates a replica on n1/s1 for the specified range ID. The created replica
5158
// may be initialized or uninitialized.
5259
//
53-
// print-range-state
60+
// create-split range-id=<int> split-key=<key>
5461
// ----
5562
//
56-
// Prints the current range state in the test context.
63+
// Creates a split for the specified range at the given split key, which
64+
// entails creating a SplitTrigger with both the LHS and RHS descriptors.
65+
// Much like how things work in CRDB, the LHS descriptor is created by
66+
// narrowing the original range and a new range descriptor is created for
67+
// the RHS with the same replica set.
68+
//
69+
// set-lease range-id=<int> replica=<int> [lease-type=leader-lease|epoch|expiration]
70+
// ----
71+
//
72+
// Sets the lease for the specified range to the supplied replica. Note that
73+
// the replica parameter specifies NodeIDs, not to be confused with
74+
// ReplicaIDs. By default, the lease is of the leader-lease variety, but
75+
// this may be overriden to an epoch or expiration based lease by using the
76+
// lease-type parameter. For now, we treat the associated lease metadata as
77+
// uninteresting.
78+
//
79+
// run-split-trigger range-id=<int>
80+
// ----
81+
//
82+
// Executes the split trigger for the specified range on n1.
83+
//
84+
// print-range-state [sort-keys=<bool>]
85+
// ----
86+
//
87+
// Prints the current range state in the test context. By default, ranges are
88+
// sorted by range ID. If sort-keys is set to true, ranges are sorted by their
89+
// descriptor's start key instead.
5790
func TestReplicaLifecycleDataDriven(t *testing.T) {
5891
defer leaktest.AfterTest(t)()
5992
defer log.Scope(t).Close(t)
@@ -116,7 +149,7 @@ func TestReplicaLifecycleDataDriven(t *testing.T) {
116149
if rs.replica != nil {
117150
return errors.New("initialized replica already exists on n1/s1").Error()
118151
}
119-
repl := rs.getReplicaDescriptor(t)
152+
repl := rs.getReplicaDescriptor(t, roachpb.NodeID(1))
120153

121154
batch := tc.storage.NewBatch()
122155
defer batch.Close()
@@ -148,14 +181,130 @@ func TestReplicaLifecycleDataDriven(t *testing.T) {
148181
require.NoError(t, err, "error committing batch")
149182
return sb.String()
150183

184+
case "create-split":
185+
rangeID := dd.ScanArg[int](t, d, "range-id")
186+
splitKey := dd.ScanArg[string](t, d, "split-key")
187+
rs := tc.mustGetRangeState(t, roachpb.RangeID(rangeID))
188+
desc := rs.desc
189+
require.True(
190+
t,
191+
roachpb.RKey(splitKey).Compare(desc.StartKey) > 0 &&
192+
roachpb.RKey(splitKey).Compare(desc.EndKey) < 0,
193+
"split key not within range",
194+
)
195+
leftDesc := desc
196+
leftDesc.EndKey = roachpb.RKey(splitKey)
197+
rightDesc := desc
198+
rightDesc.RangeID = tc.nextRangeID
199+
tc.nextRangeID++
200+
rightDesc.StartKey = roachpb.RKey(splitKey)
201+
split := &roachpb.SplitTrigger{
202+
LeftDesc: leftDesc,
203+
RightDesc: rightDesc,
204+
}
205+
tc.splits[roachpb.RangeID(rangeID)] = split
206+
return "ok"
207+
208+
case "set-lease":
209+
rangeID := dd.ScanArg[int](t, d, "range-id")
210+
replicaNodeID := dd.ScanArg[int](t, d, "replica")
211+
leaseType := "leader-lease" // default to a leader-lease
212+
if d.HasArg("lease-type") {
213+
leaseType = dd.ScanArg[string](t, d, "lease-type")
214+
}
215+
rs := tc.mustGetRangeState(t, roachpb.RangeID(rangeID))
216+
targetReplica := rs.getReplicaDescriptor(t, roachpb.NodeID(replicaNodeID))
217+
// NB: The details of the lease are not important to the test;
218+
// only the type is.
219+
var lease roachpb.Lease
220+
switch leaseType {
221+
case "leader-lease":
222+
lease = roachpb.Lease{
223+
Replica: *targetReplica,
224+
Term: 10,
225+
MinExpiration: hlc.Timestamp{WallTime: 100},
226+
}
227+
case "epoch":
228+
lease = roachpb.Lease{
229+
Replica: *targetReplica,
230+
Epoch: 20,
231+
}
232+
case "expiration":
233+
lease = roachpb.Lease{
234+
Replica: *targetReplica,
235+
Expiration: &hlc.Timestamp{WallTime: 300},
236+
}
237+
default:
238+
t.Fatalf("unknown lease type: %s", leaseType)
239+
}
240+
rs.lease = lease
241+
return "ok"
242+
243+
case "run-split-trigger":
244+
rangeID := dd.ScanArg[int](t, d, "range-id")
245+
split, ok := tc.splits[roachpb.RangeID(rangeID)]
246+
require.True(t, ok, "split trigger not found for range-id %d", rangeID)
247+
rs := tc.mustGetRangeState(t, roachpb.RangeID(rangeID))
248+
desc := rs.desc
249+
batch := tc.storage.NewBatch()
250+
defer batch.Close()
251+
252+
rec := (&batcheval.MockEvalCtx{
253+
ClusterSettings: tc.st,
254+
Desc: &desc,
255+
Clock: tc.clock,
256+
AbortSpan: rs.abortspan,
257+
LastReplicaGCTimestamp: rs.lastGCTimestamp,
258+
RangeLeaseDuration: tc.rangeLeaseDuration,
259+
}).EvalContext()
260+
261+
in := batcheval.SplitTriggerHelperInput{
262+
LeftLease: rs.lease,
263+
GCThreshold: &rs.gcThreshold,
264+
GCHint: &rs.gcHint,
265+
ReplicaVersion: rs.version,
266+
}
267+
// Actually run the split trigger.
268+
_, _, err := batcheval.TestingSplitTrigger(
269+
ctx, rec, batch /* bothDeltaMS */, enginepb.MVCCStats{}, split, in, hlc.Timestamp{},
270+
)
271+
require.NoError(t, err)
272+
273+
// Update the test context's notion of the range state after the
274+
// split.
275+
tc.updatePostSplitRangeState(t, ctx, batch, roachpb.RangeID(rangeID), split)
276+
// Print the state of the batch (all keys/values written as part
277+
// of the split trigger).
278+
output, err := print.DecodeWriteBatch(batch.Repr())
279+
require.NoError(t, err)
280+
// Commit the batch.
281+
err = batch.Commit(true)
282+
require.NoError(t, err, "error committing batch")
283+
// TODO(arul): There are double lines in the output (see tryTxn
284+
// in debug_print.go) that we need to strip out for the benefit
285+
// of the datadriven test driver. Until that TODO is addressed,
286+
// we manually split things out here.
287+
return strings.ReplaceAll(output, "\n\n", "\n")
288+
151289
case "print-range-state":
152290
var sb strings.Builder
153291
if len(tc.ranges) == 0 {
154292
return "no ranges in test context"
155293
}
156-
// Sort by range IDs for consistent output.
294+
295+
sortByKeys := false
296+
if d.HasArg("sort-keys") {
297+
sortByKeys = dd.ScanArg[bool](t, d, "sort-keys")
298+
}
299+
157300
rangeIDs := maps.Keys(tc.ranges)
158-
slices.Sort(rangeIDs)
301+
slices.SortFunc(rangeIDs, func(a, b roachpb.RangeID) int {
302+
if sortByKeys {
303+
return tc.ranges[a].desc.StartKey.Compare(tc.ranges[b].desc.StartKey)
304+
}
305+
// Else sort by range IDs for consistent output.
306+
return cmp.Compare(a, b)
307+
})
159308

160309
for _, rangeID := range rangeIDs {
161310
rs := tc.ranges[rangeID]
@@ -172,9 +321,14 @@ func TestReplicaLifecycleDataDriven(t *testing.T) {
172321

173322
// rangeState represents the state of a single range in the test context.
174323
type rangeState struct {
175-
desc roachpb.RangeDescriptor
176-
version roachpb.Version
177-
replica *replicaInfo // replica on n1/s1.
324+
desc roachpb.RangeDescriptor
325+
version roachpb.Version
326+
lease roachpb.Lease
327+
gcThreshold hlc.Timestamp
328+
gcHint roachpb.GCHint
329+
abortspan *abortspan.AbortSpan
330+
lastGCTimestamp hlc.Timestamp
331+
replica *replicaInfo // replica on n1/s1.
178332
}
179333

180334
// replicaInfo contains the basic info about a replica, used for managing its
@@ -188,20 +342,30 @@ type replicaInfo struct {
188342
// testCtx is a single test's context. It tracks the state of all ranges and any
189343
// intermediate steps when performing replica lifecycle events.
190344
type testCtx struct {
191-
ranges map[roachpb.RangeID]*rangeState
345+
st *cluster.Settings
346+
clock *hlc.Clock
347+
rangeLeaseDuration time.Duration
348+
192349
nextRangeID roachpb.RangeID // monotonically-increasing rangeID
193-
st *cluster.Settings
350+
ranges map[roachpb.RangeID]*rangeState
351+
splits map[roachpb.RangeID]*roachpb.SplitTrigger
194352
// The storage engine corresponds to a single store, (n1, s1).
195353
storage storage.Engine
196354
}
197355

198356
// newTestCtx constructs and returns a new testCtx.
199357
func newTestCtx() *testCtx {
200358
st := cluster.MakeTestingClusterSettings()
359+
manual := timeutil.NewManualTime(timeutil.Unix(0, 10))
360+
clock := hlc.NewClockForTesting(manual)
201361
return &testCtx{
202-
ranges: make(map[roachpb.RangeID]*rangeState),
362+
st: st,
363+
clock: clock,
364+
rangeLeaseDuration: 99 * time.Nanosecond,
365+
203366
nextRangeID: 1,
204-
st: st,
367+
ranges: make(map[roachpb.RangeID]*rangeState),
368+
splits: make(map[roachpb.RangeID]*roachpb.SplitTrigger),
205369
storage: storage.NewDefaultInMemForTesting(),
206370
}
207371
}
@@ -213,9 +377,16 @@ func (tc *testCtx) close() {
213377

214378
// newRangeState constructs a new rangeState for the supplied descriptor.
215379
func newRangeState(desc roachpb.RangeDescriptor) *rangeState {
380+
gcThreshold := hlc.Timestamp{WallTime: 4}
381+
gcHint := roachpb.GCHint{GCTimestamp: gcThreshold}
382+
216383
return &rangeState{
217-
desc: desc,
218-
version: roachpb.Version{Major: 10, Minor: 8, Internal: 7}, // dummy version to avoid churn
384+
desc: desc,
385+
version: roachpb.Version{Major: 10, Minor: 8, Internal: 7}, // dummy version to avoid churn
386+
gcThreshold: gcThreshold,
387+
gcHint: gcHint,
388+
abortspan: abortspan.New(desc.RangeID),
389+
lastGCTimestamp: hlc.Timestamp{},
219390
}
220391
}
221392

@@ -248,13 +419,42 @@ func (tc *testCtx) updatePostReplicaCreateState(
248419
}
249420
}
250421

251-
func (rs *rangeState) getReplicaDescriptor(t *testing.T) *roachpb.ReplicaDescriptor {
422+
// updatePostSplitRangeState updates the range state after a split.
423+
func (tc *testCtx) updatePostSplitRangeState(
424+
t *testing.T,
425+
ctx context.Context,
426+
reader storage.Reader,
427+
lhsRangeID roachpb.RangeID,
428+
split *roachpb.SplitTrigger,
429+
) {
430+
originalRangeState := tc.mustGetRangeState(t, lhsRangeID)
431+
// The range ID should not change for LHS since it's the same range.
432+
require.Equal(t, lhsRangeID, split.LeftDesc.RangeID)
433+
// Update LHS by just updating the descriptor.
434+
originalRangeState.desc = split.LeftDesc
435+
tc.ranges[lhsRangeID] = originalRangeState
436+
rhsRangeState := newRangeState(split.RightDesc)
437+
// Create RHS range state by reading from the reader.
438+
rhsSl := kvstorage.MakeStateLoader(split.RightDesc.RangeID)
439+
rhsState, err := rhsSl.Load(ctx, reader, &split.RightDesc)
440+
require.NoError(t, err)
441+
rhsRangeState.lease = *rhsState.Lease
442+
rhsRangeState.gcThreshold = *rhsState.GCThreshold
443+
rhsRangeState.gcHint = *rhsState.GCHint
444+
rhsRangeState.version = *rhsState.Version
445+
446+
tc.ranges[split.RightDesc.RangeID] = rhsRangeState
447+
}
448+
449+
func (rs *rangeState) getReplicaDescriptor(
450+
t *testing.T, nodeID roachpb.NodeID,
451+
) *roachpb.ReplicaDescriptor {
252452
for i, repl := range rs.desc.InternalReplicas {
253-
if repl.NodeID == roachpb.NodeID(1) {
453+
if repl.NodeID == nodeID {
254454
return &rs.desc.InternalReplicas[i]
255455
}
256456
}
257-
t.Fatal("replica not found")
457+
t.Fatalf("replica with NodeID %d not found in range descriptor", nodeID)
258458
return nil // unreachable
259459
}
260460

@@ -264,6 +464,9 @@ func (rs *rangeState) String() string {
264464
if rs.replica != nil {
265465
sb.WriteString(fmt.Sprintf("\n replica (n1/s1): %s", rs.replica))
266466
}
467+
if (rs.lease != roachpb.Lease{}) {
468+
sb.WriteString(fmt.Sprintf("\n lease: %s", rs.lease))
469+
}
267470
return sb.String()
268471
}
269472

0 commit comments

Comments
 (0)