Skip to content

Commit cc7108f

Browse files
authored
feat(main): keyviz wiring follow-up (--keyvizHistoryColumns + Phase-2 read TODO) (#651)
## Summary Follow-up to PR #647, which was merged at the round-1 commit before round-2 review fixes propagated. This PR carries the orphaned round-2 changes: - New `--keyvizHistoryColumns` flag (defaults to `keyviz.DefaultHistoryColumns = 1440`, i.e. 24h at 60s) so operators can shorten the ring buffer for high-cardinality clusters without rebuilding. - `startKeyVizFlusher` early-returns when the sampler is nil instead of spawning a goroutine that just parks on `ctx.Done` — the goroutine was harmless but had no signal. - TODO on `observeMutation` documenting the Phase-2 read-sampling milestone (design §5.1, §10) so future readers don't think the missing read path is a regression. Until that wiring lands the matrix's `Reads`/`ReadBytes` series stay zero. These items came out of Claude bot's round-2 review of #647 but landed after the merge button was pressed. ## Test plan - [x] `go build .`, `go vet .`, `golangci-lint run ./...` clean. - [x] `go test -race -count=1 -run 'TestBuildKeyVizSampler|TestSeedKeyVizRoutes|TestStartKeyVizFlusher' .` clean.
2 parents 34651ad + 8f5d0e0 commit cc7108f

5 files changed

Lines changed: 74 additions & 6 deletions

File tree

keyviz/sampler.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ const (
8282
DefaultMaxMemberRoutesPerSlot = 256
8383
)
8484

85+
// MaxHistoryColumns is the upper bound on opts.HistoryColumns. The
86+
// ring buffer pre-allocates a slice of capacity HistoryColumns at
87+
// construction; misconfiguration (e.g. an operator typo of
88+
// 100_000_000) would otherwise reserve gigabytes up front. 100 000
89+
// columns at the default 60s Step is ~70 days of history — longer
90+
// retention is the Phase 3 persistence path's job, not the in-memory
91+
// ring's.
92+
const MaxHistoryColumns = 100_000
93+
8594
// MemSamplerOptions configures NewMemSampler. Zero values fall back to
8695
// the Default* constants above; passing a struct literal with only the
8796
// fields you care about is the expected call style.
@@ -308,6 +317,9 @@ func NewMemSampler(opts MemSamplerOptions) *MemSampler {
308317
if opts.HistoryColumns <= 0 {
309318
opts.HistoryColumns = DefaultHistoryColumns
310319
}
320+
if opts.HistoryColumns > MaxHistoryColumns {
321+
opts.HistoryColumns = MaxHistoryColumns
322+
}
311323
if opts.MaxTrackedRoutes <= 0 {
312324
opts.MaxTrackedRoutes = DefaultMaxTrackedRoutes
313325
}
@@ -798,6 +810,17 @@ func (s *MemSampler) Step() time.Duration {
798810
return s.opts.Step
799811
}
800812

813+
// HistoryColumns returns the configured ring-buffer length after
814+
// applying defaults and the MaxHistoryColumns clamp. Wiring tests use
815+
// this to verify --keyvizHistoryColumns is forwarded end-to-end
816+
// without exposing the internal opts struct.
817+
func (s *MemSampler) HistoryColumns() int {
818+
if s == nil {
819+
return DefaultHistoryColumns
820+
}
821+
return s.opts.HistoryColumns
822+
}
823+
801824
// appendDrainedRow swaps the slot's counters to zero and appends a
802825
// MatrixRow when any counter was non-zero. Idle slots are skipped.
803826
// Metadata is read under the slot's metaMu so a concurrent

keyviz/sampler_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,31 @@ func TestNonPositiveOptionsFallBackToDefaults(t *testing.T) {
869869
}
870870
}
871871

872+
// TestHistoryColumnsClampedAboveMax pins the upper bound on
873+
// HistoryColumns. The ring buffer pre-allocates a slice of capacity
874+
// HistoryColumns at construction; an operator typo (e.g.
875+
// 100_000_000) would otherwise reserve gigabytes up front. Confirm
876+
// that values above MaxHistoryColumns are silently clamped to
877+
// MaxHistoryColumns instead of trusted as-is.
878+
func TestHistoryColumnsClampedAboveMax(t *testing.T) {
879+
t.Parallel()
880+
s, _ := newTestSampler(t, MemSamplerOptions{
881+
Step: time.Second,
882+
HistoryColumns: MaxHistoryColumns * 10,
883+
})
884+
if s.opts.HistoryColumns != MaxHistoryColumns {
885+
t.Fatalf("HistoryColumns clamp = %d, want %d", s.opts.HistoryColumns, MaxHistoryColumns)
886+
}
887+
// At the cap exactly: preserved.
888+
s2, _ := newTestSampler(t, MemSamplerOptions{
889+
Step: time.Second,
890+
HistoryColumns: MaxHistoryColumns,
891+
})
892+
if s2.opts.HistoryColumns != MaxHistoryColumns {
893+
t.Fatalf("HistoryColumns at cap = %d, want %d", s2.opts.HistoryColumns, MaxHistoryColumns)
894+
}
895+
}
896+
872897
// TestStepAccessor pins the Step() accessor contract: returns the
873898
// configured Step (after defaulting) for a constructed sampler, and
874899
// returns DefaultStep for a typed nil so callers wiring RunFlusher

kv/sharded_coordinator.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,13 @@ func (c *ShardedCoordinator) txnLogs(reqs *OperationGroup[OP]) ([]*pb.Request, e
980980
// pre-commit, so a mutation that subsequently fails its Raft
981981
// proposal is still recorded — the heatmap reflects offered load,
982982
// not just committed writes (intentional for traffic visualisation).
983+
//
984+
// TODO(keyviz Phase 2): the design (§5.1, §10) calls for read
985+
// sampling on the node that serves the read (LeaseRead /
986+
// LinearizableRead / follower reads). Until that wiring lands the
987+
// matrix's Reads / ReadBytes series stay zero. Tracked as a Phase-2
988+
// milestone in docs/admin_ui_key_visualizer_design.md — not a
989+
// regression for the writes-first slice this method covers.
983990
func (c *ShardedCoordinator) observeMutation(routeID uint64, mut *pb.Mutation) {
984991
if c.sampler == nil {
985992
return

main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ var (
135135
keyvizStep = flag.Duration("keyvizStep", keyviz.DefaultStep, "Flush interval / matrix-column resolution for the keyviz sampler")
136136
keyvizMaxTrackedRoutes = flag.Int("keyvizMaxTrackedRoutes", keyviz.DefaultMaxTrackedRoutes, "Maximum routes tracked individually before excess routes coarsen into virtual buckets")
137137
keyvizMaxMemberRoutesPerSlot = flag.Int("keyvizMaxMemberRoutesPerSlot", keyviz.DefaultMaxMemberRoutesPerSlot, "Maximum members listed on a virtual bucket; excess routes still drive the bucket counters")
138+
keyvizHistoryColumns = flag.Int("keyvizHistoryColumns", keyviz.DefaultHistoryColumns, "Maximum matrix columns retained in the keyviz ring buffer (each column = one Step)")
138139
)
139140

140141
const adminTokenMaxBytes = 4 << 10
@@ -1376,6 +1377,7 @@ func buildKeyVizSampler() *keyviz.MemSampler {
13761377
}
13771378
return keyviz.NewMemSampler(keyviz.MemSamplerOptions{
13781379
Step: *keyvizStep,
1380+
HistoryColumns: *keyvizHistoryColumns,
13791381
MaxTrackedRoutes: *keyvizMaxTrackedRoutes,
13801382
MaxMemberRoutesPerSlot: *keyvizMaxMemberRoutesPerSlot,
13811383
})
@@ -1411,9 +1413,13 @@ func seedKeyVizRoutes(s *keyviz.MemSampler, engine *distribution.Engine) {
14111413
// startKeyVizFlusher launches RunFlusher in the supplied errgroup
14121414
// and harvests the in-progress step with a final Flush after the
14131415
// goroutine returns, so a graceful shutdown does not lose the most
1414-
// recent partial column. Nil-safe: a disabled sampler reduces the
1415-
// goroutine to ctx-wait + a no-op Flush.
1416+
// recent partial column. Skip the goroutine entirely when the
1417+
// sampler is disabled — RunFlusher would just park on ctx.Done with
1418+
// no work to do, which is a free goroutine but adds no signal.
14161419
func startKeyVizFlusher(ctx context.Context, eg *errgroup.Group, s *keyviz.MemSampler) {
1420+
if s == nil {
1421+
return
1422+
}
14171423
eg.Go(func() error {
14181424
keyviz.RunFlusher(ctx, s, s.Step())
14191425
s.Flush()

main_keyviz_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@ import (
1414
// TestBuildKeyVizSamplerHonorsEnabledFlag pins the on/off contract:
1515
// --keyvizEnabled=false returns nil (so coordinator/admin server take
1616
// the disabled paths), and --keyvizEnabled=true with explicit options
17-
// returns a configured sampler.
17+
// returns a configured sampler whose options match every flag.
1818
func TestBuildKeyVizSamplerHonorsEnabledFlag(t *testing.T) {
1919
t.Parallel()
20-
withFlags(t, false, time.Second, 5, 7, func() {
20+
withFlags(t, false, time.Second, 5, 7, 16, func() {
2121
require.Nil(t, buildKeyVizSampler())
2222
})
23-
withFlags(t, true, 250*time.Millisecond, 5, 7, func() {
23+
withFlags(t, true, 250*time.Millisecond, 5, 7, 16, func() {
2424
s := buildKeyVizSampler()
2525
require.NotNil(t, s)
2626
require.Equal(t, 250*time.Millisecond, s.Step())
27+
// Pin the --keyvizHistoryColumns forwarding so a future
28+
// refactor that drops the flag from buildKeyVizSampler is
29+
// caught here, not at runtime.
30+
require.Equal(t, 16, s.HistoryColumns())
2731
})
2832
}
2933

@@ -95,23 +99,26 @@ func withFlags(
9599
t *testing.T,
96100
enabled bool,
97101
step time.Duration,
98-
maxTracked, maxMembers int,
102+
maxTracked, maxMembers, historyColumns int,
99103
fn func(),
100104
) {
101105
t.Helper()
102106
prevEnabled := *keyvizEnabled
103107
prevStep := *keyvizStep
104108
prevMaxTracked := *keyvizMaxTrackedRoutes
105109
prevMaxMembers := *keyvizMaxMemberRoutesPerSlot
110+
prevHistoryColumns := *keyvizHistoryColumns
106111
*keyvizEnabled = enabled
107112
*keyvizStep = step
108113
*keyvizMaxTrackedRoutes = maxTracked
109114
*keyvizMaxMemberRoutesPerSlot = maxMembers
115+
*keyvizHistoryColumns = historyColumns
110116
defer func() {
111117
*keyvizEnabled = prevEnabled
112118
*keyvizStep = prevStep
113119
*keyvizMaxTrackedRoutes = prevMaxTracked
114120
*keyvizMaxMemberRoutesPerSlot = prevMaxMembers
121+
*keyvizHistoryColumns = prevHistoryColumns
115122
}()
116123
fn()
117124
}

0 commit comments

Comments
 (0)