Skip to content

perf: cache full HistoryQueryResult + add edge cache headers#99

Open
hubsmoke wants to merge 37 commits into
developfrom
perf/cache-history-and-edge
Open

perf: cache full HistoryQueryResult + add edge cache headers#99
hubsmoke wants to merge 37 commits into
developfrom
perf/cache-history-and-edge

Conversation

@hubsmoke

@hubsmoke hubsmoke commented May 6, 2026

Copy link
Copy Markdown
Member

Summary

Two optimizations targeting warm-path latency on GET /api/v2/resolve/dpid/{n} (currently ~3s warm, even with Redis enabled).

Why warm hits are slow today: ceramic.loadStream(streamID) runs on every request in getCodexHistory to enumerate the commit log. Per-commit metadata is already Redis-cached, but the loadStream + enumerate work happens regardless. Public HTTP responses also have no Cache-Control header, so Cloudflare can't cache them at the edge.

Changes

  1. Full-history Redis cache in getCodexHistory (key resolver-${DPID_ENV}-history-full-${streamId}). On hit, return immediately and skip ceramic.loadStream entirely. Per-commit cache below remains as the cold-path fallback. Exported getKeyForHistoryFull so future push-invalidation (e.g. from desci-server publish flow) can delete it.
  2. CACHE_TTL_HISTORY_FULL env var, default 60 seconds. Short by design — bounds publish→visibility staleness since the resolver has no push-invalidation today. Tunable without redeploy.
  3. Cache-Control: public, s-maxage=60, stale-while-revalidate=300 on successful resolveDpidHandler responses. Errors (400/404/500) remain uncached.

Drive-by (unblocks pre-commit on this branch)

The repo's pre-commit eslint hook fails on 4 pre-existing errors in src/api/v2/data/getIpfsFolder.ts (1 unused import referenced in commented-out code, 3 deliberate any types around dynamic IPFS DAG fetches). Added inline eslint-disable comments to unblock. Should be cleaned up properly in a follow-up. Also includes 2 trivial prettier auto-formats on dpids.ts and pagination.spec.ts from the same hook run.

Rollout plan

This is the last of 3 perf PRs and has the largest blast radius. Companion PRs (in order):

  1. dpid-rewrite-worker#2 — preconnect (smallest, lowest risk)
  2. nodes-web-v2#1538 — SSR resolver-unblock (largest user impact)
  3. This PR — resolver caching (largest blast radius, ship after others soak)

Steps

  1. Merge to main when companion PRs have been live ≥30 min without regressions.
  2. CI builds image via .github/workflows/build-dpid-resolver.yaml.
  3. Staging deploy first — bump tag in kubernetes/deployment.staging.yaml via pushVersion.sh. Soak ≥30 min.
  4. Production deploy — bump tag in kubernetes/deployment.prod.yaml.

Pre-deploy checklist

  • Confirm REDIS_HOST / REDIS_PORT are set on staging + prod resolver pods (else cache is silently bypassed — code is null-safe).
  • Decide on CACHE_TTL_HISTORY_FULL — leave at default 60s for first deploy, tune after measurement.
  • Verify Cloudflare is configured to honor Cache-Control on beta.dpid.org/api/* (default behavior should be fine, but confirm no override).

Acceptance criteria

Staging soak (≥30 min before prod)

for i in {1..10}; do
  curl -w "$i: TTFB=%{time_starttransfer}s cf=%{header_cf-cache-status}\n" -so /dev/null \
    "https://staging-resolver.desci.com/api/v2/resolve/dpid/1077"
done
  • Request 1 ≤ 3s (Ceramic cold), requests 2-10 ≤ 200ms.
  • cf-cache-status: HIT after request 1.
  • Resolver pod logs show source: "redis-history-full" for warm hits.

Production verification

for id in 100 250 500 750 1077 1234 1500 1800 2000 999; do
  for i in 1 2; do
    curl -w "$id rep$i: %{time_starttransfer}s cf=%{header_cf-cache-status}\n" -so /dev/null \
      "https://beta.dpid.org/api/v2/resolve/dpid/$id"
  done
done
  • Warm P95 TTFB < 200ms (was 2.8-3.2s).
  • CF HIT rate >70% over a 1-hour sample.

Freshness validation (critical)

  • Publish a new version of a known staging dpid via the editor.
  • Hit staging-resolver.desci.com/api/v2/resolve/dpid/{that_dpid} every 10s.
  • New version visible within 60s (within CACHE_TTL_HISTORY_FULL bound).

Rollback / mitigation

Risk level: medium-high. Touches the cache layer for every resolver consumer (search engines, citation tools, the SSR fallback path, third-party integrations).

Risks and mitigations

Risk Mitigation
Stale data after publish > 60s TTL is the bound; lower CACHE_TTL_HISTORY_FULL env var without redeploy. Set to 1 for instant reads if needed.
Redis serialization bug for HistoryQueryResult Code uses existing redisService.getFromCache<T> JSON path — same as getKeyForCommit cache that's been in prod. If a key fails to parse, getFromCache already calls del(key) to self-heal.
Cloudflare over-caching during incident Roll back via Cloudflare cache purge for beta.dpid.org/api/* (instant) + revert k8s image.
Hot key thundering herd on cold setToCache is fire-and-forget; concurrent requests during a cold window all run loadStream once each. Acceptable for this volume. SWR on the CF layer absorbs most of this.

Rollback procedure

  1. Stop the bleeding fast: kubectl set image deployment/dpid-resolver dpid-resolver=<previous-tag> -n <ns> — ~30s rollout.
  2. Drain stale cache: Redis KEYS resolver-*-history-full-* then DEL (only if the cache itself caused issues; otherwise let TTLs expire naturally over 60s).
  3. CF purge if edge cache is implicated: Cloudflare dashboard → Caching → Purge by URL.

Watch metrics for 1h post-deploy

  • Sentry / pod logs error rate on /api/v2/resolve/* — threshold: >2x baseline.
  • p99 latency on the endpoint — should drop significantly. If it spikes, investigate before celebrating.
  • Cloudflare analytics: cf-cache-status distribution — HIT % climbing toward steady state.

Test plan

  • npm run build clean.
  • npm test — 5 failures match baseline (pre-existing network-dependent tests hitting live Ceramic/registry; verified by stashing changes and re-running).
  • Staging soak (30 min, see above).
  • Prod verification curl loop (see above).
  • Freshness validation (publish + read).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Performance

    • Implemented advanced caching infrastructure for historical data queries, reducing retrieval latency and improving system responsiveness
    • Enhanced DPID resolution performance with edge caching capabilities for faster query responses
  • Code Quality

    • Improved lint-rule compliance and strengthened code quality standards throughout the codebase
    • Refined test suite and optimized validation procedures

hubsmoke and others added 30 commits May 27, 2023 22:01
fix resolution for data files, error when can't find data
no superfluous return data structure
fix dpid resolution for maritime assets
fix resolution bug for non external cids
fix resolution behavior for external datasets
Push last two patches to `main`
promote main dpid resolver
m0ar and others added 7 commits December 15, 2025 16:06
Promote optimised dPID routes
Two optimizations targeting warm-path latency on /api/v2/resolve/dpid/{n}.

Today's warm hit takes ~3s because ceramic.loadStream(streamID) runs
on every request to enumerate the commit log — even though individual
commits are already Redis-cached. Public HTTP responses also have no
Cache-Control header, so Cloudflare doesn't cache them.

Changes (perf):
- Wrap getCodexHistory in a Redis cache for the entire HistoryQueryResult
  (key: resolver-${DPID_ENV}-history-full-${streamId}). On hit, skip
  ceramic.loadStream entirely. Per-commit cache below remains as cold-path
  fallback. Exported getKeyForHistoryFull so future push-invalidation can
  delete it.
- Add CACHE_TTL_HISTORY_FULL config (default 60s, env-tunable). Short by
  design — bounds publish→visibility staleness since the resolver has
  no push-invalidation path today.
- Set Cache-Control: public, s-maxage=60, stale-while-revalidate=300 on
  successful resolveDpidHandler responses. Errors stay uncached.

Drive-by (unblocks pre-commit on this branch):
- eslint-disable comments on 4 pre-existing lint errors in getIpfsFolder.ts
  (1 unused import referenced in commented-out code, 3 deliberate `any`
  types around dynamic IPFS DAG fetches).
- Prettier auto-format on dpids.ts (trailing comma in generic) and
  pagination.spec.ts (string collapse).

Expected impact: warm TTFB drops from 2.8-3.2s to <100ms (Cloudflare HIT)
or <200ms (Redis HIT). Cold-cold unchanged (~3s, Ceramic stream load).
Publish→visibility window: ≤60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR enhances API caching capabilities by introducing full-history Redis caching for history queries, adding Cloudflare edge caching to DPID resolution, and providing supporting configuration and linting updates across multiple API and utility files.

Changes

Caching Enhancement & Infrastructure

Layer / File(s) Summary
Configuration & Constants
src/util/config.ts
Added ONE_MINUTE constant and new exported CACHE_TTL_HISTORY_FULL environment-driven TTL configuration for full-history caching.
History Query Caching
src/api/v2/queries/history.ts
Implemented full-history Redis caching with new exported helper getKeyForHistoryFull(), cache lookup before ceramic/flight queries, and cache storage of resolved results under configurable TTL.
DPID Resolution Edge Caching
src/api/v2/resolvers/dpid.ts
Added Cloudflare cache-control headers (public, s-maxage=60, stale-while-revalidate=300) on successful DPID resolution responses.
Linting & Syntax Cleanup
src/api/v2/data/getIpfsFolder.ts, src/api/v2/queries/dpids.ts
Added ESLint suppressions for no-unused-vars and no-explicit-any around typed-any usages; removed trailing comma in generic type parameter <T> syntax.
Test Updates
test/util/pagination.spec.ts
Simplified assertion in pagination test by condensing multi-line string expectation to single-line format.

Sequence Diagram

sequenceDiagram
    participant Client
    participant APIHandler as API Handler<br/>(history)
    participant Redis
    participant Ceramic
    participant Flight
    participant CloudflareEdge as Cloudflare Edge

    Client->>APIHandler: GET /history/:streamId
    
    APIHandler->>Redis: Check full-history cache<br/>(getKeyForHistoryFull)
    Redis-->>APIHandler: Cache hit or miss
    
    alt Cache Hit
        APIHandler-->>Client: Return cached result
    else Cache Miss
        APIHandler->>Ceramic: Query ceramic history
        Ceramic-->>APIHandler: History data
        
        alt Ceramic Fallback
            APIHandler->>Flight: Query flight client<br/>(backup source)
            Flight-->>APIHandler: Full history result
        end
        
        APIHandler->>Redis: Store result with<br/>CACHE_TTL_HISTORY_FULL
        Redis-->>APIHandler: Cache stored
        
        APIHandler->>CloudflareEdge: Response with<br/>Cache-Control headers
        CloudflareEdge->>Client: Edge-cached response
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • m0ar

Poem

🐰 A rabbit's ode to caching swift,
Redis stores what tiles drift,
Cloudflare edges cache so bright,
History flows, history's light!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main changes: introducing full-history Redis caching for HistoryQueryResult and adding Cloudflare edge cache headers to the resolver.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/cache-history-and-edge

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Microsoft Presidio Analyzer (2.2.362)
src/api/v2/data/getIpfsFolder.ts

Microsoft Presidio Analyzer failed to scan this file


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/api/v2/data/getIpfsFolder.ts (1)

9-10: 💤 Low value

Consider removing the unused isRawCodecCid import instead of suppressing the lint warning.

The isRawCodecCid function is imported but only referenced in commented-out code (lines 422-434). If that code path is intentionally disabled, removing the import would be cleaner than suppressing the lint rule.

Proposed fix
-// eslint-disable-next-line `@typescript-eslint/no-unused-vars`
-import { hackyTsizeIsDir, isRawCodecCid, magicIsUnixFsDir, type PbLink } from "../../../util/ipfs.js";
+import { hackyTsizeIsDir, magicIsUnixFsDir, type PbLink } from "../../../util/ipfs.js";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/v2/data/getIpfsFolder.ts` around lines 9 - 10, Remove the unused
isRawCodecCid import from the import list at the top of getIpfsFolder.ts (the
line currently importing hackyTsizeIsDir, isRawCodecCid, magicIsUnixFsDir,
PbLink); keep the needed imports (hackyTsizeIsDir, magicIsUnixFsDir, PbLink) and
drop isRawCodecCid (and then remove the accompanying eslint-disable-next-line
`@typescript-eslint/no-unused-vars` comment if it only existed to silence this
import). Also verify any commented-out code that references isRawCodecCid
(around the block currently commented on) is either restored or left commented
intentionally; if left commented, ensure no remaining unused import warnings.
src/api/v2/resolvers/dpid.ts (1)

96-99: 💤 Low value

Consider extracting the hardcoded TTL values to maintain consistency with Redis cache TTL.

The s-maxage=60 is hardcoded here while CACHE_TTL_HISTORY_FULL is defined as a configurable constant (defaulting to 60s). If CACHE_TTL_HISTORY_FULL is changed via environment variable, the edge cache would remain at 60s, potentially causing inconsistent staleness behavior between Redis and Cloudflare layers.

Proposed fix to use config constant
+import { CACHE_TTL_HISTORY_FULL } from "../../../util/config.js";
+
 // Cloudflare edge caching — only on success path (errors above remain uncached).
-// s-maxage=60 caps shared-cache freshness; SWR allows up to 5min stale-while-revalidate.
-res.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
+// s-maxage caps shared-cache freshness; SWR allows up to 5min stale-while-revalidate.
+res.set("Cache-Control", `public, s-maxage=${CACHE_TTL_HISTORY_FULL}, stale-while-revalidate=300`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/v2/resolvers/dpid.ts` around lines 96 - 99, The Cache-Control header
in the DPID resolver is using a hardcoded s-maxage=60 which can drift from the
configurable CACHE_TTL_HISTORY_FULL constant; update the response header
construction in src/api/v2/resolvers/dpid.ts (where res.set(...) and return
res.json(result) live) to interpolate/format the numeric value of
CACHE_TTL_HISTORY_FULL instead of the literal 60 (and keep
stale-while-revalidate as-is or derive it consistently if needed) so the
Cloudflare edge TTL matches the Redis cache TTL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/api/v2/data/getIpfsFolder.ts`:
- Around line 9-10: Remove the unused isRawCodecCid import from the import list
at the top of getIpfsFolder.ts (the line currently importing hackyTsizeIsDir,
isRawCodecCid, magicIsUnixFsDir, PbLink); keep the needed imports
(hackyTsizeIsDir, magicIsUnixFsDir, PbLink) and drop isRawCodecCid (and then
remove the accompanying eslint-disable-next-line
`@typescript-eslint/no-unused-vars` comment if it only existed to silence this
import). Also verify any commented-out code that references isRawCodecCid
(around the block currently commented on) is either restored or left commented
intentionally; if left commented, ensure no remaining unused import warnings.

In `@src/api/v2/resolvers/dpid.ts`:
- Around line 96-99: The Cache-Control header in the DPID resolver is using a
hardcoded s-maxage=60 which can drift from the configurable
CACHE_TTL_HISTORY_FULL constant; update the response header construction in
src/api/v2/resolvers/dpid.ts (where res.set(...) and return res.json(result)
live) to interpolate/format the numeric value of CACHE_TTL_HISTORY_FULL instead
of the literal 60 (and keep stale-while-revalidate as-is or derive it
consistently if needed) so the Cloudflare edge TTL matches the Redis cache TTL.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b231525a-9faa-422f-bf2a-149a09b473e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9a52555 and 767133a.

📒 Files selected for processing (6)
  • src/api/v2/data/getIpfsFolder.ts
  • src/api/v2/queries/dpids.ts
  • src/api/v2/queries/history.ts
  • src/api/v2/resolvers/dpid.ts
  • src/util/config.ts
  • test/util/pagination.spec.ts

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.

3 participants