{"record":{"id":"ae9161d16590d4ba","repo":"koala73/worldmonitor","slug":"cache-all-null","errorCode":null,"errorMessage":"cache_all_null","messagePattern":"cache_all_null","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/mcp/registry/rpc-tools.ts","lineNumber":1224,"sourceCode":"        { key: `seed-meta:consumer-prices:categories:${code}:30d`, maxStaleMin: 1500 },\n        { key: `seed-meta:consumer-prices:movers:${code}:30d`,     maxStaleMin: 1500 },\n        { key: `seed-meta:consumer-prices:spread:${code}`,         maxStaleMin: 1500 }, // producer's actual key shape\n        { key: `seed-meta:consumer-prices:freshness:${code}`,      maxStaleMin: 1500 },\n      ];\n\n      const [dataResults, metaResults] = await Promise.all([\n        Promise.all(dataKeys.map((k) => readJsonFromUpstash(k))),\n        Promise.all(freshnessChecks.map((c) => readJsonFromUpstash(c.key))),\n      ]);\n\n      // F6 contract parity with the cache-tool path (executeTool, ~line 1139):\n      // if every data read is null/undefined, this is a degenerate-empty\n      // response (Redis transient / stampede / pre-seed). Throw so\n      // dispatchToolsCall reports a normal tool-execution failure. For Pro\n      // callers the already-reserved slot stays charged because the tool has\n      // executed.\n      if (dataResults.every((v: unknown) => v === null || v === undefined)) {\n        throw new Error('cache_all_null');\n      }\n\n      const { cached_at, stale } = evaluateFreshness(freshnessChecks, metaResults);\n\n      return {\n        cached_at,\n        stale,\n        country_code: code,\n        data: {\n          overview: dataResults[0],\n          categories: dataResults[1],\n          movers: dataResults[2],\n          retailerSpread: dataResults[3],\n          freshness: dataResults[4],\n        },\n      };\n    },\n    // Hybrid tool covers the consumer-prices domain via direct Redis reads","sourceCodeStart":1206,"sourceCodeEnd":1242,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/mcp/registry/rpc-tools.ts#L1206-L1242","documentation":"Thrown inside the country-markets MCP tool path when every Redis data read (overview, categories, movers, etc. for a given country_code) returned null or undefined. This is a contract-parity guard matching the cache-tool executeTool path: if ALL data keys are null the response is degenerate (Redis transient, stampede, or pre-seed), so it throws a plain Error to surface as a tool-execution failure in dispatchToolsCall. For Pro callers the already-reserved billing slot stays charged because the tool did execute.","triggerScenarios":"Calling the country markets snapshot tool for a country_code where all market data keys are null in Redis — before the market seeder has populated that country, after a Redis flush, or during a stampede where all concurrent cold reads see null. The freshness metadata reads may still succeed (seed-meta keys exist), but the data keys themselves are absent.","commonSituations":"A country_code that has never been seeded (the market seeder covers a specific symbol set); a Redis flush or eviction that dropped the data keys but left seed-meta keys; a weekend/holiday where the market data producer did not run and TTLs expired.","solutions":["Verify the market data keys exist in Redis for that country_code — check GET /api/health for the relevant market seed-meta entries.","Retry after the market data cadence — stampede and transient null conditions resolve on TTL refresh.","Try a different country_code known to be seeded (e.g. 'US') to confirm whether the issue is country-specific or systemic.","If systemic, verify the market seeder (Yahoo Finance / provider) is running and the data keys are being written."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Verify market data is seeded for the country before calling\nconst health = await fetch('/api/health').then(r => r.json());\nconst marketKeys = health.coverage ? Object.keys(health.coverage).filter(k => k.includes(`market:${countryCode}`)) : [];\nif (marketKeys.length === 0) {\n  throw new Error(`No market data seeded for ${countryCode}; try a different country or wait for seeder`);\n}","typeGuard":"function isCacheAllNullError(e: unknown): boolean {\n  return e instanceof Error && e.message === 'cache_all_null';\n}","tryCatchPattern":"try {\n  const snapshot = await callMcpTool('get_market_snapshot', { country_code: 'US' });\n} catch (e) {\n  if (e instanceof Error && e.message === 'cache_all_null') {\n    // Degenerate empty — retry after seeder cycle, or try a known-seeded country\n    await backoffRetry(30_000);\n  } else throw e;\n}","preventionTips":["Test with a known-seeded country (e.g. 'US') to distinguish country-specific gaps from systemic outages.","Monitor market seed-meta keys in GET /api/health.","Be aware that for Pro callers the billing slot is already charged even on this failure — avoid rapid retries."],"tags":["mcp","redis","cache-miss","markets","degenerate-empty"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}