Skip to content

Commit d7e5c43

Browse files
committed
feat(keyviz): address PR #720 round-1 (Gemini HIGH/MEDIUM + Codex P2)
Three review items, two of them resolved by the same semantic change (per-cell wire format): - Gemini HIGH (proto/admin.proto:116) — row-level scalar raft_group_id / leader_term insufficient for the per-cell dedupe goal. The pivot in admin_grpc.go and keyviz_handler.go only captures the FIRST column's identity, so subsequent columns under a mid-window leader flip get the wrong term and the aggregator's (bucket, group, term, column) dedupe key breaks. - Gemini MEDIUM (keyviz_fanout.go:529) — first-seen identity vs max-merge value can mismatch when the source whose value won did not match the source whose identity was seeded first. Both resolved by switching wire format to per-cell parallel arrays: // proto/admin.proto KeyVizRow: repeated uint64 raft_group_ids = 13; repeated uint64 leader_terms = 14; // internal/admin JSON KeyVizRow: RaftGroupIDs []uint64 `json:"raft_group_ids,omitempty"` LeaderTerms []uint64 `json:"leader_terms,omitempty"` The pivot loops in admin_grpc.go and keyviz_handler.go now stamp row.RaftGroupIDs[j] = mr.RaftGroupID per column (parallel to row.Values[j] = pick(mr)). The mergeRowInto in keyviz_fanout.go stamps the destination cell's identity from whichever source contributed the value kept at that cell — for maxMerge that's the larger source; for sumMerge that's best-effort last-touched (sum has no single owner, but in the steady state both sources agree on the term, so the distinction doesn't matter). - Codex P2 (main.go:1565) — publishLeaderTerms reads rt.engine twice (nil-check + Status() call) which races rt.Close() setting rt.engine = nil during startup-error teardown. Snapshotted into a local once. Caller audit (per loop instructions): the only production consumers of KeyVizRow.RaftGroupID/LeaderTerm were the pivot/merge paths in adapter/admin_grpc.go and internal/admin/keyviz_{handler,fanout}.go, all updated in this commit. No external consumer reads these fields. The PR-3c aggregator that will USE the per-cell dedupe hasn't been written yet, so there are no callers expecting the old scalar shape. Test updates: - TestKeyVizHandlerStampsRaftIdentity: extended to a two-column scenario with a mid-window flip; asserts parallel arrays match (route 1: groups=[7,7], terms=[42,43]; route 2 absent in col1: zero pair). - TestGetKeyVizMatrixStampsRaftIdentity: same shape on the gRPC path against pb.KeyVizRow.RaftGroupIds / LeaderTerms. - TestMergeKeyVizMatricesPreservesRaftIdentity: source rows now use parallel-array shape; pinned identity survives mergeRowInto. - TestMergeKeyVizMatricesPerCellIdentityMatchesValueOwner (NEW): leadership flip across two cells with each leader winning one cell; asserts merged identity at each cell matches the value's owner (Gemini MEDIUM regression guard). Sampler tests at the package level are unchanged because keyviz.MatrixRow still carries scalar RaftGroupID / LeaderTerm — the per-cell representation only emerges at the pivot where multiple per-column MatrixRows are folded into one row-major KeyVizRow.
1 parent c34c8bd commit d7e5c43

9 files changed

Lines changed: 217 additions & 83 deletions

File tree

adapter/admin_grpc.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,12 @@ func matrixToProto(cols []keyviz.MatrixColumn, pick func(keyviz.MatrixRow) uint6
612612
order = append(order, mr.RouteID)
613613
}
614614
pr.Values[j] = pick(mr)
615+
// Per-cell Raft identity stamped from this column's
616+
// MatrixRow (Gemini HIGH on PR #720 — a row-level
617+
// scalar would only capture the first column's
618+
// identity and break the per-cell dedupe goal).
619+
pr.RaftGroupIds[j] = mr.RaftGroupID
620+
pr.LeaderTerms[j] = mr.LeaderTerm
615621
}
616622
}
617623
resp.Rows = make([]*pb.KeyVizRow, len(order))
@@ -675,9 +681,12 @@ func newKeyVizRowFrom(mr keyviz.MatrixRow, numCols int) *pb.KeyVizRow {
675681
Aggregate: mr.Aggregate,
676682
RouteCount: total,
677683
RouteIdsTruncated: mr.Aggregate && total > uint64(len(mr.MemberRoutes)),
678-
RaftGroupId: mr.RaftGroupID,
679-
LeaderTerm: mr.LeaderTerm,
680684
Values: make([]uint64, numCols),
685+
// Per-cell parallel arrays — populated in the column-loop
686+
// above, not here, because the first MatrixRow seen for a
687+
// route only carries that one column's identity.
688+
RaftGroupIds: make([]uint64, numCols),
689+
LeaderTerms: make([]uint64, numCols),
681690
}
682691
if mr.Aggregate {
683692
row.RouteIds = append([]uint64(nil), mr.MemberRoutes...)

adapter/admin_grpc_keyviz_test.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,18 @@ func TestGetKeyVizMatrixHonorsRowsBudget(t *testing.T) {
274274
require.Equal(t, "route:4", resp.Rows[1].BucketId)
275275
}
276276

277-
// TestGetKeyVizMatrixStampsRaftIdentity pins the Phase 2-C+ wire
278-
// extension: MatrixRow.RaftGroupID and MatrixRow.LeaderTerm propagate
279-
// through matrixToProto into the proto KeyVizRow's
280-
// raft_group_id (field 13) and leader_term (field 14). The fan-out
281-
// aggregator's per-term dedupe key requires both fields on the wire.
277+
// TestGetKeyVizMatrixStampsRaftIdentity pins the Phase 2-C+ per-cell
278+
// wire extension: MatrixRow.RaftGroupID and MatrixRow.LeaderTerm
279+
// propagate per-column through matrixToProto into the proto
280+
// KeyVizRow's raft_group_ids (field 13) and leader_terms (field 14)
281+
// parallel-to-values arrays. The fan-out aggregator's per-cell
282+
// dedupe key requires per-column identity (Gemini HIGH on PR #720
283+
// — row-level scalars only captured the first column's identity
284+
// and broke dedupe under mid-window leader flips).
282285
func TestGetKeyVizMatrixStampsRaftIdentity(t *testing.T) {
283286
t.Parallel()
284287
t0 := time.Unix(1_700_000_000, 0)
288+
t1 := t0.Add(time.Minute)
285289
srv := newAdminServerWithFakeSampler(t, []keyviz.MatrixColumn{
286290
{
287291
At: t0,
@@ -290,19 +294,27 @@ func TestGetKeyVizMatrixStampsRaftIdentity(t *testing.T) {
290294
{RouteID: 2, Start: []byte("b"), End: []byte("c"), Writes: 9, RaftGroupID: 0, LeaderTerm: 0},
291295
},
292296
},
297+
// col 1: term flipped on route 1 mid-window; route 2 absent.
298+
{
299+
At: t1,
300+
Rows: []keyviz.MatrixRow{
301+
{RouteID: 1, Start: []byte("a"), End: []byte("b"), Writes: 11, RaftGroupID: 7, LeaderTerm: 43},
302+
},
303+
},
293304
})
294305

295306
resp, err := srv.GetKeyVizMatrix(context.Background(), &pb.GetKeyVizMatrixRequest{
296307
Series: pb.KeyVizSeries_KEYVIZ_SERIES_WRITES,
297308
})
298309
require.NoError(t, err)
299310
require.Len(t, resp.Rows, 2)
300-
// route:1 — non-zero identity propagated.
311+
// route:1 — per-cell identity captures the term flip.
301312
require.Equal(t, "route:1", resp.Rows[0].BucketId)
302-
require.Equal(t, uint64(7), resp.Rows[0].RaftGroupId, "RaftGroupID must propagate to proto field 13")
303-
require.Equal(t, uint64(42), resp.Rows[0].LeaderTerm, "LeaderTerm must propagate to proto field 14")
304-
// route:2 — zero values stay zero (legacy max-merge fallback).
313+
require.Equal(t, []uint64{7, 7}, resp.Rows[0].RaftGroupIds, "raft_group_ids must be parallel to values")
314+
require.Equal(t, []uint64{42, 43}, resp.Rows[0].LeaderTerms,
315+
"leader_terms must capture per-column term so the aggregator can dedupe across the flip")
316+
// route:2 — present only in col0; col1 zero is the "term not tracked" sentinel.
305317
require.Equal(t, "route:2", resp.Rows[1].BucketId)
306-
require.Equal(t, uint64(0), resp.Rows[1].RaftGroupId)
307-
require.Equal(t, uint64(0), resp.Rows[1].LeaderTerm)
318+
require.Equal(t, []uint64{0, 0}, resp.Rows[1].RaftGroupIds)
319+
require.Equal(t, []uint64{0, 0}, resp.Rows[1].LeaderTerms)
308320
}

internal/admin/keyviz_fanout.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,9 +525,14 @@ func mergeRowInto(
525525
RouteIDs: append([]uint64(nil), row.RouteIDs...),
526526
RouteIDsTruncated: row.RouteIDsTruncated,
527527
RouteCount: row.RouteCount,
528-
RaftGroupID: row.RaftGroupID,
529-
LeaderTerm: row.LeaderTerm,
530528
Values: make([]uint64, mergedWidth),
529+
// Per-cell parallel arrays sized to the merged column
530+
// width. Stamped per-cell in the loop below from the
531+
// largest-source for that cell, so the identity always
532+
// matches the value we kept (Gemini MEDIUM on PR #720
533+
// resolved by going per-cell).
534+
RaftGroupIDs: make([]uint64, mergedWidth),
535+
LeaderTerms: make([]uint64, mergedWidth),
531536
}
532537
rowsByBucket[row.BucketID] = dst
533538
*bucketOrder = append(*bucketOrder, row.BucketID)
@@ -537,11 +542,26 @@ func mergeRowInto(
537542
if !ok || j >= len(row.Values) {
538543
continue
539544
}
540-
next, conflict := mergeFn(dst.Values[idx], row.Values[j])
545+
prev := dst.Values[idx]
546+
next, conflict := mergeFn(prev, row.Values[j])
541547
dst.Values[idx] = next
542548
if conflict {
543549
dst.Conflict = true
544550
}
551+
// Identity belongs to the source whose value we kept. For
552+
// sumMerge (reads) this is best-effort: prev+incoming has
553+
// no single owner, so we keep whichever side contributed
554+
// most recently — close-to-correct in the steady state.
555+
// For maxMerge (writes), `next == row.Values[j]` exactly
556+
// when the incoming source won the cell; in the tied
557+
// (prev == incoming != 0) case `next == prev`, both sides
558+
// agree on the value, and either identity is acceptable.
559+
if next == row.Values[j] && j < len(row.RaftGroupIDs) {
560+
dst.RaftGroupIDs[idx] = row.RaftGroupIDs[j]
561+
}
562+
if next == row.Values[j] && j < len(row.LeaderTerms) {
563+
dst.LeaderTerms[idx] = row.LeaderTerms[j]
564+
}
545565
}
546566
}
547567

internal/admin/keyviz_fanout_test.go

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,67 @@ func TestMergeKeyVizMatricesWritesMaxStableLeader(t *testing.T) {
7171
}
7272

7373
// TestMergeKeyVizMatricesPreservesRaftIdentity pins the Phase 2-C+
74-
// wire extension on the fan-out merge path: when mergeRowInto seeds
75-
// the destination row from the first source, RaftGroupID and
76-
// LeaderTerm are copied through. PR-3c will use these fields to
77-
// switch from §4.2's row-level max-merge to §9.1's per-cell
78-
// (group, term) dedupe; this PR ensures the fields survive the
79-
// merge so PR-3c has data to act on.
74+
// per-cell wire extension on the fan-out merge path: mergeRowInto
75+
// stamps the destination row's per-cell (RaftGroupIDs[idx],
76+
// LeaderTerms[idx]) from whichever source contributed the value
77+
// kept at that cell. Both sources reporting the same identity
78+
// for a writes-max merge is the steady-state shape — merged
79+
// identity matches regardless of source order. (Gemini HIGH on
80+
// PR #720 resolved by going per-cell; row-level scalars would
81+
// only capture the first column's identity and break the per-cell
82+
// dedupe goal.)
8083
func TestMergeKeyVizMatricesPreservesRaftIdentity(t *testing.T) {
8184
t.Parallel()
8285
col := []int64{1_700_000_000_000}
8386
a := KeyVizMatrix{
8487
ColumnUnixMs: col,
8588
Series: keyVizSeriesWrites,
8689
Rows: []KeyVizRow{
87-
{BucketID: "route:5", Values: []uint64{30}, RaftGroupID: 7, LeaderTerm: 42},
90+
{BucketID: "route:5", Values: []uint64{30}, RaftGroupIDs: []uint64{7}, LeaderTerms: []uint64{42}},
8891
},
8992
}
9093
b := KeyVizMatrix{
9194
ColumnUnixMs: col,
9295
Series: keyVizSeriesWrites,
9396
Rows: []KeyVizRow{
94-
{BucketID: "route:5", Values: []uint64{0}, RaftGroupID: 7, LeaderTerm: 42},
97+
{BucketID: "route:5", Values: []uint64{0}, RaftGroupIDs: []uint64{7}, LeaderTerms: []uint64{42}},
9598
},
9699
}
97100
merged := mergeKeyVizMatrices([]KeyVizMatrix{a, b}, keyVizSeriesWrites)
98101
require.Len(t, merged.Rows, 1)
99-
require.Equal(t, uint64(7), merged.Rows[0].RaftGroupID, "RaftGroupID must survive mergeRowInto")
100-
require.Equal(t, uint64(42), merged.Rows[0].LeaderTerm, "LeaderTerm must survive mergeRowInto")
102+
require.Equal(t, []uint64{7}, merged.Rows[0].RaftGroupIDs, "RaftGroupIDs must survive mergeRowInto")
103+
require.Equal(t, []uint64{42}, merged.Rows[0].LeaderTerms, "LeaderTerms must survive mergeRowInto")
104+
}
105+
106+
// TestMergeKeyVizMatricesPerCellIdentityMatchesValueOwner pins the
107+
// Gemini MEDIUM fix: when maxMerge picks a value from one source,
108+
// the identity at that cell must come from the SAME source — not
109+
// from whoever happened to be processed first. Drives a leadership
110+
// flip across two consecutive columns with each leader winning
111+
// one cell.
112+
func TestMergeKeyVizMatricesPerCellIdentityMatchesValueOwner(t *testing.T) {
113+
t.Parallel()
114+
col := []int64{1_700_000_000_000, 1_700_000_001_000}
115+
exLeader := KeyVizMatrix{
116+
ColumnUnixMs: col,
117+
Series: keyVizSeriesWrites,
118+
Rows: []KeyVizRow{
119+
{BucketID: "route:9", Values: []uint64{50, 0}, RaftGroupIDs: []uint64{7, 7}, LeaderTerms: []uint64{42, 42}},
120+
},
121+
}
122+
newLeader := KeyVizMatrix{
123+
ColumnUnixMs: col,
124+
Series: keyVizSeriesWrites,
125+
Rows: []KeyVizRow{
126+
{BucketID: "route:9", Values: []uint64{0, 80}, RaftGroupIDs: []uint64{7, 7}, LeaderTerms: []uint64{43, 43}},
127+
},
128+
}
129+
merged := mergeKeyVizMatrices([]KeyVizMatrix{exLeader, newLeader}, keyVizSeriesWrites)
130+
require.Len(t, merged.Rows, 1)
131+
require.Equal(t, []uint64{50, 80}, merged.Rows[0].Values, "writes max-merge picks the larger per cell")
132+
require.Equal(t, []uint64{7, 7}, merged.Rows[0].RaftGroupIDs, "groupID stays 7 across both cells")
133+
require.Equal(t, []uint64{42, 43}, merged.Rows[0].LeaderTerms,
134+
"col0's identity comes from exLeader (term 42, won 50 vs 0); col1's identity comes from newLeader (term 43, won 80 vs 0)")
101135
}
102136

103137
// TestMergeKeyVizMatricesWritesMaxLeadershipFlip pins §4.2 under a

internal/admin/keyviz_handler.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,20 @@ type KeyVizRow struct {
8888
RouteCount uint64 `json:"route_count"`
8989
Values []uint64 `json:"values"`
9090
Conflict bool `json:"conflict,omitempty"`
91-
// RaftGroupID and LeaderTerm carry the route's Raft identity at
92-
// flush time. Phase 2-C+ fan-out uses
93-
// (bucket_id, raft_group_id, leader_term, column) as the dedupe
94-
// key so writes from a leader and the previous leader can be
95-
// summed across terms instead of conservatively max-merged.
96-
// Zero values mean "term not tracked" (legacy single-group
97-
// deployments, virtual aggregate buckets, or nodes that have
98-
// not wired the leader-term publisher) — the aggregator falls
99-
// back to the legacy max-merge for those cells.
100-
RaftGroupID uint64 `json:"raft_group_id,omitempty"`
101-
LeaderTerm uint64 `json:"leader_term,omitempty"`
91+
// RaftGroupIDs[j] and LeaderTerms[j] carry the route's Raft
92+
// identity at the time column j was flushed (parallel to
93+
// Values[]). Phase 2-C+ fan-out uses
94+
// (bucket_id, raft_group_id, leader_term, column) as the
95+
// dedupe key. Per-cell representation is required because
96+
// leadership can flip within the requested window; row-level
97+
// scalars would only capture the first column's identity and
98+
// cause incorrect dedupe for later columns (Gemini HIGH on
99+
// PR #720). Zero values mean "term not tracked" — the
100+
// aggregator falls back to the legacy max-merge for those
101+
// cells. The slices are either nil (legacy server, no
102+
// per-column identity to share) or len == len(Values).
103+
RaftGroupIDs []uint64 `json:"raft_group_ids,omitempty"`
104+
LeaderTerms []uint64 `json:"leader_terms,omitempty"`
102105
// total accumulates the sum of Values during pivot so the
103106
// rowBudget sort is O(N log N) on a precomputed key rather
104107
// than O(N log N × M) recomputing the sum per comparison.
@@ -333,6 +336,13 @@ func pivotKeyVizColumns(cols []keyviz.MatrixColumn, series KeyVizSeries, rowBudg
333336
}
334337
v := pick(mr)
335338
row.Values[j] = v
339+
// Per-cell Raft identity stamped from this column's
340+
// MatrixRow. Each MatrixRow is emitted by Flush() with
341+
// the term snapshot taken at that flush moment, so the
342+
// j-th column's identity may differ from j-1 if a leader
343+
// term flipped between flushes (Gemini HIGH on PR #720).
344+
row.RaftGroupIDs[j] = mr.RaftGroupID
345+
row.LeaderTerms[j] = mr.LeaderTerm
336346
row.total += v
337347
}
338348
}
@@ -383,9 +393,12 @@ func newKeyVizRowFrom(mr keyviz.MatrixRow, numCols int) *KeyVizRow {
383393
Aggregate: mr.Aggregate,
384394
RouteCount: total,
385395
RouteIDsTruncated: mr.Aggregate && total > uint64(len(mr.MemberRoutes)),
386-
RaftGroupID: mr.RaftGroupID,
387-
LeaderTerm: mr.LeaderTerm,
388396
Values: make([]uint64, numCols),
397+
// Per-cell parallel arrays — populated in the column-loop
398+
// above, not here, because the first MatrixRow seen for a
399+
// route only carries that one column's identity.
400+
RaftGroupIDs: make([]uint64, numCols),
401+
LeaderTerms: make([]uint64, numCols),
389402
}
390403
if mr.Aggregate {
391404
row.RouteIDs = append([]uint64(nil), mr.MemberRoutes...)

internal/admin/keyviz_handler_test.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -499,13 +499,17 @@ func TestKeyVizHandlerSkipsFanoutForPeerCall(t *testing.T) {
499499
"recursion guard violated: handler dialled a peer despite X-Admin-Fanout-Peer being set")
500500
}
501501

502-
// TestKeyVizHandlerStampsRaftIdentity pins the Phase 2-C+ wire
503-
// extension on the JSON path: MatrixRow.RaftGroupID and
504-
// MatrixRow.LeaderTerm propagate through newKeyVizRowFrom into the
505-
// JSON KeyVizRow's raft_group_id and leader_term fields.
502+
// TestKeyVizHandlerStampsRaftIdentity pins the Phase 2-C+ per-cell
503+
// wire extension on the JSON path: MatrixRow.RaftGroupID and
504+
// MatrixRow.LeaderTerm propagate per-column through the pivot into
505+
// the JSON KeyVizRow's raft_group_ids[] and leader_terms[] arrays
506+
// (parallel to values[]). A row that appears in only some columns
507+
// reports zero (the documented "term not tracked" sentinel) for
508+
// the columns where it's absent.
506509
func TestKeyVizHandlerStampsRaftIdentity(t *testing.T) {
507510
t.Parallel()
508511
t0 := time.Unix(1_700_000_000, 0)
512+
t1 := t0.Add(time.Minute)
509513
srv := newKeyVizTestServer(t, &fakeKeyVizSource{cols: []keyviz.MatrixColumn{
510514
{
511515
At: t0,
@@ -514,6 +518,13 @@ func TestKeyVizHandlerStampsRaftIdentity(t *testing.T) {
514518
{RouteID: 2, Start: []byte("b"), End: []byte("c"), Writes: 9},
515519
},
516520
},
521+
// col 1: route 1 still active (term flipped to 43); route 2 absent.
522+
{
523+
At: t1,
524+
Rows: []keyviz.MatrixRow{
525+
{RouteID: 1, Start: []byte("a"), End: []byte("b"), Writes: 11, RaftGroupID: 7, LeaderTerm: 43},
526+
},
527+
},
517528
}})
518529
defer srv.Close()
519530

@@ -525,10 +536,12 @@ func TestKeyVizHandlerStampsRaftIdentity(t *testing.T) {
525536
require.Len(t, matrix.Rows, 2)
526537
r1, r2 := matrix.Rows[0], matrix.Rows[1]
527538
require.Equal(t, "route:1", r1.BucketID)
528-
require.Equal(t, uint64(7), r1.RaftGroupID, "RaftGroupID must propagate to JSON raft_group_id")
529-
require.Equal(t, uint64(42), r1.LeaderTerm, "LeaderTerm must propagate to JSON leader_term")
530-
// route:2 — zero values stay zero.
539+
// Per-cell: col0 was term 42, col1 was term 43 (mid-window flip).
540+
require.Equal(t, []uint64{7, 7}, r1.RaftGroupIDs, "raft_group_ids must be parallel to values")
541+
require.Equal(t, []uint64{42, 43}, r1.LeaderTerms,
542+
"leader_terms must capture per-column term so the aggregator can dedupe across the flip")
543+
// route:2 — present only in col0; col1 zero is the "term not tracked" sentinel.
531544
require.Equal(t, "route:2", r2.BucketID)
532-
require.Equal(t, uint64(0), r2.RaftGroupID)
533-
require.Equal(t, uint64(0), r2.LeaderTerm)
545+
require.Equal(t, []uint64{0, 0}, r2.RaftGroupIDs)
546+
require.Equal(t, []uint64{0, 0}, r2.LeaderTerms)
534547
}

main.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,10 +1559,22 @@ type groupTermSnapshot struct {
15591559
func publishLeaderTerms(s *keyviz.MemSampler, runtimes []*raftGroupRuntime) {
15601560
snaps := make([]groupTermSnapshot, 0, len(runtimes))
15611561
for _, rt := range runtimes {
1562-
if rt == nil || rt.engine == nil {
1562+
if rt == nil {
1563+
continue
1564+
}
1565+
// Snapshot rt.engine into a local once so a concurrent
1566+
// rt.Close() (which sets rt.engine = nil) cannot race the
1567+
// nil-check and the Status() call. On startup-error paths
1568+
// that return before joining all goroutines (e.g.
1569+
// setupAdminService failing), this goroutine can otherwise
1570+
// observe rt.engine going nil between the two reads and
1571+
// panic / trigger a race-detector failure. (Codex P2 on
1572+
// PR #720.)
1573+
engine := rt.engine
1574+
if engine == nil {
15631575
continue
15641576
}
1565-
snaps = append(snaps, groupTermSnapshot{groupID: rt.spec.id, term: rt.engine.Status().Term})
1577+
snaps = append(snaps, groupTermSnapshot{groupID: rt.spec.id, term: engine.Status().Term})
15661578
}
15671579
publishLeaderTermsFromSnapshots(s, snaps)
15681580
}

0 commit comments

Comments
 (0)