Skip to content

feat(ops): add /readyz readiness probe - #763

Merged
phoenix-server merged 2 commits into
mainfrom
feat/readyz-endpoint
Sep 8, 2026
Merged

feat(ops): add /readyz readiness probe #763
phoenix-server merged 2 commits into
mainfrom
feat/readyz-endpoint

Conversation

@Ferryx349

Copy link
Copy Markdown
Collaborator

Description

Summary

This PR adds public readiness endpoint for HAProxy blue/green cutover. /healthz stays
liveness-only;

Related Issue

Closes:- #762

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my code changes.
  • I added a changeset, or this is docs-only and I added an empty changeset.
  • All new and existing tests passed.

Public readiness endpoint for HAProxy blue/green cutover. /healthz stays
liveness-only; /readyz returns 503 when Postgres or Redis is unavailable.
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 133184b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls

coveralls commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 71.936% (+0.3%) from 71.671% — feat/readyz-endpoint into main

@phoenix-server

Copy link
Copy Markdown
Collaborator

Verdict: approve/readyz does real readiness (knex SELECT 1 on the master pool + Redis PING with a configurable 3s timeout), returns correct 200/503 JSON that leaks only {ok} booleans, and registration, handler conventions, tests, integration coverage, and deploy docs are consistent and accurate against #762's liveness-vs-readiness goal. Recommendations below are non-blocking.

Important

1. Unauthenticated /readyz does per-request Postgres and Redis I/O with no cache or rate limitsrc/handlers/request-handlers/get-readyz-request-handler.ts:23-32

Every hit calls collectAdminHealthSnapshot(): a SELECT 1 on the shared master knex pool plus a Redis PING (with a client.connect() retry while Redis is down), each held up to 3s (ADMIN_DEPENDENCY_PING_TIMEOUT_MS, src/utils/admin-health.ts:8). Two consequences:

  1. This is the first unauthenticated route on the public relay port that does backend I/O per request — /, /terms, /privacy and nodeinfo serve settings/static content only, and the work-performing public routes (/invoices, /admissions, /callbacks, /admin/*) are all behind rateLimiterMiddleware (src/routes/index.ts:36-38). A cheap flood translates directly into master-pool churn (prod ships 16-64 pool connections shared with event persistence, deploy/env.example:20-21).
  2. When a dependency is down, withTimeout's Promise.race returns {ok:false} after 3s but the underlying knex acquire (60s acquire timeout) and Redis connect attempts keep running — sustained probing piles up pending acquires exactly while the DB is struggling.

Suggested mitigation: cache the snapshot in-process for ~1s (absorbs HAProxy's ~2s polling and floods with ≤1s staleness — fine for cutover), or make an explicit rate-limit/annotation decision using the repo's // codeql[js/missing-rate-limiting] justification convention (src/routes/index.ts:17-18,26). A Redis-backed rate limiter on this route is NOT recommended — it would entangle the probe with the dependency it checks.

2. Changeset bumps patch, but repo precedent for feat: PRs is minor.changeset/readyz-endpoint.md:1-3

Every recent feat: change used minor (#738, #737, #734, #716). A new public endpoint is a feature per semver:

"nostream": minor

3. Deploy docs don't reconcile the 3s worst-case probe latency with LB check timeoutsdeploy/README.md:104-117

The probe can take up to ~3s when a dependency is slow; HAProxy timeout check falls back to timeout connect when unset. A check timeout tighter than ~3.5s would flap the backend exactly during dependency slowness and spuriously reject cutovers — the failure mode this endpoint exists to prevent.

`/readyz` pings Postgres and Redis with a configurable timeout (default 3s,
`ADMIN_DEPENDENCY_PING_TIMEOUT_MS`); set your load balancer check timeout above
that (e.g. HAProxy `timeout check 5s`) so dependency slowness doesn't flap the probe.

Minor / Nit

4. Cache-Control: no-store on probe responsesget-readyz-request-handler.ts:29 (and the catch path). HAProxy hits it on 127.0.0.1 today, but a caching layer serving a stale 200 during an outage would silently defeat the probe. Belt-and-braces one-liner in both response paths.

5. Import orderingsrc/routes/index.ts:7-10: get-readyz is inserted between get-health and get-privacy; alphabetically it belongs between privacy and terms. Purely stylistic consistency with the file's convention.

6. Handler spec lacks a database-only failure caseget-readyz-request-handler.spec.ts:74-83: redis-down → 503 and throwing-snapshot are covered, but DB-down-only is asserted only against buildReadyzSnapshot. One more stub resolving {database:{ok:false}, redis:{ok:true}} expecting 503 would pin the status mapping for either dependency failing independently.

@phoenix-server

Copy link
Copy Markdown
Collaborator

Re-review after the fix-up push (133184b): APPROVE. All 6 findings from my earlier review are addressed:

  1. FIXED — per-request dependency I/O on the unauthenticated route: now a ~1s in-process snapshot cache with single-flight dedup (get-readyz-request-handler.ts:7,38-64 — concurrent misses coalesce onto one in-flight probe, cleared in finally, failed collections never cached), plus the repo's // codeql[js/missing-rate-limiting] annotation at src/routes/index.ts:32. Worst case is exactly one outstanding probe per process — no pool churn, no pending-acquire pile-up during outages, and 503 still propagates from a cached unavailable snapshot.
  2. FIXED — changeset is minor now.
  3. FIXED — deploy docs call out the 3s default ping timeout and recommend timeout check 5s.
  4. FIXEDCache-Control: no-store on both response paths, asserted in tests.
  5. FIXED — import ordering restored.
  6. FIXED — redis-only and db-only 503 cases now tested independently (5→7 tests, nothing dropped).

No blocking issues introduced. One non-blocking note for later: the PR's spec pins the cache-hit path but not TTL expiry/single-flight; I verified those behaviors are currently correct (TTL-expiry re-collect, concurrent single-flight, cached-503 all pass in a throwaway spec), so it's a coverage-only follow-up.

Merging. Thanks @Ferryx349 — clean fix-up, and the single-flight cache design is exactly the right shape for this.

@phoenix-server phoenix-server left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve — all findings addressed, re-review clean.

@phoenix-server phoenix-server left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve - all findings addressed, re-review clean.

@phoenix-server
phoenix-server merged commit 358763a into main Sep 8, 2026
21 checks passed
@phoenix-server
phoenix-server deleted the feat/readyz-endpoint branch September 8, 2026 14:25
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