Files
buzz/docs/storage-accounting.md
Luke Tornquist 506a588f88 refactor(relay): remove database usage-metric queries (#9040)
## Summary

The relay no longer runs database aggregate queries to emit usage
gauges. Fleet and per-community counts of users, channels, relay
members, workflows, git repos, communities, messages, and active
users/channels are no longer relay metrics, and neither are the snapshot
health gauges that came with them.

### Removed

- **Gauges:** `buzz_total_users`, `buzz_total_channels`,
`buzz_total_relay_members`, `buzz_total_workflows`,
`buzz_total_git_repos`, `buzz_total_active_users`,
`buzz_total_messages`, `buzz_total_active_channels`,
`buzz_communities_estimated` / `buzz_communities_total`, and every
DB-derived `buzz_community_*` series (users, channels, messages, relay
members, workflows, git repos, active users, active channels).
- **Per-community mode:** `BUZZ_USAGE_METRICS_PER_COMMUNITY=all` and
everything it fed: the per-community in-memory gauges,
`buzz_community_storage_bytes` / `buzz_community_storage_objects`,
`buzz_storage_unmapped_community_bytes`, the community → host map query,
and the emission-scope bookkeeping that cleared series for renamed or
removed communities. No deployment set it. If one still does, the relay
ignores it.
- **Health signals:** `buzz_usage_snapshot_available`,
`buzz_usage_snapshot_age_seconds`, `buzz_usage_query_skipped_total`, and
the `usage_fleet_*` paths of `buzz_db_route_decision`.
- **Code and config:**
  - `buzz-db/src/store/usage.rs`, including its aggregate queries.
- The relay's `FleetUsageSchedule` and its cadence/deadline/retry
machinery.
  - The replica-only telemetry route (`Db::route_usage_read`).
- `BUZZ_USAGE_METRICS_REPLICA_MAX_AGE_MS` and
`DbConfig::usage_metrics_replica_max_age_ms`. If a deployment still sets
the env var, the relay now ignores it; startup doesn't fail.
- **Reader maintenance pool operation:** `ReaderOperation::Maintenance`
and its `close_on_drop` release path existed only for those telemetry
reads, so they're removed too. The pool-acquire contract drops from
twelve to eleven valid pairs, which lowers the per-pod series ceiling
from 216 to 198.
- **Dead code:** `Db::active_community_hosts` had no callers.

### Kept

- **Leader:** the usage poller's leader lock, plus the leader's invite
reaping and fleet storage-snapshot gauges. The session advisory lock
(`SessionLeader`, `Db::try_lock_session_leader`) moves to
`buzz-db/src/store/leader_lock.rs`, since storage-usage delivery uses it
too and it has nothing to do with usage queries.
- **In-memory gauges:** pod-local connection, subscription, and
online-user gauges. They read process state, not the database.
- **Writer maintenance pool operation:** `WriterOperation::Maintenance`
stays. Dozens of writer-side maintenance paths across `buzz-db` use it
(reapers, retention, the leader lock). The reader variant only ever
served the telemetry reads.
- **Storage-usage delivery:** unchanged. It reads the storage snapshot
for billing, not for metrics.

`docs/usage-metrics.md`, `docs/storage-accounting.md`, `.env.example`,
and the Helm chart README are updated to match.

## What got simpler

This is net deletion (~3.6k lines). One telemetry subsystem is gone: two
collection families with independent cadences, availability and age
semantics, relay-side deadlines, a dedicated replica routing policy, two
config knobs, a per-community emission mode, and a pool-operation
variant. Leader election now does only leader work, and it lives in its
own module.

## Operational note

Dashboards and monitors that read the removed series will go to no data
once this rolls out.

## Testing

- `cargo fmt --check`, workspace `cargo clippy --all-targets -D
warnings`.
- `buzz-db` lib, `observability_source`, and doc tests.
- `buzz-relay` lib tests.
- The Postgres lane for `buzz-db` (lib + tests) and `buzz-relay` (lib).

All of these run on a remote workstation at the PR head.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:00:30 -05:00

108 lines
5.5 KiB
Markdown

# Storage accounting
`buzz-admin storage-snapshot` lists S3, calculates physical and per-community
logical usage, and atomically replaces the completed snapshot in PostgreSQL.
The relay reads that snapshot and exposes the existing storage gauges on its
metrics endpoint for collection by Datadog or another metrics collector.
## Independent rollout
Deploy the snapshot-reading relay version through the usual release process.
It is ready before a worker exists: no database row means no storage gauges.
Add a scheduled worker to each environment when ready. Its first successful
publication becomes visible on the next leader usage tick, without restarting
the relay or changing its mode. The default tick interval is 300 seconds.
The snapshot table is part of the normal database schema, independent of worker
installation. A missing table or inaccessible database is an error, not evidence
that the worker is absent. Snapshot reads have a five-second budget, including
connection acquisition, and retry on the next usage tick. They do not run on
the relay startup path and never fall back to an S3 scan.
`BUZZ_STORAGE_METRICS` accepts:
| Value | Behavior |
|---|---|
| Unset, `external`, `snapshot`, `on` | Read completed worker snapshots. |
| `inline` | Legacy alias for the reader; logs a migration warning once. |
| `off` | Skip storage reads and metrics. |
| Anything else | Log a configuration error and disable storage metrics. |
Relay-local scans have been removed. Existing chart values that set `inline`
need no coordinated configuration change. Environments without a worker will
stop producing storage totals when this relay version arrives. The former
`BUZZ_STORAGE_SWEEP_*` settings no longer apply; size and bound the worker
instead. An intentional `off` override still requires an operator to enable it.
## Freshness and failures
The reader publishes the last completed totals together with
`buzz_storage_snapshot_age_seconds`. Re-reading an old row never resets its
completion time. Alert on age using a threshold appropriate for the worker's
schedule and allowed run time; for a daily worker, allow more than 24 hours.
`buzz_storage_snapshot_load_ok=1` means the row was read and decoded, not that
the worker's most recent attempt succeeded. Check Job failures separately.
A failed or invalid read logs an error, sets load health to zero, and retains
the last good totals with their original age. With no prior good snapshot,
only failed-load health is emitted. A successful read that finds no row clears
the cache and stops refreshing storage series. Existing exported series expire
through the metrics exporter's idle timeout; absence is never reported as zero
bytes. A valid empty snapshot can report zero bytes.
The reader emits fleet totals from the leader only; per-community storage is
not a relay metric. It does not need S3 listing permission. The worker still
needs that permission and its database configuration.
## Usage delivery
Set `BUZZ_STORAGE_USAGE_URL` to push each new snapshot's per-community totals
to an operator endpoint. kgoose's `POST {prefix}/v1/buzz/storage-usage/record`
receives them and bills sponsored communities in Orb. Unset, nothing is sent.
Every five minutes, one relay pod (elected by a PostgreSQL session advisory
lock) compares the stored snapshot's `completed_at` with the last one it
handled and, when it is new, POSTs every community in consecutive batches of
at most 50:
```json
{
"measured_at": "2026-10-30T03:04:12Z",
"communities": [
{
"community_host": "acme.communities.buzz.xyz",
"storage_bytes": 123456789,
"storage_objects": 412
}
]
}
```
- `measured_at` is the snapshot's completion time in RFC 3339, the same for
every batch of a snapshot. Each batch is an independent request.
- Each request carries a NIP-98 `Authorization` header signed by the relay
key, binding the URL and body, as operator-listener mention notifications
do. kgoose authenticates the relay by its `buzz` mesh service identity
instead, so the relay needs mesh egress to kgoose-builderbot.
- `storage_bytes` and `storage_objects` are the logical per-community values
described above: a blob bound to several communities counts in full for
each.
- Every snapshot community with a community row is sent, including archived
or deleting ones that still hold bytes. An active community absent from the
snapshot is sent with zero, so a receiver keeping the latest value never
retains a stale one. A snapshot community with no community row has no host
and is skipped with a warning.
- A 2xx accepts a batch. A 408, 429, 5xx, or transport error stops the run and
re-sends the whole snapshot on the next check. Any other status abandons
that snapshot; the next worker run is delivered normally.
- Which snapshot was handled is kept in memory, so a restart or leader change
re-sends the latest snapshot. Receivers MUST treat a repeated community and
`measured_at` as already recorded; kgoose bills only a community's first
report for each UTC day.
- A snapshot replaced before it was delivered is never sent. Alert on
`time() - buzz_storage_usage_delivered_snapshot_timestamp_seconds` alongside
snapshot age; `buzz_storage_usage_deliveries_total{outcome}` counts
`accepted`, `retry`, and `rejected` checks. The leader re-publishes the
delivered timestamp on every check, so it survives the gauge idle timeout
between daily snapshots. After a restart or leader change it has no sample
until the next delivery succeeds, so treat a missing series as failing too.