Skip to content

feat(web): move cache refresh crons from GitHub Actions to Vercel Cron - #468

Merged
matheus1lva merged 16 commits into
mainfrom
matheus1lva/cron
Sep 7, 2026
Merged

feat(web): move cache refresh crons from GitHub Actions to Vercel Cron#468
matheus1lva merged 16 commits into
mainfrom
matheus1lva/cron

Conversation

@matheus1lva

@matheus1lva matheus1lva commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Moves the five Redis REST cache refresh jobs from GitHub Actions to Vercel Cron. GitHub's schedule trigger fired the 30-min refresh every 1–3h in practice, so the cache went stale. Each job is now a route under packages/web/app/api/cron/ scheduled from vercel.json, with the refresh logic exported as plain functions and thin *.cli.ts entrypoints kept for local/manual runs.

Also deletes the five workflow files and documents the cutover in packages/web/README.md. main already runs bun --filter web test on PRs via test.yml (added in #442 after this branch started); this PR does not touch that workflow.

How to review

  • packages/web/app/api/cron/handler.ts: bearer auth against CRON_SECRET (timing-safe), runs the job, pushes up/down to Uptime Kuma, 500 on failure. A missing Kuma URL skips the push and the route still returns 200, so the monitor never alerts; a non-2xx push is logged, not raised. 401 does not touch Kuma.
  • packages/web/app/api/db/cron.ts: crons query through their own pool (cronDb, POSTGRES_CRON_POOL_MAX, default 40, sized to the 10 vaults × 4 labels a timeseries batch queries concurrently, 60s acquire timeout). The GraphQL/REST pool stays in packages/web/app/api/db/index.ts and does not construct cronDb.
  • packages/web/app/api/rest/cache.ts: adds an error listener on the shared Redis write client so a dropped connection logs instead of crashing the process. Affects all web Redis writes, not just crons.
  • packages/web/app/api/cron/*/route.ts: one route per job. reports-* are manual-only (no vercel.json entry), same as the workflow_dispatch-only workflows they replace.
  • packages/web/vercel.json: schedules. timeseries-refresh-historical moved from 0 0 to 15 2 UTC to avoid the top-of-hour pile-up with timeseries-refresh.
  • packages/web/app/api/rest/**/refresh*.ts: diff is mostly indentation from wrapping the body in try/finally for elapsed logging; require.main bootstraps moved to *.cli.ts via run-cli.ts.
  • Ignore: .claude/skills/trace-propagation/SKILL.md and the two scripts READMEs, path updates only.

Test plan

  • Manual: set the env vars listed in packages/web/README.md on the Vercel project (Production scope), deploy, then curl -H "Authorization: Bearer $CRON_SECRET" https://<deployment>/api/cron/refresh-cache and check the Uptime Kuma monitor flips to up.
  • Manual: confirm the crons tab in Vercel shows the three scheduled routes and the first */30 run completes within maxDuration.
  • Manual: confirm a timeseries historical run finishes before the 800s kill; duration was not measured against prod vault count.
  • Automated: bun --filter web test (handler, routes, cron-vs-request pool isolation, db helper pool passthrough, refresh jobs passing cronDb, cli 0/1 exit). New specs in this loop were checked by reading, not re-run. tsc --noEmit on packages/web was clean before this loop.

Risk / impact

  • Cutover gap: the old workflows are gone at merge; until CRON_SECRET and the Postgres/Redis vars exist in Vercel every cron returns 401 or fails; missing Kuma vars do not fail the cron, they only silence its heartbeat. Set them before merging.
  • maxDuration: 800 on the historical routes needs Vercel Pro with Fluid Compute; otherwise the cap is 300s and the historical rebuild will be killed. The 300s routes (refresh-cache, timeseries-refresh) will also be killed if a run exceeds 5 min; GHA's default job timeout was 6h. A platform timeout also skips the Kuma down-push, so keep the monitor heartbeat interval tight. Hobby only fires crons daily, so */30 and hourly need Pro.
  • Redis keys and JSON shapes are unchanged. gh search code over yearn/ for rest:list:vaults, refresh-vaults.ts, and UPTIME_KUMA_PUSH_URL_REFRESH_VAULTS only hits this repo (REST routes, ingest container specs, two script READMEs). CLI path updates are in those ingest specs. No other yearn consumer of the GHA workflows.
  • Observable besides the scheduler move: refresh logs elapsed ms on success and failure; idle Redis write errors log instead of crashing the process (all REST cache writes).
  • Pools are per function instance on Vercel. POSTGRES_POOL_MAX (web) plus POSTGRES_CRON_POOL_MAX (crons, default 40) must fit the Postgres connection limit across concurrent instances. Size before enabling. Cron instances still load the request pool module (db helpers default to it) but do not query it.
  • Redis idle-disconnect handler is not unit-tested (needs a live Redis drop).
  • Rollback: git revert restores the workflows; nothing in the data path changed.

matheus1lva and others added 6 commits August 3, 2026 14:24
Moves each 'if (require.main === module)' bootstrap out of the refresh
modules and into a '*.cli.ts' sibling, so the modules stay importable
without a module-scope process.exit(). Exports the refresh functions and
repoints the ingest container specs at refresh-vaults.cli.ts.
Replaces the five scheduled GitHub Actions workflows with authenticated
App Router routes under app/api/cron/, scheduled by vercel.json. The
shared handler gates on CRON_SECRET with timingSafeEqual and pushes
up/down heartbeats to Uptime Kuma.

The historical timeseries rebuild does not fit in one invocation: GitHub
allowed 360 minutes, Vercel caps at 800s. It now runs hourly over 1/24 of
the vaults per tick, hashed by chainId:address so a vault keeps its shard
as the list grows, giving each vault a full rebuild once a day.

The reports jobs were workflow_dispatch-only, so their routes carry no
cron entry and are triggered by an authenticated request.

Also adds an error listener to the rest cache redis client, which now
holds an open socket between invocations instead of dying with the CI
process.
Adds a test job running 'bun --filter web test'. Documents the cron
routes, their env contract, and manual invocation in the web README and
.env.local.example, and repoints the trace-propagation skill at the cron
route now that refresh-cache.yml is gone.
Full historical rebuild fits Vercel's 800s (223-422s on Actions), so
timeseries-refresh-historical runs a daily full pass instead of hourly
1/24 shards. CLI bootstraps share runCli(). Refresh modules log elapsed
ms instead of console.time. SHARD_TOTAL route export removed (broke
next build). Stale runbook paths, env example, and README env scoping
fixed.
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
kong Ready Ready Preview Sep 4, 2026 5:30pm UTC

Request Review

@matheus1lva matheus1lva left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Bearer auth is timing-safe with length check, Kuma push is best-effort with a timeout, and the 500-on-failure path is covered by handler.spec.ts. No defects found.


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 1 candidate finding was refuted and dropped.

@murderteeth
murderteeth self-requested a review September 3, 2026 18:06

@murderteeth murderteeth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Moves the five Redis REST cache refresh jobs from GitHub Actions to Vercel Cron routes under packages/web/app/api/cron/, with a shared bearer-auth handler, Uptime Kuma heartbeats, thin CLI entrypoints for local runs, a new CI test job, and cutover docs. The auth path is timing-safe, Kuma pushes are best-effort with a timeout, and the failure path is covered by tests.

Issues

  • packages/web/app/api/cron/timeseries-refresh-historical/route.ts:8 - Cron work shares the web DB pool (high) — the daily historical rebuild now runs inside the web function and drives 40 concurrent queries per batch through the same module-level pg.Pool (max 4, 5-second acquire timeout) that serves live GraphQL and REST requests. Under GitHub Actions the job had its own process and its own pool. For the minutes it runs, every web query on that instance queues behind a cron batch, and any wait past 5 seconds fails the user-facing request. The same overlap applies at the top of each hour when refresh-cache and timeseries-refresh fire together.
    • Done when: cron DB work runs on a pool separate from the one the GraphQL and REST paths use — a second Pool exported from app/api/db/index.ts with the same connection settings but its own max (e.g. from a POSTGRES_CRON_POOL_MAX variable) and acquire timeout, passed into the query helpers in rest/timeseries/db.ts, rest/reports/db.ts, and rest/list/db.ts by the five refresh jobs while live callers keep the default — sized so 40 in-flight queries do not trip the acquire timeout, and concurrent web requests complete without connection-timeout errors while the historical cron runs.
    • Provenance: 6aab748
  • packages/web/app/api/cron/handler.spec.ts:29 - Secret comparison is untested (low) — the only wrong-token test uses a token shorter than the secret, so it is rejected by the length pre-check and the actual byte comparison never runs; the whole suite would still pass if that comparison were replaced with an unconditional true.
    • Done when: the handler suite fails when the secret comparison is replaced with an unconditional true, i.e. a same-length, non-matching bearer token is asserted to yield 401 and not invoke the job.
    • Provenance: f3759ea

Verdict

REQUEST_CHANGES


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 0 candidate findings were refuted and dropped.


GraphQL/REST imported app/api/db and constructed the cron pool too.
Move it to db/cron.ts so request handlers never allocate it.
Keep cron pool injection and main's mergeSnapshot + name fallback.
Stale-hook PPS suite still ran refresh-vaults.ts, which no longer bootstraps.

@murderteeth murderteeth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Moves the five Redis REST cache refresh jobs from GitHub Actions to Vercel Cron routes under packages/web/app/api/cron/, with a shared timing-safe bearer handler, Uptime Kuma heartbeats, a dedicated cron Postgres pool threaded through the db helpers, thin CLI entrypoints, and cutover docs. Both items from the prior review are resolved: cron queries no longer touch the request pool, and a same-length wrong-token case now exercises the secret comparison.

Issues

  • packages/web/app/api/db/cron.spec.ts:15 - Pool config tests grep the source file instead of testing the pool (medium) — the two pool-default tests do not touch a pool. Each one calls readFileSync on cron.ts or index.ts, gets the TypeScript back as a string, and runs assert.match against it for POSTGRES_CRON_POOL_MAX ?? '40' and connectionTimeoutMillis: 60_000. That is a grep. It passes if the text is anywhere in the file, including a comment or dead code, and it fails on a quote change or on moving the default into config.ts while the pool is configured identically. The cronDb object under test is already imported at the top of the same spec and never used by these two tests. pg exposes the resolved config as cronDb.options.max and cronDb.options.connectionTimeoutMillis, so the real assertion is one line each.
    • Done when: readFileSync is gone from the spec; the tests assert cronDb.options and db.options directly, fail when either pool has an effective max or connectionTimeoutMillis other than the documented values, and pass unchanged after a behavior-preserving rewrite of the two new Pool({...}) calls.
    • Provenance: 306874f
  • packages/web/vercel.json:1 - 800s maxDuration depends on a dashboard-only setting (medium)timeseries-refresh-historical and reports-refresh-historical export maxDuration = 800, and the README documents that cap. 800s is only valid with Fluid compute on; without it the Pro limit is 300s and the daily historical rebuild is killed mid-run. Fluid is currently enabled in the Vercel dashboard, but nothing in the repo says so, so a project recreate, a settings change, or a second deployment of this package silently turns the cap into a 504 at minute five.
    • Done when: packages/web/vercel.json sets "fluid": true so the setting is code, and the README cron section states that the 800s routes require it.
    • Provenance: f3759ea
  • packages/web/app/api/rest/cache.ts:6 - Redis error listener is unpinned (low) — the listener that stops an idle Redis drop from crashing a cron invocation or web instance has no test, unlike the matching pg pool listener asserted in cron.spec.ts, so a later refactor of cache.ts can drop it and nothing goes red. Registration is assertable without a live Redis.
    • Done when: a web spec fails if the error listener on the REST cache write client is removed, and passes with no Redis server running.
    • Provenance: f3759ea

Verdict

REQUEST_CHANGES


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 1 candidate finding was refuted and dropped.

@murderteeth murderteeth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary

Third iteration of the GitHub Actions to Vercel Cron cutover. All three items from the prior review are resolved: pool config tests assert the live pool options, vercel.json pins Fluid compute, and the Redis error listener is covered. Bearer auth is timing-safe, Kuma pushes are best-effort, and CI plus the Vercel preview deploy are green on the head commit.

Issues

None.

Verdict

APPROVE


How This Was Reviewed

Reviewed with the review-pr-workflow skill
5 review lenses, each finding independently verified by claude. 1 candidate finding was refuted and dropped.

@matheus1lva
matheus1lva merged commit 3b80064 into main Sep 7, 2026
7 checks passed
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.

2 participants