Skip to content

fix(health): measure list-typed data keys with LLEN, not STRLEN#5254

Merged
koala73 merged 1 commit into
mainfrom
fix/health-list-key-forecast-bets
Jul 12, 2026
Merged

fix(health): measure list-typed data keys with LLEN, not STRLEN#5254
koala73 merged 1 commit into
mainfrom
fix/health-list-key-forecast-bets

Conversation

@koala73

@koala73 koala73 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

The bug

/api/health measures every registered data key with STRLEN. But forecast:bets:history:v1 is a Redis listseed-forecast-bets.mjs:161-163 writes it with LPUSH/LTRIM. STRLEN against a list returns WRONGTYPE, the pipeline records that as a per-command error, and classifyKey turns it into REDIS_PARTIAL (records: null) — permanently, for a seeder that is perfectly healthy.

Prod has reported forecastBets: REDIS_PARTIAL ever since #5233 registered the key, and it is one of the two warns currently holding /api/health at DEGRADED.

Verified against prod Redis

STRLEN forecast:bets:history:v1 -> WRONGTYPE Operation against a key holding the wrong kind of value
                                -> {"status":"REDIS_PARTIAL","records":null}
LLEN   forecast:bets:history:v1 -> 1
                                -> {"status":"OK","records":4,"seedAgeMin":691}

The seeder was never broken: seed-meta:forecast:bets is fresh with recordCount: 4.

The fix

LIST_DATA_KEYS names the list-typed keys, dataLenCommand() issues LLEN for them and STRLEN for everything else, and keyHasData() gates presence on len > 0 for lists.

That last part is load-bearing. strlenIsData() rejects exactly 10 bytes as the __WM_NEG__ sentinel — a string concept. Reusing it for LLEN would silently classify a 10-element history as EMPTY (crit). The test pins that case explicitly.

A TYPE sweep of all 218 registered data keys against prod Redis confirms forecastBets is the only non-string key today, so the blast radius is exactly one key.

Tests

tests/health-list-data-keys.test.mjs — written first, confirmed failing against unfixed code (dataLenCommand is not a function; the 10-element list scored records: 0 instead of 7). Covers both halves of the bug plus the regressions:

  • LLEN is issued for list keys, STRLEN for string keys
  • a populated bets list with fresh seed-meta classifies OK, not REDIS_PARTIAL
  • a 10-element list is data, not the 10-byte sentinel
  • a real per-command Redis error still surfaces REDIS_PARTIAL
  • string keys keep NEG_SENTINEL semantics (strlen 10 is not data)

172 tests pass across every file that touches health internals (health-classify, health-content-age, health-redis-down-status, forecast-health-registration, health-compact-cdn-cache, bootstrap, mcp-bootstrap-parity, resilience-scores-seed, temporal-anomalies-cache). biome lint and typecheck:api clean.

Not in scope

Found while triaging the same DEGRADED response — filing separately:

  • acledIntel (crit)seed-conflict-intel has no ACLED credentials (VITE_ACLED_ACCESS_TOKEN is set but empty; the seeder reads ACLED_EMAIL/ACLED_PASSWORD or ACLED_ACCESS_TOKEN), and its GDELT fallback is dead upstream. It publishes nothing, exits 0, and its keys have expired. Needs credentials, not code.
  • defensePatents (crit) — the seeder targets search.patentsview.org, which is now NXDOMAIN; PatentsView has migrated to USPTO's Open Data Portal (needs an API key + a rewrite). The cron is already disabled; health still scores the key as crit.

https://claude.ai/code/session_014xoeVpgK3YJtUcr4t5VzF4

/api/health STRLENs every registered data key. `forecast:bets:history:v1` is a
Redis LIST (LPUSH/LTRIM in scripts/seed-forecast-bets.mjs), so STRLEN returns
WRONGTYPE, the pipeline records a per-command error, and classifyKey pins the
key at REDIS_PARTIAL (records: null) — permanently, for a seeder that is
perfectly healthy. Prod has reported `forecastBets: REDIS_PARTIAL` ever since
#5233 shipped the key, and it was one of the two warns holding /api/health at
DEGRADED.

Verified against prod Redis:
  STRLEN forecast:bets:history:v1 -> WRONGTYPE ... -> REDIS_PARTIAL, records null
  LLEN   forecast:bets:history:v1 -> 1          -> OK, records 4

LIST_DATA_KEYS names the list-typed keys; dataLenCommand() picks LLEN for them
and STRLEN for everything else, and keyHasData() gates presence on `len > 0`
for lists. That last part is load-bearing: strlenIsData() rejects exactly 10
bytes as the '__WM_NEG__' sentinel, which is a STRING concept — reusing it for
LLEN would silently classify a 10-ELEMENT history as EMPTY (crit). A TYPE sweep
of all 218 registered data keys against prod confirms forecastBets is the only
non-string key today.

tests/health-list-data-keys.test.mjs pins both halves (LLEN is issued; a
10-element list is data) plus the regressions: a real per-command error still
surfaces REDIS_PARTIAL, and string keys keep NEG_SENTINEL semantics.
@strix-security

strix-security Bot commented Jul 12, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 9412e28.


Reviewed by Strix
Re-run review · Configure security review settings

@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Jul 12, 2026 9:08pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a production DEGRADED health status caused by STRLEN being issued against forecast:bets:history:v1, a Redis list key written with LPUSH/LTRIM. STRLEN on a list returns WRONGTYPE, which the pipeline recorded as a per-command error and classifyKey permanently mapped to REDIS_PARTIAL — even though the seeder itself was healthy.

  • Introduces LIST_DATA_KEYS, dataLenCommand() (issues LLEN for list keys, STRLEN for strings), and keyHasData() (uses len > 0 for lists, the strlenIsData sentinel logic for strings), applied in both classifyKey and isCascadeCovered.
  • Adds tests/health-list-data-keys.test.mjs covering LLEN dispatch, the 10-element-vs-10-byte sentinel edge case, real Redis error surfacing, and string-key regression.

Confidence Score: 5/5

Safe to merge. The change is narrowly scoped to the Redis measurement command for list-typed keys and has no effect on string keys or any other part of the health pipeline.

The fix is surgical: one new Set, two small pure functions, two call-site swaps. All three paths (list key healthy, list key erroring, string key with sentinel) are explicitly pinned by tests confirmed failing before the fix. The only observable gap is a stale comment on the keyStrens map variable.

No files require special attention. api/health.js carries a minor comment inconsistency on the keyStrens map that is worth cleaning up but does not affect behaviour.

Important Files Changed

Filename Overview
api/health.js Adds LIST_DATA_KEYS set, dataLenCommand(), and keyHasData() to correctly measure list-typed Redis keys with LLEN instead of STRLEN; fixes forecastBets permanently reporting REDIS_PARTIAL. Minor: the keyStrens map comment/name no longer fully describes its contents.
tests/health-list-data-keys.test.mjs New test file covering both halves of the bug: correct LLEN/STRLEN command dispatch and correct presence semantics (10-element list ≠ 10-byte NEG_SENTINEL). Also pins regressions for real Redis errors and string-key sentinel behaviour.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant H as handler (health.js)
    participant DC as dataLenCommand()
    participant R as Redis pipeline
    participant CK as classifyKey()

    H->>DC: allDataKeys.map(dataLenCommand)
    DC-->>H: LLEN for list keys / STRLEN for string keys
    H->>R: pipeline([LLEN/STRLEN, GET meta, EXISTS markers])
    R-->>H: results (numeric len or error per key)
    H->>H: populate keyStrens / keyErrors maps
    H->>CK: classifyKey(name, redisKey, opts, ctx)
    CK->>CK: "keyHasData: list->len>0, string->strlenIsData"
    CK-->>H: "{status, records, ...}"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant H as handler (health.js)
    participant DC as dataLenCommand()
    participant R as Redis pipeline
    participant CK as classifyKey()

    H->>DC: allDataKeys.map(dataLenCommand)
    DC-->>H: LLEN for list keys / STRLEN for string keys
    H->>R: pipeline([LLEN/STRLEN, GET meta, EXISTS markers])
    R-->>H: results (numeric len or error per key)
    H->>H: populate keyStrens / keyErrors maps
    H->>CK: classifyKey(name, redisKey, opts, ctx)
    CK->>CK: "keyHasData: list->len>0, string->strlenIsData"
    CK-->>H: "{status, records, ...}"
Loading

Comments Outside Diff (1)

  1. api/health.js, line 987-994 (link)

    P2 keyStrens comment/name is now misleading for list keys

    The comment on line 987 still reads keyStrens: byte length per data key, but keyStrens now also holds LLEN results (element counts) for list-typed keys. A reader tracing a list key through classifyKey will find the map described as storing byte lengths even though it stores element counts for that key. Worth updating the comment to reflect the dual semantics, e.g. keyStrens: byte length (STRLEN) or element count (LLEN) per data key.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "fix(health): measure list-typed data key..." | Re-trigger Greptile

@koala73
koala73 merged commit e615188 into main Jul 12, 2026
29 checks passed
@koala73
koala73 deleted the fix/health-list-key-forecast-bets branch July 12, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant