Skip to content

Commit f3cb756

Browse files
authored
keyviz admin: apply default rows budget when omitted (PR #660 follow-up) (#672)
## Summary Follow-up to PR #660, which was merged at the round-2 commit before this Codex P1 fix propagated. The orphaned change: **Codex round-3 P1**: `?rows=` was optional and parsed to `0` when omitted; `applyKeyVizRowBudget` then treats `budget <= 0` as "no cap" and returns every tracked route in one payload — defeating the 1024-row resource guard the cap was supposed to provide. A normal SPA poll without `?rows=` would fall into this trap. - `parseKeyVizParams` now pre-seeds `rows = keyVizRowBudgetCap` so an omitted query parameter inherits the cap. - `setKeyVizRowsParam` collapses an explicit `rows=0` to the cap so callers cannot disable the budget by passing 0. - Godoc updated: omitted / 0 / negative all yield the cap; explicit values above the cap are silently clamped down. ## Test plan - [x] `TestKeyVizHandlerOmittedRowsAppliesDefaultCap` — stages `keyVizRowBudgetCap+5` routes and confirms both the omitted (`""`) and `?rows=0` forms truncate down to `keyVizRowBudgetCap`. - [x] Existing `TestKeyVizHandlerHonorsRowsBudget` and `TestKeyVizHandlerClampsRowsBudgetToCap` still pass — the change is additive at the default end. - [x] `go test -race -count=1 ./internal/admin/...` clean. - [x] `golangci-lint run ./internal/admin/...` clean.
2 parents a58fb54 + b629f2b commit f3cb756

2 files changed

Lines changed: 79 additions & 5 deletions

File tree

internal/admin/keyviz_handler.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ type KeyVizRow struct {
8888
// from_unix_ms - lower bound in unix ms; 0 or omitted means unbounded
8989
// on that side (NOT the Unix epoch)
9090
// to_unix_ms - upper bound in unix ms; same 0 = unbounded contract
91-
// rows - row budget; 0 means no cap, capped at 1024 (default: 0)
91+
// rows - row budget; default and maximum is 1024 (design §4.1).
92+
// Omitted / 0 / negative all yield the cap; explicit
93+
// values above the cap are silently clamped down.
9294
//
9395
// Returns 503 codes.Unavailable when no sampler is configured so the
9496
// SPA can distinguish "keyviz disabled" from "no data yet" (the
@@ -166,7 +168,12 @@ type keyVizParams struct {
166168
}
167169

168170
func parseKeyVizParams(r *http.Request) (keyVizParams, error) {
169-
p := keyVizParams{series: keyVizDefaultSeries}
171+
// rows defaults to the cap so a normal SPA poll without the
172+
// query parameter still respects the budget — leaving it at the
173+
// zero value would let applyKeyVizRowBudget fall through to "no
174+
// cap" and return every tracked route in one payload (Codex
175+
// round-3 P1 on PR #660).
176+
p := keyVizParams{series: keyVizDefaultSeries, rows: keyVizRowBudgetCap}
170177
q := r.URL.Query()
171178
if err := setKeyVizSeriesParam(&p, q.Get("series")); err != nil {
172179
return keyVizParams{}, err
@@ -209,13 +216,17 @@ func setKeyVizTimeParam(dst *time.Time, name, raw string) error {
209216

210217
func setKeyVizRowsParam(dst *int, raw string) error {
211218
if raw == "" {
219+
// Caller pre-set dst to the default cap; preserve it.
212220
return nil
213221
}
214222
n, err := strconv.Atoi(raw)
215-
if err != nil || n < 0 {
216-
return errors.New("rows must be a non-negative integer")
223+
if err != nil {
224+
return errors.New("rows must be an integer")
217225
}
218-
if n > keyVizRowBudgetCap {
226+
if n <= 0 || n > keyVizRowBudgetCap {
227+
// Explicit 0 / negative / above-cap all collapse to the cap
228+
// (same as omitting the param) so callers can't disable the
229+
// budget by passing pathological values.
219230
n = keyVizRowBudgetCap
220231
}
221232
*dst = n

internal/admin/keyviz_handler_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,69 @@ func TestKeyVizHandlerRowsBudgetTieBreakDeterministic(t *testing.T) {
291291
}
292292
}
293293

294+
// TestKeyVizHandlerOmittedRowsAppliesDefaultCap pins Codex round-3 P1
295+
// on PR #660: when the SPA polls without ?rows=, the handler must
296+
// still apply the keyVizRowBudgetCap default — leaving p.rows at
297+
// zero would let applyKeyVizRowBudget fall through to "no cap" and
298+
// return every tracked route in one payload.
299+
func TestKeyVizHandlerOmittedRowsAppliesDefaultCap(t *testing.T) {
300+
t.Parallel()
301+
srv := newKeyVizTestServer(t, &fakeKeyVizSource{cols: []keyviz.MatrixColumn{
302+
{At: time.Unix(1_700_000_000, 0), Rows: stagedRowsForBudgetTest()},
303+
}})
304+
defer srv.Close()
305+
306+
for _, query := range []string{"", "?rows=0", "?rows=-1"} {
307+
resp := keyVizGet(t, srv.URL+query)
308+
require.Equal(t, http.StatusOK, resp.StatusCode)
309+
var matrix KeyVizMatrix
310+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&matrix))
311+
require.NoError(t, resp.Body.Close())
312+
require.Len(t, matrix.Rows, keyVizRowBudgetCap,
313+
"omitted/0/negative rows must apply the default cap (query=%q)", query)
314+
}
315+
}
316+
317+
// TestKeyVizHandlerClampsRowsBudgetToCap pins the above-cap branch of
318+
// setKeyVizRowsParam: an explicit rows= value greater than
319+
// keyVizRowBudgetCap must be silently clamped down so callers cannot
320+
// bypass the resource guard by asking for more rows than the cap.
321+
func TestKeyVizHandlerClampsRowsBudgetToCap(t *testing.T) {
322+
t.Parallel()
323+
srv := newKeyVizTestServer(t, &fakeKeyVizSource{cols: []keyviz.MatrixColumn{
324+
{At: time.Unix(1_700_000_000, 0), Rows: stagedRowsForBudgetTest()},
325+
}})
326+
defer srv.Close()
327+
328+
resp := keyVizGet(t, srv.URL+"?rows=9999")
329+
require.Equal(t, http.StatusOK, resp.StatusCode)
330+
var matrix KeyVizMatrix
331+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&matrix))
332+
require.NoError(t, resp.Body.Close())
333+
require.Len(t, matrix.Rows, keyVizRowBudgetCap,
334+
"rows=9999 must clamp down to keyVizRowBudgetCap")
335+
}
336+
337+
// stagedRowsForBudgetTest builds keyVizRowBudgetCap+5 distinct rows so
338+
// any test that exercises the budget cap can confirm truncation
339+
// occurred. The loop counter is uint64 to avoid an int→uint64
340+
// conversion that would need a gosec suppression; Start / End encode
341+
// the index as a 2-byte big-endian key.
342+
func stagedRowsForBudgetTest() []keyviz.MatrixRow {
343+
const total uint64 = keyVizRowBudgetCap + 5
344+
rows := make([]keyviz.MatrixRow, total)
345+
for i := uint64(0); i < total; i++ {
346+
n := i + 1
347+
rows[i] = keyviz.MatrixRow{
348+
RouteID: n,
349+
Start: []byte{byte(i >> 8), byte(i)},
350+
End: []byte{byte(n >> 8), byte(n)},
351+
Writes: n,
352+
}
353+
}
354+
return rows
355+
}
356+
294357
// TestKeyVizHandlerTimeBoundsParam exercises the from_unix_ms /
295358
// to_unix_ms query parameters: a non-zero pair filters columns to the
296359
// requested half-open window, while 0 means "unbounded on that side"

0 commit comments

Comments
 (0)