Skip to content

perf(relay): bound fleet usage metrics collection - #7176

Open
TheSentinel454 wants to merge 3 commits into
mainfrom
codex/bounded-usage-metrics
Open

TheSentinel454 wants to merge 3 commits into
mainfrom
codex/bounded-usage-metrics

Conversation

@TheSentinel454

@TheSentinel454 TheSentinel454 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Why

The usage poller currently pays community-proportional memory and query costs even when per-community emission is disabled: every pod loads the full host map and the leader materializes all per-community aggregates before suppressing their labels. That makes telemetry collection itself a scaling constraint.

What changed

  • Make fleet-only usage metrics the safe default; retain all as an explicit rollback mode.
  • In fleet-only mode, skip the community host map and all community-sized query results.
  • Replace exact community counting with the PostgreSQL planner table-size estimate.
  • Collect stock totals as one fixed row, scanning each stock table once per hour.
  • Derive 1d/7d/30d active-user totals from one bounded 30-day scan per day.
  • Run fleet database snapshots only on a proved read replica using a telemetry-specific freshness budget and server-side statement timeouts; skip rather than fall back to the writer.
  • Cache successful fixed-row snapshots and re-emit them on each poll tick so gauges remain scrape-visible without increasing query frequency.
  • Retry failed or unavailable fleet collections after 60 seconds and expose fixed-cardinality availability gauges.
  • Clear cached snapshots and availability on leader demotion, force fresh collection after reacquisition, and document leader-filtered multi-pod aggregation.
  • Stop querying lifetime stored-message and active-channel totals in fleet-only mode. Event throughput remains available through the existing stored-event counter.
  • Finalize storage sweeps without allocating the per-community result map in fleet-only mode, and distinguish an omitted breakdown from a real zero.
  • Add seeded PostgreSQL contract tests and operator rollout, rollback, availability, and leader-election documentation.

No migrations, tables, indexes, or new persistent storage are added.

Risk

Medium. The default changes which telemetry is emitted:

  • buzz_communities_estimated replaces the exact buzz_communities_total in fleet-only mode.
  • buzz_total_messages and buzz_total_active_channels are not refreshed in fleet-only mode.
  • Deployments without an available proved reader skip database-derived fleet snapshots rather than querying the writer.
  • Multi-pod dashboards must filter database and storage series to buzz_usage_poller_is_leader == 1.

Setting BUZZ_USAGE_METRICS_PER_COMMUNITY=all restores the legacy collector for temporary rollback.

Verification

  • Current-base cargo fmt, git diff --check, actionlint, PostgreSQL test discovery/wrapper contracts, and focused clippy passed.
  • cargo test -p buzz-db --lib: 115 passed, 258 PostgreSQL tests ignored in the infrastructure-free run.
  • cargo test -p buzz-media: 127 passed, 3 external MinIO tests ignored.
  • cargo test -p buzz-relay: 1,025 passed, 90 infrastructure tests ignored.
  • Seeded PostgreSQL tests verified exact stock/activity aggregates and replica-only skip behavior.
  • Live reduced-idle test verified cached fleet gauges remain present while stock/activity database route counters stay at one.
  • Live two-relay tests verified one advisory-lock owner, follower takeover, demoted availability clearing, and fresh snapshots on the new leader.
  • Live no-reader test verified no writer fallback, unavailable gauges, and 60-second retry.
  • Live rollback test verified all restores per-community series.
  • Desktop Playwright smoke against the live relay verified connect, channel navigation, message send/receive, settings interaction, and clean browser logs.
  • Independent exact-head review found no Critical or Important issues and confirmed alignment with the architecture, vision, and multi-tenant guidance.

Generated with Codex

Implemented and verified with Codex.

Signed-off-by: tornquist <tornquist@squareup.com>
@github-actions

github-actions Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 5aed49b505a7e27f3b0e34dafa53d6c4e8cdcd64...aa3e70234f5842f9bfa49b472e2e66fa3011f727.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review aa3e70234f5842f9bfa49b472e2e66fa3011f727 to authorize a new review.
Any previous review applies only to its recorded range.

@TheSentinel454
TheSentinel454 marked this pull request as ready for review September 1, 2026 20:42
@TheSentinel454
TheSentinel454 requested a review from a team as a code owner September 1, 2026 20:42
ravarora2 added a commit that referenced this pull request Sep 23, 2026
## Summary

Make the relay read completed storage-accounting snapshots from
PostgreSQL by default. An environment with no snapshot emits no storage
metrics. When a worker publishes its first result, the relay picks it up
on the next successful leader usage tick, without a restart or
configuration change.

This changes how storage usage is measured and reported. **S3 remains
the object store. PostgreSQL stores the completed counts, byte totals,
community breakdown, and calculation metadata—not the objects
themselves.**

### Why move relay storage accounting entirely to database reads?

Today the relay supports two sources: its own S3 scan (`inline`, the
default) and the worker's saved snapshot (`external`). That requires
operators to deploy a worker and then coordinate a separate relay-mode
change in each environment.

The worker already owns the S3 listing and calculation. It runs in a
separate process with its own object cap, memory limit, and deadline.
Keeping a second scan inside the serving relay retains the resource
pressure and failure modes that this isolation is meant to remove.

PostgreSQL provides a durable handoff. The worker replaces one completed
snapshot atomically after a successful calculation. The relay reads that
row and exposes its values through the existing metrics endpoint. A
relay restart or leadership change can load the last completed result
without scanning the bucket again.

There is deliberately no S3 fallback when a snapshot is absent or a read
fails. Such a fallback would put the expensive scan back in the relay
precisely when a worker is absent or unhealthy. It would also leave two
calculation paths with different resource limits and freshness behavior.

### What happens if no worker exists?

**A worker that has never published is a normal, successful inactive
state.** The snapshot table must exist through the normal database
schema, but it may contain zero rows. The reader returns success, emits
no storage totals or failed-load gauge, and checks again on the next
usage tick. It does not exit the relay, start a worker, or list S3.

The relay checks for a completed database row; it does not inspect
Kubernetes Jobs or CronJobs. Worker presence and snapshot presence are
therefore different:

| Situation | Storage-reader behavior |
|---|---|
| No worker and no snapshot row | Return success, emit no storage
metrics, and query again next tick. |
| Worker starts later and publishes its first row | Read and emit it on
the next successful leader usage tick; no restart or mode change. |
| Worker is removed or fails, but its last valid row remains | Continue
reporting the last completed totals with their increasing age. |
| Query succeeds but a previously present row has been removed | Clear
the cache and stop refreshing storage series; existing exporter series
expire through the idle timeout. |
| Database read fails, times out, or returns an invalid snapshot | Log a
warning, set `buzz_storage_snapshot_load_ok=0`, retain any last-good
cached totals, and retry next tick. |
| Snapshot table is missing | Treat this as a schema error, not normal
worker absence; follow the failed-read behavior above. |
| `BUZZ_STORAGE_METRICS=off` | Skip the snapshot query and all storage
metric emission. |

No row means “not measured,” not zero bytes. A valid completed snapshot
of an empty bucket can report zero. If a read fails before any good
snapshot has been cached, only failed-load health is emitted.

These reads happen in the existing background metrics task, not the
relay startup path. A storage-read failure does not request process exit
or change readiness directly. A wider database outage can still affect
the relay's existing database-dependent operations and readiness.

### Read, parse, and emit path

```text
Worker: S3 listing -> BucketSnapshot -> JSON -> PostgreSQL
Relay: PostgreSQL -> JSON -> BucketSnapshot -> existing metric gauges
Collector: relay /metrics endpoint -> Datadog
```

1. `main.rs::run_storage_sweep_tick` calls the reader from the existing
leader-only usage task. The default interval remains 300 seconds.
2. `storage_sweep.rs::run_storage_metrics_tick` coordinates the read,
cache update, failure health, and metric emission.
3. `refresh_persisted_snapshot` calls the existing
`Db::load_storage_accounting_snapshot()` method. A five-second timeout
covers pool acquisition and the query. It uses the relay's existing
pool.
4. The existing query in `buzz-db/src/store/storage_accounting.rs` reads
`snapshot`, `completed_at`, `duration_ms`, `max_objects`, and `code_sha`
from the singleton row.
5. `serde_json::from_value(stored.snapshot)` decodes the JSON into the
existing `buzz_media::BucketSnapshot` type. Rust infers that type from
`cache_persisted_snapshot`'s argument. The generated `Deserialize`
implementation handles all fields, including the UUID-keyed
`per_community` map.
6. `emit_cached_storage_metrics` sets the existing physical/logical
totals and community byte/object gauges. The existing Prometheus
exporter exposes them for Datadog collection; this code does not send
snapshot JSON to Datadog.

The worker's `BucketSnapshot`/`CommunityStorage` structures, JSON field
names, SQL publication method, and database schema are unchanged. The
old external-mode JSON conversion moves out of `main.rs` into the reader
helper. No new format or hand-written field parser is needed. Duration
and object-cap metadata are validated before replacing the cache.

The five-second relay read timeout is separate from the worker's
30-second startup acquisition budget introduced in #7770. This PR does
not change the worker's pool or startup logic.

### Configuration and code changes

| Configuration | Before | After |
|---|---|---|
| Unset, `inline`, or `on` | Relay-local S3 scan | Read completed
database snapshots. `inline` logs one migration warning. |
| `external` or `snapshot` | Read completed database snapshots |
Continue reading snapshots. |
| `off` | Disabled | Disabled. |
| Unknown or empty value | Disabled with a configuration error | Same. |

Existing `inline` settings intentionally become reader aliases. Merely
changing the default would leave older deployments on their explicit
`inline` setting and preserve the need for coordinated configuration
PRs.

Remove relay-local scan scheduling, in-flight scan state, scan
configuration, and obsolete scan-attempt tests and health metrics.
Preserve the physical/logical total and per-community metric names,
leader-only emission, community scope filtering, and cleanup of old
community labels.

The shared relay-state field remains in place; its comment now describes
a cached worker result. The Helm chart defaults to `relayMode: external`
while leaving `storageAccounting.enabled: false`. Its rendering test
verifies that the reader is configured even when no worker is created.
The reader regression tests cover missing rows, later publication,
updates, invalid data, timeout, removal, reactivation, and the explicit
off switch. `docs/storage-accounting.md` describes this operating model.

### Freshness and rollout impact

`buzz_storage_snapshot_age_seconds` uses the worker's original
completion timestamp. Re-reading an old row never makes it fresh.
`buzz_storage_snapshot_load_ok=1` means the row was read and decoded; it
does not prove that the worker's latest attempt succeeded.

Monitor snapshot age against the worker's schedule and allowed runtime,
and monitor Job failures separately. The old relay scan-attempt health
gauges are retired because the relay no longer runs those scans.

Deploy this relay version through the usual release process. Once it
reaches an environment, workers can be enabled independently.
Already-released charts that still set `inline` work with the new
binary; future chart releases default explicitly to `external`. An
intentional `off` override still requires an operator to enable the
reader.

**Environments with no completed snapshot lose relay-calculated storage
totals after this upgrade.** That is the intended tradeoff for removing
scans from the serving process. Environments with a prior snapshot
continue showing its last completed totals and age. This PR does not
install workers in additional environments or deploy the new relay
image.

### Related issue

Related to #4601. Follows the worker startup fix in #7770. The separate
fleet-usage collector work in #7176 overlaps this code and may need
reconciliation when it merges.

### Testing

Built and ran the relay locally against isolated PostgreSQL and Redis
with the legacy `inline` setting, five-second usage ticks, and a
15-second metric idle timeout. The same process was exercised through
these steps:

1. Start with an empty snapshot table: relay becomes ready and exposes
no storage series.
2. Publish a snapshot containing 123 bytes: the community gauge becomes
123.
3. Replace it with 456 bytes: the gauge becomes 456.
4. Remove the row: storage series expire without fabricating a zero-byte
measurement.
5. Publish 789 bytes: metrics reactivate without restarting the relay;
readiness remains healthy.

The local S3 endpoint recorded zero requests. The unrelated Git
conformance startup probe was disabled for this isolated reader check;
the test covers storage accounting, not every other use of S3 in the
relay. No new relay image was deployed to staging or production during
this test.

The broad local integration run also hit an existing
`p0_pool_acquisitions_use_typed_operation_pairs_without_other` failure
in `buzz-db/tests/observability_source.rs`. The same source-only test
fails on unmodified `main` at `0ef7a2222`; this PR changes neither the
test nor its failing source.

The full local `just ci` run stopped at mobile native-asset setup: the
`objective_c` build hook received no SDK path from `xcrun`. Mobile tests
did not run.

Generated with Codex

Signed-off-by: Ravneet Arora <rarora@squareup.com>

@wesbillman wesbillman 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.

Carl, an automated reviewer, commenting via Wes’s GitHub account.

Changes requested. Reviewed HEAD aa3e70234f5842f9bfa49b472e2e66fa3011f727 against BASE 42b42447b0dc47c3e1a4d95caab893567dcd1677. Keep the fleet-only default, replica-only collection, cached fixed-row snapshots, and explicit all rollback. Merge criteria: correct warm-refresh availability, make rollback availability/gating coherent, and execute the five new relay/media regression witnesses in CI. Details are inline.

Source-only review; no PR code or tests executed. Existing PostgreSQL CI passed the three new fleet database tests on a merge tree identical to this HEAD. The PR-body live-test claims were not independently verified.

}

fn mark_stock_failure(&mut self, now: std::time::Instant) {
self.next_stock = now + self.retry_interval;

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.

[P2] Mark a failed refresh unavailable even when a cached snapshot exists

After one successful stock collection, let the next due refresh return Ok(None) (reader missing/stale/unavailable) or Err. This method only advances the retry deadline; stock_snapshot remains Some, so stock_available() remains true and emit_fleet_db_usage_metrics re-emits the old values with buzz_usage_snapshot_available{family="stock"}=1 on every tick. The activity family has the same path. Repeated failures therefore advertise arbitrarily stale data as available.

That contradicts .env.example:30–34 and the availability gate prescribed in docs/usage-metrics.md:19–33,44–45; snapshot age exists, but those instructions do not require an age gate. Preserve last-known values if desired, but track latest-refresh availability separately: clear it on a due skipped/failed refresh and restore it only after success. Add a success → failed/omitted refresh → successful recovery regression for both families, including the emitted availability gauges.

}

#[test]
fn usage_metrics_default_to_fleet_only() {

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.

[P2] Wire the new relay/media regression tests into an executed CI lane

Five of the eight new tests are excluded by the configured Rust CI selections. just test-unit (Justfile:357–433) enumerates packages without buzz-media and runs buzz-relay --lib only under test(/^api::admin::/): the two tests here are binary-target tests, while the new config::tests and storage_sweep::tests are filtered out. Backend Integration selects workflow_sink/e2e_event_reminder; the PostgreSQL lane selects ignored PostgreSQL tests, not these five nonignored witnesses. Existing run 33551030165 confirms those invocations on a merge tree identical to this HEAD; the three new database tests do execute.

Consequently, reverting the fleet-only default, breaking cadence/cache reset, or making totals-only storage return incorrect totals/false-zero attribution can pass the configured gates despite these tests. Add narrow infra-free selections for the five existing witnesses (including --bin buzz-relay for these two) to the unit recipe, preserving existing exclusions. Verify their test names execute; compiling them is not a regression gate. This is not a request to broaden the entire relay suite or add a new harness.

Comment thread docs/usage-metrics.md
1. Deploy with per-community mode unset or `off` and the telemetry replica
freshness budget at its 30000ms default.
2. Update dashboards to use fleet gauges and gate alerts on the availability
gauges above.

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.

[P2] Make availability gating coherent with the documented rollback

Following step 2, a dashboard gates fleet totals on buzz_usage_snapshot_available == 1. Restarting with the documented BUZZ_USAGE_METRICS_PER_COMMUNITY=all rollback then suppresses those totals in the dashboard even when collection succeeds: run_usage_metrics_tick calls only emit_db_usage_metrics in that mode (main.rs:1782–1791), whereas the only assignments of availability 1 are in emit_fleet_db_usage_metrics (:1886–1894). Thus the gates are absent after startup; after demotion they can also be 0, with no success path restoring them.

Either publish coherent availability for successful/failed legacy collection as well, or explicitly document that these gates apply only to off and give the corresponding rollback dashboard procedure. The rollback must not silently invalidate the monitoring instructions immediately above it. If fixing the emitter, add a successful all collection/demotion/recovery witness.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Review at aa3e70234f5842f9bfa49b472e2e66fa3011f727 (base 42b42447b). Thanks for this. The fixed-row fleet SQL and the replica-only routing look right to me. route_usage_read never falls back to the writer, it records a skip reason for every early exit, and the two queries get their own SET LOCAL statement_timeout inside the proved transaction. The new PG tests (fleet_snapshots_match_seeded_stock_and_activity_exactly and friends) run and pass in the PostgreSQL lane at this head.

Three things block it for me:

  1. IMPORTANT / Correctness: the storage half is superseded by main and needs a rework on rebase, not just a conflict fix. Since this branched, #7543 and #7845 moved S3 listing out of the relay. On main, storage_sweep.rs only loads the latest buzz-admin worker snapshot from PostgreSQL (run_storage_metrics_tick), and the relay never calls fold_bucket_listing. That makes finish_totals, fold_bucket_listing_totals and the relay-side include_per_community branch dead code. There's also a trap if the rebase keeps host_map empty in off mode and passes it to main's emit_cached_storage_metrics. Every per_community entry misses the host map and lands in unmapped_bytes, so buzz_storage_unmapped_community_bytes would report the whole logical total as unmapped, which is exactly the false anomaly community_breakdown_available was meant to prevent. I think the storage changes (bucket_index.rs, the lib.rs export, storage_sweep.rs, the run_storage_sweep_tick branch) should be dropped. Then fleet-only mode on main's reader either skips the unmapped/per-community emission or passes a real host map. The branch currently conflicts in bucket_index.rs, relay main.rs and storage_sweep.rs.

  2. IMPORTANT / Correctness: the availability gauges don't carry the contract the docs give them. docs/usage-metrics.md tells dashboards to gate on buzz_usage_snapshot_available, but two paths break that:

    • Stale data reads as available. stock_available() / activity_available() are just snapshot.is_some(), and mark_*_failure only moves the retry deadline. After one success, a reader that goes stale or unavailable keeps re-emitting the cached values with available=1 indefinitely. That's the same thing Carl flagged inline, and I confirmed it in emit_fleet_db_usage_metrics. Main's storage reader handles this with a separate buzz_storage_snapshot_load_ok. I think either drop availability to 0 once age passes interval plus grace, or add a last-refresh-ok gauge and document age-based gating.
    • Rollback hides everything. In BUZZ_USAGE_METRICS_PER_COMMUNITY=all, run_usage_metrics_tick only runs emit_db_usage_metrics, so the availability gauges are never set to 1, and demotion explicitly sets them to 0. Dashboards gated the documented way would blank the fleet panels during the rollback. The rollback path should set both families available (and community_breakdown_available) when the legacy collection succeeds.
  3. IMPORTANT / Correctness: five of the new non-ignored tests never run in CI. These are usage_metrics_default_to_fleet_only and fleet_usage_families_have_independent_cadences (relay binary target), usage_metrics_replica_budget_defaults_on_independently_of_serving_reads (config.rs), totals_only_snapshot_marks_breakdown_unavailable_without_false_unmapped_zero (storage_sweep.rs) and totals_only_omits_community_map_without_changing_fleet_values (bucket_index.rs). None of them show up in the Unit Tests, Backend Integration or PostgreSQL job logs at this head. The head's test-unit runs buzz-relay --lib scoped to api::admin:: and no buzz-media or relay bin tests. After rebasing, the two storage tests go away with finding 1. Main's test-unit already has a relay selector list that grows by module prefix, so the remaining three need explicit selectors there: a config::tests:: exact match plus a --bin buzz-relay line for the two schedule tests. Please mirror them in the run-tests.sh unit fallback. The cadence test is the only thing pinning the retry/interval logic behind finding 2, so it matters that it actually runs.

Non-blocking:

  1. MINOR / Correctness: default config without a replica loses every DB fleet total. With off as the new default and no writer fallback, any deployment without READ_DATABASE_URL, or with a failed floor-guard verification, goes from emitting buzz_total_users/channels/active_users/etc. to emitting nothing, forever available=0. That covers local compose and single-node self-hosts. .env.example does say this, so it may well be intended. If so, I'd put it in the rollout section of docs/usage-metrics.md too, since it's the most visible behavior change for anyone not running Aurora.

This branch has not been deployed

No deployments
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