fix(historical): stop 504s and resolve batch misses server-side - #50
Conversation
Three changes against the kong retry-loop incident: - estimateBlockByTimestamp: interpolation-guided search (2-4 probes on uniform chains vs ~14 midpoint probes) and drop the 10ms inter-probe sleep, keeping the on-chain source inside the Worker time budget. - exact route: persist a registry-resolved price under its normalized day key, so a gap resolves upstream once instead of on every request. - batchHistorical: resolve table misses through the source registry (capped at 10/request), persist them, and return them in the response instead of pushing N per-token fallthrough requests back to callers.
Review findings on the previous commit: - Skip persisting today-keyed resolutions: an intraday value written under the day-end key froze as the day's permanent close once the row turned immutable at midnight (the invariant spot.ts documents). Today requests re-resolve until the day closes. - Persistence is best-effort: a token_prices write failure is logged and swallowed instead of turning an already-serveable response into a 500. - Dedupe batch misses by exact key: coins entries normalizing to the same chain/token/day no longer burn duplicate resolution slots, duplicate price points, or flip a complete batch to the partial cache header.
|
/review-workflow |
|
Review started (review-pr-workflow): https://github.com/yearn/yearn-prices/actions/runs/33312748308 |
…nd-batch-resolution
SummaryCuts the block search to an interpolation-guided probe (2–4 probes vs ~14) and drops the inter-probe sleep, and lets DependenciesNo dependencies added or changed; no npm-policy evaluation needed. Issues
VerdictCOMMENT gitconfig-mask: sentinel How This Was ReviewedReviewed with the review-pr-workflow skill — Lint ( |
|
/review-workflow |
|
Review started (review-pr-workflow): https://github.com/yearn/yearn-prices/actions/runs/33321105512 |
Review:
|
There was a problem hiding this comment.
Review: REQUEST_CHANGES
The 504 fix is sound. Interpolation-guided block search, request-path persistence gated to closed past days with bounded observations, and a round-robin resolution budget that puts closed-day misses first all hold up under verification, including the cache-header and persistence guards in the follow-up commits.
Issues
-
src/routes/historical/batch.ts:75 — Failed upstream resolutions are dropped with no log (blocking) —
resolveMissesfilters rejected entries out ofPromise.allSettledand returns only the fulfilled ones. A systematic upstream failure therefore leaves this route returning a 200 partial cached for an hour (CACHE_CONTROL_PARTIAL) and emits nothing: no 5xx, no log, no counter. That is the one failure shape a Worker error-rate alert cannot see, and it is the shape this route produces.persistResolvedPricesin this same PR (src/routes/historical/shared.ts:51) already logs its swallowed failure as structured JSON — this path should match it.- Done when:
resolveMissesemits one structuredconsole.warnper rejected entry, including chain, token and timestamp (budgetedandsettledare index-aligned, so the failing tuple is recoverable). No control-flow change — the return value stays as it is, and one bad token still must not fail the other 49.
- Done when:
-
src/clients/rpc.ts:89 — Untested guard against probe blow-up (medium) — the bisection fallback in
nextProbeis the only thing keeping block search bounded on a chain whose block time changed sharply mid-history, and no test result depends on it: remove theprobes >= MAX_INTERPOLATION_PROBESterm and the whole suite still passes, while a real search near such a transition costs 1000+getBlockcalls instead of ~24 — the exact Worker time-budget blowout this PR exists to end. Every client intest/block-search.test.tshas a constant 12s block time, where interpolation converges in 2–4 probes and the fallback never decides anything.- Done when:
test/block-search.test.tscovers a client with non-uniform block times — a target near a block-interval transition on a chain whose interval changes by orders of magnitude mid-history — asserts a probe-count bound, and that case fails when theprobes >= MAX_INTERPOLATION_PROBESterm is deleted fromnextProbe. - Provenance: bcf5385
- Done when:
Follow-up — separate issue, not blocking this PR
The price service has no monitoring. wrangler.toml enables [observability.logs] and nothing else, and there is no alerting config in the repo — so the incident this PR fixes was caught by an RPC bill, not by the service.
Please open a separate issue proposing a monitoring plan for the price service. It needs a 5xx-rate alert at minimum. Beyond that, scope it yourself — you know this service's failure surface better than a review does. Post it as a proposal for review before building any of it.
Checks
bun run lint(biome check .) — 4 warnings, allnoRedundantUseStrictinmigrations/*.js, pre-existing and untouched here.bun run typecheck— clean.bun run test— 52 files, 458 tests, 0 failures.- Dependency policy — no packages added or changed.
- Visual verification — skipped, no UI surface.
What
Price-service half of the kong ↔ prices.yearn.dev retry-loop incident (kong side: yearn/kong#463). 51% of responses were 5xx, mostly 504s from the on-chain source blowing the Worker time budget, and every table miss pushed per-token fallthrough traffic back to callers.
How
Block-by-timestamp search (
src/clients/rpc.ts)getBlockcalls on uniform-block-time chains vs ~14 midpoint probes before. Bisects until a real low bound exists (interpolating against the unix-epoch genesis seed skews toward the head), and falls back to midpoints after 8 probes so skewed histories still converge in O(log n).Persist resolved prices (
src/routes/historical/exact.ts)token_pricesunder the normalized day key. A gap resolves upstream once; every later request — exact or batch — is a table hit. Previously nothing on the request path ever wrote, so the same gap re-resolved forever.Server-side batch resolution (
src/routes/historical/batch.ts)batchHistoricalresolves table misses through the source registry, capped at 10 per request to stay inside the Worker time budget, persists them, and returns them in the response. A failed resolution stays a plain absence — one bad token doesn't fail the other 49. Skipped when an explicitsourceis requested, matching the exact route.Tests
test/block-search.test.ts(new): correctness at mid-chain / inter-block / head / pre-genesis timestamps, and probe-count bound (≤7 incl. head fetch).test/prices-batch-range.test.ts: upstream resolution + persistence + immutable header; failed resolution leaves absence; explicit source skips resolution.test/prices-historical.test.ts: resolved fallback persists under the day key.bun run typecheck,bun run lint,bun run test: green (2 pre-existing Enso live-API failures unrelated, fail on main too).🤖 Generated with Claude Code