Make relay readiness process-local - #7341
TheSentinel454 wants to merge 18 commits into
Conversation
🔐 Codex Security Review
|
A reconnect burst exhausted the per-pod writer pools and two feedback
loops turned that into a total outage.
Readiness evaluated shared Postgres, Redis, and deletion-catalog health,
so every replica went NotReady together and the burst had nowhere to
land. The probe was also part of the load: the deletion-catalog check
acquires the writer pool, so each pod spent writer connections against
the exhausted pool every five seconds while failing. /_readiness now
answers from local process lifecycle only — shutting_down is 503,
anything else is 200 — and the dependency evaluation moves to /_status
on the same private health listener, under a `dependencies` object
carrying the fields the readiness body used to return. No startup state
is added: the health listener binds only after the database,
migrations, Redis, and pub/sub are up, so a process that can answer has
booted.
run_registered_community_connection collapsed Ok(false) and Err into
"not active", so a writer-pool timeout in is_community_active read as
confirmed archival and dropped the socket, which reconnected and
re-checked. Only a confirmed Ok(false) cancels now; a lookup failure
admits the socket with a structured warning and defers to the periodic
revalidate_live_communities backstop. Writes are unaffected and remain
fail-closed on their own per-event fence.
Telemetry keeps its existing names: buzz_readiness_checks_total narrows
to {ready, shutting_down}, the dependency families are now sampled by
/_status, dependency gauges are dropped, and one new bounded counter,
buzz_community_admission_checks_total{outcome}, counts the admission
decision. The per-pod raw-series ceiling drops from 99 to 86.
This deletes the readiness publication machinery — the mutex, probe
generations, ProbeTicket/ProbeStart, finish_probe,
finish_public_evaluation, and a second shutdown flag duplicating
AppState::shutting_down. All of it existed to order concurrent async
dependency evaluations against shutdown. Readiness is now a single
atomic load, so the one ordering guarantee still worth keeping — a
racing shutdown must win, and never leave a draining pod advertising a
ready gauge — is a post-write re-read in record_readiness_probe rather
than a generation-fenced mutex.
Co-authored-by: Claude Code <noreply@anthropic.com>
Redis had no startup gate at all. `deadpool_redis` pools dial lazily and
PubSubManager::new only allocates channels, so "Redis pub/sub connected"
was logged against a dead port and boot ran to completion. With readiness
now answering from local lifecycle alone, such a pod bound its health
listener and advertised ready for the rest of its life. state::
verify_redis_command_path acquires one connection from the command pool
and issues PING before AppState is built, and therefore before the health
listener binds, because binding is the one-way latch that makes a pod
routable. No startup_ready flag is added for the same reason. Post-start
Redis failures are unchanged: they are dependency failures and never move
readiness. Postgres startup connection behavior is untouched.
Signed-off-by: tornquist <tornquist@squareup.com>
25bc2c2 to
a8e2c48
Compare
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 Process-local readiness is the correct architecture, but the bundled fail-open community admission change creates an archived-history read window. Dependency observability also needs an automatic sampling/freshness owner before rollout. The diagnostic endpoint should be bounded before it becomes the incident-time interface.
Socket admission treated a failed `is_community_active` lookup as grounds
to admit, deferring eviction to the periodic revalidator. That begins
serving AUTH and REQ frames for a tenant whose lifecycle is unknown, which
`docs/multi-tenant-relay.md` I5 (`Inv_AdmissionFence`) does not permit: read
and membership capability belong only to an actor currently admitted to that
community. The adjacent host-binding seam already refuses on exactly this
evidence.
Both non-affirmative outcomes now cancel before any frame is read. The
`buzz_community_admission_checks_total{outcome}` counter still separates
`inactive` from `check_error`, so an operator can tell archival from database
pressure without the admission decision depending on that distinction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
`/_status` evaluated Postgres, Redis, and the deletion catalog on every request. That made the load a pressured dependency sees a function of how often someone looked at the endpoint, with nothing bounding how many evaluations could be in flight, and it left the dependency metrics flat whenever nobody was looking — during exactly the outage they exist to explain. `DependencyDiagnostics` now owns one fixed-cadence loop (`run_dependency_sampler`, 30s) that is the sole caller of the evaluator. It awaits each evaluation before taking the next tick, so a pod never holds more than one open; a dependency slower than the cadence lowers the sampling rate instead of stacking probes on the slowness that caused it. Each cycle republishes the existing dependency counters and histograms and caches the report with its observation time. `/_status` now only reads that cache and performs no dependency I/O, so it can be polled freely. Its `dependencies` object always carries a `sample` field — `not_yet_sampled`, `fresh`, or `stale` — plus the report's age, so a cached verdict can never be mistaken for a current one. Freshness is also observable from a scrape alone via a new `buzz_readiness_dependency_sample_age_seconds` gauge, following the existing `buzz_storage_sweep_age_seconds` convention: absent until the first report exists, so absence means "not yet sampled" rather than "fresh". The per-pod series ceiling moves from 86 to 87. `/_readiness` is untouched: still process-local lifecycle, still one sample per probe, same response and telemetry contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Freshness does not need a series of its own. The dependency outcome and duration families stop receiving samples the moment the per-pod sampler stops, and the Datadog monitors alert on that no-data gap — so an age gauge would only add a series that the very loop whose absence it is meant to report has to keep advancing. A wedged sampler would freeze it at its last value and read as permanently fresh. Per-report freshness stays where a human reads it: `/_status` keeps its `sample`, `sample_age_seconds`, and `sample_interval_seconds` fields and still distinguishes not-yet-sampled from fresh from stale. The sampler cadence, single in-flight evaluation, and cached-read `/_status` are unchanged. Drops the readiness series ceiling from 87 to 86. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Startup is the only owner of dependency evaluation: `/_status` reads the cache, a readiness probe records no dependency attempt, and no request path may start a check. So if `main` stops spawning the sampler, the pod evaluates Postgres, Redis, and the deletion catalog exactly never — the dependency families stay absent from its scrape and `/_status` answers `not_yet_sampled` for the pod's whole life. No in-process test can fail on that, because every one of them drives `sample` itself. Adds one case to the existing real-binary boot harness: boot the relay against the PostgreSQL lane's Postgres and Redis, then read only the relay's own `/metrics` — no probe, no `/_status`, nothing that could evaluate a dependency on the test's behalf. Verified falsifiable by deleting the spawn from `main` and watching it fail. `wait_for_relay_metrics` is generalized to poll for a named metric family so the wait stays bounded by the harness deadline rather than a sleep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
The dependency outcome counters and latency histograms are cumulative, so a stopped sampler leaves their last values being scraped indefinitely: the series stay present and flat, and no-data never fires. Freshness needs a series of its own, and it has to be a timestamp rather than an age — a gauge carrying the age would have to be advanced by the very loop whose absence it is meant to report, so a wedged sampler would freeze it at its last value and read as permanently fresh. `buzz_readiness_dependency_sample_completed_timestamp_seconds` carries the Unix time the cached report completed. `sample` writes it last, once the cache already serves that report, and nothing else writes it — the age is computed by the query (`time() - <gauge>`), with no server-side aging loop. Absent until the first sample completes, so absence means "not yet sampled" rather than "fresh". `/_status` keeps its own cached `sample_age_seconds` and fresh/stale verdict unchanged. The chart README documents the query, the `absent()` case, and why no-data on the counters and histograms does not detect a stopped sampler. Readiness series ceiling 86 -> 87. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
The chart README prescribed a PromQL recipe (`time() - <gauge> > 60`) and `absent()`/no-data alerting for the new completion-timestamp gauge. Those are provider-specific and were never validated against the deployment's actual monitoring provider, where neither `time()` nor `absent()` exists in the metric-monitor query grammar and no-data semantics differ. Documenting them as the alerting contract would have sent an operator down a path that does not work. Keeps the factual server-side contract: the gauge is the Unix time the cached report completed, written once per completed sample right after the cache is replaced, by nothing else; it stands still when sampling stops, so elapsed time is whatever the reader computes; and it is not emitted until the first sample completes. Monitor query, thresholds, and per-pod tag grouping belong with the deployment's monitor configuration, which this repo does not own. Keeps the accurate note that the dependency counters and latency histogram are cumulative, so a stopped sampler leaves their series present and flat. Documentation only. No Rust or test changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
…ness-overload * origin/main: (74 commits) feat(relay): add admin HTTP routes for member restriction management (#7302) fix(relay): fire kick live side effects at convergence; persist target; fence re-add race with held lock (#7298) feat(relay): add atomic complete read-state snapshots (#7572) fix(desktop): register macOS badges for new and existing installs (#7783) fix(mobile): avoid opening empty threads on message tap (#7756) fix(workflows): make deletion persistent and retryable (#7735) fix(mobile): preserve thread replies through refresh failures (#7757) fix(mobile): keep iOS message menu actions responsive after rebuilds (#7758) fix(relay): exclude ephemeral activity from message quota (#7736) release: push gateway chart 0.3.1 (#7749) fix(push): label plaintext push gateway service as HTTP (#7717) Replace personal and internal data in desktop test fixtures (#7748) Add mobile VISION (#7710) fix(mobile): keep relay sessions stable during push lease updates (#7745) fix(desktop): keep managed agent avatars usable across communities (#7732) fix(mobile): fail open when age checks are unavailable (#7714) fix(ci): don't run desktop tests for purely mobile client changes (#7709) fix(mobile): temporarily disable age gating (#7708) feat(db): expose connection setup metrics (#7286) Isolate S3 storage metrics from the relay (#7543) ... Signed-off-by: tornquist <tornquist@squareup.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: two P2 findings below. The earlier fail-open admission, missing sampler, and request-driven diagnostic concerns are addressed at this head; process-local readiness, the one-time Redis boot gate, and probe-owned readiness telemetry are preserved.
Validation: source-only review of HEAD 3fbb54d876959e4fa45a56d62f9811e58b8b9007 against BASE 77729abfb692b25a0f4ec4a69add86af2e32c0dd, with independent admission, metrics, and regression-selection lanes. Existing CI run 35653008996 passed on a merge tree identical to HEAD, including all three new real-process boot tests. The new unit witnesses were not executed by the configured lanes. No PR code was executed for this review.
Merge criteria: resolve the completion-gauge retention mismatch and select the new contract regressions in the existing test lanes. Provider-owned alert rules are outside this review.
…cleanup Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Amp <amp@ampcode.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 I think the counter→gauge switch in 962bdca closes the OpenMetrics problem from 4099060960. Datadog's v2 gauge transformer forwards sample.value as-is, so the full epoch reaches the provider again. The republish cadence also stays under every supported idle timeout: 5s at the 15s floor, capped at 30s. There's still one blocking gap in how the new retention path is tested, plus a smaller ordering issue.
Blocking: nothing guards the production publisher wiring. scrape_retains_the_completion_timestamp_across_gauge_idle_timeout spawns run_dependency_sample_completion_publisher_for_diagnostics itself (readiness.rs#L1200-L1210). startup_owns_the_dependency_sampler waits for the first sampler-written timestamp and then terminates the process (boot_lifecycle.rs#L816-L820). If someone deletes the publisher spawn in main.rs#L1070-L1083, both tests still pass, and production silently goes back to losing the gauge once the sampler stalls past the idle timeout. That's the exact failure this delta exists to prevent.
The fix I'd suggest is to move the "start sampler + start publisher" pair behind one function that main calls. Then add a test for that seam: stop only the sampler, keep publishing, and assert the scrape still returns the same full epoch after a short idle timeout. It needs to fail when the publisher start is removed. A boot test with a small BUZZ_USAGE_METRICS_IDLE_TIMEOUT_SECS would also work, but it's slower.
Minor: a stale republish can move the epoch backwards. The sampler and the publisher both call gauge set, but nothing orders those writes (readiness.rs#L519-L538). On the multi-threaded runtime this interleaving can happen:
- The publisher loads A.
- The sampler stores B and publishes it.
- The publisher then publishes A.
The exported timestamp goes backwards until the next publish. It's bounded to one republish interval, so it's unlikely to trip a 60s staleness monitor, but it can hand a scrape an inflated age. You could hold a small mutex across the load+set and the store+set, or have the publisher only ever re-set the value it reads while holding that lock. The current-thread retention test can't exercise this, so a targeted test would be nice if you touch it.
Also, the Datadog model in datadog_openmetrics_v2_completion_epoch is handwritten. It pins the local metric type, which is useful, but it isn't the real integration, so the doc comment probably shouldn't imply more than that. Head CI was still running when I looked.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: one P2 ordering defect in the completion-gauge repair, detailed inline. The prior unit-selector omission is fixed; raw gauge semantics and independent idle-retention publication are now present. Process-local readiness, cached status, the initial Redis boot gate, and fail-closed admission remain intact.
Merge criterion: serialize completion advancement and republishing, with a deterministic regression that prevents an old republish from overwriting a newer completion. No provider-alert policy change is requested.
Validation: source-only follow-up from 3fbb54d876959e4fa45a56d62f9811e58b8b9007 to HEAD 962bdca29e268d0d1de5a6560b2cd0806c3c37dd, against BASE 77729abfb692b25a0f4ec4a69add86af2e32c0dd, with independent production and coverage lanes. Completed CI run36071969683 passed; its unit job ran 121 relay tests including the new witnesses on synthetic merge 21d09871bfbe54d7b2ca7cef5599eaea70fd0df2, not the raw pinned head. No PR code was executed for this review.
Remaining validation limits: the retention unit test exercises the real publisher/exporter but bypasses main’s publisher spawn, which is present in source; Datadog transformation is modeled, not integration-tested. These are disclosed gaps, not additional demonstrated production failures.
The completion-epoch gauge has two writers: the dependency sampler, which owns the epoch, and the idle-refresh publisher, which may only re-emit it. The publisher read the stored epoch and wrote it as two separate steps, so a sample completing in between left the older epoch as the last write — the exported series moved backwards for up to one publisher interval, which is exactly the window freshness alerting reads. The metrics facade offers a bare `set` with no compare-and-set, so the ordering has to come from the writers. Serializing them under one lock would put the sampler's authoritative write behind an idle refresh; instead the republisher now verifies after its write that the epoch it wrote is still the stored one, and re-emits the newer one if a completion landed while that write was in flight. Another pass costs another completed sample, so it ends as soon as nothing is racing it. The regression parks a republish inside the recorder — the last point of its write, after it has read the epoch — completes a newer sample behind it, then releases it and asserts the scrape still reports the newer completion. Against the unfixed publisher it reports the older epoch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: tornquist <tornquist@squareup.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 I think ed3d7ae fixes the race from my last review. The production wiring gap I flagged as blocking is still open, though, so I'm keeping this at changes requested.
Race: fixed. The verify-then-retry loop in republish_dependency_sample_completion holds up against the recorder you actually ship. With the locked metrics 0.24.6 / metrics-util 0.20.4 / metrics-exporter-prometheus 0.18.3, gauge set is a swap(.., AcqRel) on the generational AtomicU64, not a plain store. If a stale republish of A overwrites the sampler's B, that swap synchronizes with the sampler's release. The sampler stores the epoch before it publishes, so the republisher's next load has to see B, and the loop re-emits it. It only goes around again when the epoch actually changed, so it can't spin on a stable value, and it never blocks the sampler. The parked-recorder test also discriminates. Swapping the loop back to the 962bdca29 single publish makes it fail on exactly the intended assertion (1700000000 instead of 1700000030), and it passes at this head.
Blocking: production publisher startup is still unguarded. main.rs and boot_lifecycle.rs haven't changed since 962bdca29. We deleted only the completion-publisher spawn at main.rs#L1070-L1084 and ran the tests at this head. Every readiness unit test (13/13) and every boot lifecycle test still passed: 9/9 without infra and 3/3 against Postgres, including startup_owns_the_dependency_sampler. That boot test proves the sampler writes the first timestamp, which it does on its own. scrape_retains_the_completion_timestamp_across_gauge_idle_timeout spawns the private publisher helper itself (readiness.rs#L1203-L1239), and the new race test calls the republish method directly. So if someone later drops that spawn, CI stays green, and production goes back to losing the gauge exactly when sampling stalls. That's the case this whole delta exists for.
The small fix I'd still suggest: put "start sampler + start publisher" behind one function that main calls, then point the retention test at that function. The test should stop only the sampler, idle past the gauge timeout, and assert the scrape still returns the same epoch, and it has to go red when the publisher start is removed. If you'd rather do it as a real boot test, note that the idle timeout is floored at three usage-poller intervals. Setting only BUZZ_USAGE_METRICS_IDLE_TIMEOUT_SECS=15 won't get under the default 900s floor, so you'd also need BUZZ_USAGE_METRICS_INTERVAL_SECS=5.
Minor: the README now overstates the guarantee. deploy/charts/buzz/README.md#L190-L192 says the exported value never regresses. The loop repairs a stale overwrite after it lands, so a scrape that hits the window between writing A and re-writing B can still read A, and preemption can stretch that window. The code comment gets this right ("instead of leaving the exported series moved backwards"). I'd narrow the README to match: a racing republish fixes its own stale write before it returns, rather than leaving it until the next tick.
The one failure in our full buzz-relay run was api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo (504), which matches the environment baseline you described and has nothing to do with this delta. Head CI was still running when I looked.
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blocking code findings in this follow-up. The previous P2 ordering defect is addressed: the republisher rechecks after its write and repairs a raced completion before returning, rather than leaving the older epoch until the next tick. I accept this verify/retry approach instead of requiring lock serialization. The deterministic parked-recorder regression calls the production methods and would catch the previous single-write implementation.
Process-local readiness, cached status, the initial Redis boot gate, and fail-closed admission remain intact. One nonblocking documentation clarification is inline.
Validation: source-only review of HEAD ed3d7ae7bffb6b86395eb2c825da925991ae9a3b against BASE 77729abfb692b25a0f4ec4a69add86af2e32c0dd, focused on the delta from 962bdca29e268d0d1de5a6560b2cd0806c3c37dd, with independent source and CI-metadata lanes. No PR code was executed. At the 14:35 UTC snapshot, relevant unit and relay integration CI was unfinished; run 36147376862 checks synthetic merge 5c5e439099c21c624b32d306e481966473ac6d83, not the raw head. This is not a green-CI or merge-readiness claim.
Unchanged validation limits remain: production publisher startup wiring is present but not bound by a dedicated integration regression, and Datadog transformation is modeled rather than integration-tested. Their previous nonblocking disposition stands; this review does not resolve other reviewers’ outstanding requests.
Gimli E2E verdict — exercised and worksExact head: I exercised the relay →
The deterministic concurrency arm was local to the detached validation worktree and restored byte-for-byte before the clean release-binary probes; final worktree was clean at the tested SHA. I tore down the named Compose project and owned tmux sessions. Artifact — live raw Prometheus gauge, official submitted gauge point, and recorded race sequence: Detailed receipts: |
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Amp <amp@ampcode.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blocking findings in this follow-up. At HEAD 3de2266efed39930bf74e0c831d8cfbcfc3d4ccb (BASE 77729abfb692b25a0f4ec4a69add86af2e32c0dd), the one-commit delta from ed3d7ae7 narrows the startup-coverage gap and addresses the README clarification:
- Main and the retention regression now call the same sampler/publisher startup function. The test waits for a completed sample, cancels only the sampler, and checks that the independent publisher retains the same full epoch beyond the gauge idle timeout. The existing boot witness covers starting the sampler from main; the shared-seam regression now covers keeping its publisher wired alongside it.
- The README correctly permits a briefly stale scrape while the existing verify/retry path repairs a raced write before returning. The prior ordering repair and process-local readiness, cached diagnostics, initial Redis boot gate, and fail-closed admission contracts remain intact.
Validation: source-only review, with independent source and CI-metadata lanes; no PR code or mutation tests executed. At the captured CI snapshot, Rust unit coverage was still running and relay/Postgres integration had not reported. Run36157462166 tests synthetic merge d7e7d634e3a1bfc0dcc2bdb1ccdd13877af7451d (main 930b8bb… + this HEAD), not the pinned base/head pair. This is not a green-CI or merge-readiness claim. Datadog transformation remains modeled, not integration-tested; its nonblocking disposition is unchanged. This comment does not dismiss standing change requests from other reviews.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Re-review at 3de2266e (base 77729abf). Both findings from my earlier review are fixed and I found no new blockers. Two small non-blocking notes are below.
The startup-wiring blocker is fixed. main.rs now starts both tasks through start_dependency_sampler_and_completion_publisher, and the retention test starts them through that same function. We checked the fix by deliberately breaking the code at each point and confirming that a test fails:
| Change we made | Result |
|---|---|
| Clean head | readiness 13/13, boot lifecycle 12/12 (9 ordinary + 3 Postgres) |
Remove only the publisher tokio::spawn inside the seam |
caught: dependency_runtime_retains_the_completion_timestamp_when_only_sampler_stops gets None after idle timeout |
Remove only the seam call in main.rs |
caught: Postgres startup_owns_the_dependency_sampler never sees the completion metric |
Return Some(0) before the first write |
caught only by the Postgres boot test (boot_lifecycle.rs:827, 4/4 runs); every unit test passes |
| Head restored | everything passes again |
The README note is fixed. The text no longer says "never regresses". It now says one scrape can briefly see the older epoch and that the republish repairs it before returning, which matches how the code behaves.
Non-blocking:
- Duplicate entry points.
run_dependency_sample_completion_publisherhas no callers now, andrun_dependency_sampleris only called by the router test. The new seam copies their argument handling instead of calling them, and the doc comment onDependencyDiagnosticsstill saysrun_dependency_sampleris the only caller ofsample. I think having the seam spawn the two existingpubrunners, and deleting whichever one ends up unused, would leave a single path and keep the comment accurate. - The "no gauge before the first completion" rule has no direct test. The last row of the table is only caught because the Postgres boot test happens to scrape at the right moment; no unit test covers it. The assertion that used to cover this in the old retention test never actually worked, so nothing was lost, but a direct test would help. One way: hold the evaluator behind a barrier, start through the seam, let a few publisher ticks run, assert the raw series is absent, then release the barrier.
One unrelated note: the full buzz-relay package run has one failure, api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo (504 instead of 200). It also fails on the earlier head ed3d7ae7, so it isn't caused by this PR.
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>
|
Addressed the two nonblocking follow-ups at exact head
The zero-before-completion mutation fails this regression with All inline review threads are currently resolved; there was no new inline thread to resolve for these review-body notes. This comment was generated by an AI coding agent. |
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
No blocking findings in this follow-up. HEAD 4ed6d780ee99a96346bc471fee69e00822d37b7b, BASE 77729abfb692b25a0f4ec4a69add86af2e32c0dd; reviewed the one-file delta from 3de2266efed39930bf74e0c831d8cfbcfc3d4ccb and its production callers.
The shared startup seam now uses the existing sampler and publisher runners, preserving their separate cancellation tokens and cadence. The extended retention regression holds the first evaluation across publisher ticks, asserts the completion series is absent, then releases sampling and verifies the independent publisher retains that epoch after sampler cancellation and exporter idle timeout. Both previous nonblocking follow-ups are addressed. One optional test-failure diagnostic improvement is inline. The accepted brief stale-scrape window and other previously reviewed contracts are unchanged.
Validation: pinned-source review on Blox, independently checked against existing hosted CI evidence; no PR code or mutation tests executed for this review. Run 36170855372 passed the 122 selected relay unit tests, including the extended retention regression, and 483 PostgreSQL tests, including the real startup witness. Those jobs tested synthetic merge 7779ad704ba4447c29847c4f200e21153c0b1078 (main 9263cda8 + this head), not the raw pinned head/base pair.
At the captured CI snapshot, Desktop Core remained in progress and Codex Security Review was skipped. No fresh live Datadog validation was performed. This is a clear code review, not approval or a claim that every merge gate is complete.
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Re-review at 4ed6d780. Both of the non-blocking notes from my last pass are addressed, and I don't see anything blocking.
The duplicate entry points are gone. start_dependency_sampler_and_completion_publisher now spawns the public run_dependency_sampler and run_dependency_sample_completion_publisher, so production (main.rs) and router.rs go through the same runners. The wiring is the same as before: DEPENDENCY_SAMPLE_INTERVAL, the same diagnostics handle and the same two cancel tokens. The updated DependencyDiagnostics doc comment matches what the code does now.
The missing "no gauge before first completion" test is covered too. DelayedEvaluator plus the start_paused runtime holds the first evaluate() open across three republish intervals and asserts the scrape is None. Then it releases and checks that the value shows up and is still there after the idle timeout. A publisher that emitted Some(0) or any placeholder before the first completion would fail this test.
One nit, not blocking: release is only notified once, so after the first sample the sampler's next evaluate() parks on release.notified() forever. It never sees dependency_sampler_cancel. The test still models "sampler stopped" correctly, since no more completions happen, but the cancel isn't really what stops it. If you want cancel to be the thing under test, you could use notify_waiters or a flag so later evaluations return right away.
All CI is green except Desktop Domain / Desktop Core, which was still running when I posted. This PR doesn't touch desktop/.
Signed-off-by: tornquist <tornquist@squareup.com> Co-authored-by: Codex <noreply@openai.com>

Summary
Make Kubernetes readiness depend only on the relay process lifecycle, so a shared Postgres or Redis slowdown cannot withdraw every pod at once.
Move dependency checks to one fixed-cadence loop per pod. The loop permits only one evaluation at a time, emits metrics after each sample, and caches a timestamped report.
/_statusreads that cache without calling Postgres, Redis, or the deletion catalog. It reportsnot_yet_sampled,fresh, orstale. Each completed sample also writes a Unix timestamp gauge, so Datadog can compute sample age at query time.Fail closed when the community lifecycle lookup errors. An inconclusive tenant-state lookup no longer admits a socket to AUTH or REQ handling.
Related issue
None found. This addresses the elevated relay HTTP 500 and reconnect incident investigated on 2026-09-03.
Tradeoff
The readiness gauge now reports the latest private-probe observation, not the shutdown transition itself. After shutdown starts, a scrape can still see
1until the next readiness probe refreshes the gauge to0. Kubernetes routing is unaffected because it uses the readiness response from the authoritative process state.A terminal
0was never guaranteed: even a transition-owned gauge could update in process and then exit before Prometheus scraped it. If no scrape occurs before exit, both designs miss the terminal sample. The new design adds at most the interval until the next readiness probe when a scrape occurs during shutdown.Dependency status is now sampled every 30 seconds rather than on demand. A slow evaluation delays the next sample instead of stacking more work. The status payload exposes the cached report age. The completion timestamp stays unchanged when sampling stops, so
time() - timestampcrosses the stale threshold without a server aging loop. Outcome counters and duration histograms remain cumulative and cannot provide no-data detection.Testing
Gimli exercised exact SHA
edff1ec4a5362e132e367bf7449d2b0a68f1e9a9on an isolated Blox stack. Repeated/_statusbursts caused no dependency calls, metrics advanced without status traffic, stale status remained visible, and lifecycle lookup failures admitted no AUTH or REQ handling. Follow-up verification atfdbb394ed598c94cde85ed01cb11e9de0ee8f00dconfirmed sampler-owned outcome and duration emission without status traffic and bound the real startup seam. The final timestamp-gauge change is covered by targeted red/green tests and the same boot regression atc0f36e22b527c2fd6a63a548d036d006ffa17171.Complexity
Readiness remains one process-local sample per probe. Dependency I/O now has one owner and one execution path; request volume cannot increase dependency-check load. One completion timestamp replaces the prior server-side age update path; Datadog computes age without another loop. Community admission also returns to one fail-closed rule for inactive and unknown tenant state.
Generated with Claude Code