feat(commandlog): SCAN large-reply hash-skew advisory (valkey#3955) - #338
Conversation
- Pure analyzer over stored large-reply entries: SCAN-family filter, COUNT parsing (default 10, MATCH/NOVALUES/cursor permutations), bytes-per-requested-element ratio vs tunable budget, recurrence weighting with extreme single-sighting override - GET /commandlog-analytics/scan-skew endpoint, worst-first ranking - ScanSkewAdvisory alert on the SlowLog large-reply tab with remediation copy; hidden when no offenders - Type anomaly API client responses (replaces pre-existing any usage)
…sighting label - keyless SCAN offenders no longer get keyed-scan re-create-the-key advice; message points at the main keyspace dictionary instead - advisory intro copy distinguishes keyed vs keyspace remediation - sighting count renders singular for single-hit extreme offenders
|
@BugBot review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7ea64e5. Configure here.
KIvanow
left a comment
There was a problem hiding this comment.
Nice work - the SCAN-family parser is solid (cursor offsets per verb, MATCH before/after COUNT, NOVALUES, and default COUNT=10 all check out). A few things to address before merge, none of them structural:
Should fix:
- Small-collection false positive.
duration / counttreats COUNT as an element count, but for a listpack/intset-encoded collection the server returns all elements in one reply regardless of COUNT. A 200-field listpack hash scanned with default COUNT=10 will show a high bytes/element and get flagged - and the "re-create the key" advice is actively wrong there (it's a normal full return, not a degenerate chain). Can we either guard this or at least caveat it in the advisory copy? limitis unclamped -parseInt(limit)in the controller has no upper bound, and the service only defaults when falsy.?limit=1000000will fetch and in-memory-analyze that many rows. Please clamp to a sane max.
Nits:
3. The anomaly-any retyping (types/anomaly.ts + the 5 client fns) is unrelated to SCAN skew and inflates the diff - would prefer it split into its own PR, but not a blocker.
4. SlowLog.tsx comment says the advisory "works the same with or without a time filter," but it does pass startTime/endTime, so results are window-scoped - comment is misleading.
5. Test gap: MATCH-after-COUNT is never tested, even though order-independence is the parser's trickiest claim. Worth a case.
Verdict: minor nits - happy to approve once the small-collection caveat and the limit clamp land.
… limit - Skip keyed-scan replies under 256KB: listpack/intset collections return whole in one reply, which is normal, not a degenerate chain - Add compact-encoding caveat to the keyed advisory remediation copy - Clamp the scan-skew analyze limit to 5000 in the controller - Correct SlowLog advisory comment: results are window-scoped - Test MATCH-after-COUNT parsing order-independence
|
Addressed in ef9103f:
|
|
One follow-up on the compact-encoding caveat: it currently lives only in the backend |
- Render offender.message under each row so backend remediation copy — including the compact-encoding caveat and the keyed vs keyspace distinction — reaches users instead of living only in the API payload - Slim the hardcoded intro to avoid duplicating per-offender guidance
|
Surfaced in the just-pushed commit — |
- getScanSkewAnalysis loaded the newest large-reply rows of every command, so busy HGETALL/GET/LRANGE traffic could fill the analysis window and hide real SCAN offenders; pass the storage command substring filter (matches SCAN/SSCAN/HSCAN/ZSCAN case-insensitively)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bcac271. Configure here.
…rray - The sqlite adapter applied LIKE to the JSON-serialized command array, so keys/args containing the filter text matched (GET user:scan:results satisfied command=SCAN) and diluted the scan-skew window; extract the verb with json_extract to align with postgres (command[1] ILIKE) and memory (command[0] substring) - Add a cross-adapter regression spec for verb-only matching
KIvanow
left a comment
There was a problem hiding this comment.
looks good after the changes!

Summary
Adds a SCAN large-reply / hash-skew advisory for valkey-io/valkey#3955: a skewed hashtable (degenerate hash chain) makes SCAN-family calls return replies vastly larger than the requested
COUNT. The server can't tell you this happened — but the signature is fully visible in the large-reply command log we already persist. Second of the five upstream-issue detector cards (first was #337).No new polling, no new MetricType, no
packages/mcpchanges (deferred while #330–#332 are in flight) — the analysis rides the stored large-reply entries on demand.Changes
scan-skew-analyzer.ts— pure analyzer over stored large-reply entries: filters to the SCAN family (SCAN/SSCAN/HSCAN/ZSCAN), parses the requestedCOUNTfrom the raw command (absent → server default 10; MATCH before/after COUNT, cursor position per verb, HSCAN NOVALUES), and computes reply-bytes per requested element against a named tunable budget (SCAN_SKEW_BYTES_PER_ELEMENT = 4096). Recurrence weighting: a key surfaces at ≥2 sightings, or a single sighting at ≥10× the budget (SCAN_SKEW_EXTREME_MULTIPLIER). Proportional large replies and one-off ordinary sightings stay quiet.GET /commandlog-analytics/scan-skew— reuses the stored-entry query (type forced tolarge-reply, whose magnitude column is bytes); returns offenders ranked worst-first with per-key advisory copy.ScanSkewAdvisoryon the Slow Log page's large-reply tab — polls the endpoint at 30s (only while that tab is active), respects the time filter, lists offender keys with verb, sighting count and worst bytes-per-element, with the valkey#3955 remediation copy (re-create the key, or upgrade once the upstream fix lands). Hidden when there are no offenders.anygenerics chore deferred from feat(monitor): config-hazard advisory — default user disabled + AOF silent data loss (valkey#3983) #337 is resolved here:apps/web/src/types/anomaly.tsmirrors the proprietary wire shapes (web can't import the optional proprietary module) and the five anomaly API client functions are now typed; removed a needlessas anyincommandlog-analytics.service.ts.Verification
large-replytype, default limit, pass-through filters — 3 tests.ScanSkewAdvisoryrender states (offenders / empty / not loaded) — 3 tests.tsc --noEmitandeslintclean on api and web; full web suite 294/294 green.Checklist
Note
Low Risk
Read-only analytics over existing stored logs plus a narrow SQLite query fix; no auth, persistence schema, or runtime Redis behavior changes.
Overview
Adds on-demand SCAN skew detection for degenerate hash chains (valkey#3955) by analyzing stored large-reply command log entries—no new polling or metrics pipeline.
A new
scan-skew-analyzerparses SCAN-family commands, compares reply bytes to requestedCOUNT, and surfaces offenders with recurrence/extreme thresholds and compact-encoding/keyless-SCAN caveats.GET /commandlog-analytics/scan-skewexposes this (type forced tolarge-reply,command: 'SCAN', capped limit). SharedScanSkewReporttypes land in@betterdb/shared.The Slow Log large-reply tab polls the endpoint and shows
ScanSkewAdvisorywhen offenders exist (respects time range). SQLite command-log filtering now matches the verb only viajson_extract(command, '$[0]')so keys likeuser:scan:resultsare not mistaken for SCAN.Also replaces anomaly API
anyresponses withapps/web/src/types/anomaly.tswire types and drops an unnecessaryas anyin slow-log pattern caching.Reviewed by Cursor Bugbot for commit dd6c61d. Bugbot is set up for automated code reviews on this repo. Configure here.