mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Summary
The migrations leave `events` and `delivery_log` with a catch-all
partition that already covers every month from 2026-07. Only catch-all
advancement replaces it with monthly partitions. Advancement was off by
default, so any deployment that left out
`BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED` routed every new row into the
catch-all. An operator then had to repair it by hand: drain the
catch-all, swap it for monthly partitions, and re-attach. This has
already happened in one staging environment.
- **Advancement on by default.**
`BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED` now defaults to `true`, and an
explicit `false` still disables it.
`BUZZ_PARTITION_MANAGER_CREATE_ENABLED=false` remains the kill switch
for all automatic partition DDL.
- **Prompt retry while the current month routes to the catch-all.** If a
startup or periodic pass leaves the current month routed to an empty
catch-all that the maintenance planner would replace, the audit loop
retries on its existing backoff. That is 5 s, doubling to a 60 s cap,
for at most 8 retries (about 5 minutes). After that it returns to
`BUZZ_PARTITION_AUDIT_INTERVAL_SECS`. Before this change, a failed first
pass, such as a lock timeout during a rollout, waited the full 15
minutes.
- The routine monthly advance, where only the last lookahead month falls
into the catch-all, stays on the normal interval. Each attempt queues an
`ACCESS EXCLUSIVE` request on the parent, so it shouldn't be retried
without urgency.
- The budget resets once the catch-all no longer awaits advancement.
- A failed attempt keeps the last known state.
- Layouts the planner refuses, such as a catch-all that holds rows, need
an operator, so they are never retried early. Neither is a policy with
create or advance disabled.
- The check is `PartitionTableAudit::awaits_catch_all_advance(policy)`,
plus a `PartitionAudit` helper that applies it to every table. It is
derived from `plan_maintenance`, so it can't drift from the planner's
rules.
- **Fresh-install test.** A new `#[ignore = "requires Postgres"]` test
runs every migration in a dedicated database. It checks that both tables
start out waiting on the catch-all and that create-only maintenance does
not fix them. It then checks that the first pass with advancement
produces canonical monthlies, an empty `{table}_p_future`, trigger
parity and a non-degraded audit, and that a second pass is a no-op.
### Related issue
None found.
### Testing
**Automated:**
- `cargo test -p buzz-db --lib partition`, including `--ignored` against
local Postgres: 21 unit tests and 55 Postgres tests pass.
- `cargo test -p buzz-relay` for the config tests and the partition
schedule tests passes.
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
and `cargo fmt --check` are clean.
- Mutation checks on `awaits_catch_all_advance`. Each fails the new unit
test:
- matching any window month instead of the current month;
- replacing the planner check with the policy flags alone;
- short-circuiting the current-month gate.
**Local relay** (`BUZZ_AUTO_MIGRATE=true`, no advance env var):
- **Fresh database:** startup committed `advanced` for both tables,
creating `p2026_07` through `p2027_04` plus `_p_future`.
- **Failed first pass:** I held `LOCK TABLE ONLY communities IN SHARE
ROW EXCLUSIVE MODE` for 4 s across startup. The startup pass failed with
`lock_timeout` at 17:07:20.8, and the first periodic retry committed
`advanced` for both tables at 17:07:29.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Ian Oberst <ioberst@block.xyz>
Co-authored-by: Ian Oberst <ioberst@block.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
7.8 KiB
Markdown
151 lines
7.8 KiB
Markdown
# Partition catalog monitoring
|
|
|
|
The relay audits the `events` and `delivery_log` partition catalogs at startup
|
|
and every `BUZZ_PARTITION_AUDIT_INTERVAL_SECS` (default 900 seconds). Results are
|
|
diagnostic only. They appear in `/_status.partition_catalog` and in the metrics
|
|
below, and they never change `/_readiness` or `/_liveness`. Alert on them; do not
|
|
gate traffic on them.
|
|
|
|
## Outcomes
|
|
|
|
`buzz-admin partition-audit` prints one outcome for the whole catalog.
|
|
|
|
| Outcome | Meaning | `buzz-admin` exit code |
|
|
|---|---|---|
|
|
| `ok` | Every managed table covers now and needs no operator attention. | 0 |
|
|
| `degraded` | Every table covers now, but one needs attention. | 0 |
|
|
| `unsafe` | A table cannot route a row timestamped now. | 2 |
|
|
| `error` | At least one table could not be audited. | 5 |
|
|
|
|
The relay's `buzz_partition_audit_runs_total{outcome}` is per table and is only
|
|
`ok`, `degraded`, or `error`. There is no `unsafe` outcome there: a table that
|
|
cannot route a row timestamped now is `degraded` and has
|
|
`buzz_partition_serving_safe` 0.
|
|
|
|
A table is `degraded` when it has a partition-key mismatch, a `DEFAULT` or
|
|
anomalous child, trigger drift, an occupied catch-all or `DEFAULT` leaf, or a
|
|
current or lookahead month not covered by a bounded leaf. A bounded legacy leaf
|
|
with a non-canonical name degrades the table while it covers the current month or
|
|
later. Once its upper bound is at or before the start of the current month, it is
|
|
historical. It remains in the per-child report as `legacy_leaf` but no longer
|
|
degrades the table.
|
|
|
|
## Metrics
|
|
|
|
Per-table gauges are set only by a successful audit of that table:
|
|
`buzz_partition_audit_last_success_timestamp_seconds`,
|
|
`buzz_partition_serving_safe`, `buzz_partition_uncovered_months`,
|
|
`buzz_partition_catch_all_covered_months`, `buzz_partition_default_covered_months`,
|
|
`buzz_partition_anomalous_children`, `buzz_partition_catch_all_nonempty`,
|
|
`buzz_partition_default_nonempty`, `buzz_partition_trigger_parity_missing`, and
|
|
`buzz_partition_trigger_parity_extra`. A failed audit leaves them unchanged, so a
|
|
failure never makes the last success look fresh.
|
|
|
|
The exporter removes a gauge that has not been updated within its idle timeout.
|
|
The default is 2,700 seconds: three times the larger of the usage-metrics and
|
|
partition-audit intervals. A failed audit of a table refreshes that table's
|
|
`buzz_partition_audit_last_success_timestamp_seconds` without changing its
|
|
value, so sustained failures keep the last success exported and its age keeps
|
|
rising. A table that has never been audited successfully exports `0`. The
|
|
timestamp disappears only when audits stop running for longer than the idle
|
|
timeout; the other per-table gauges can be evicted during sustained failures.
|
|
|
|
Counters are not idle-evicted while the process runs. The relay exports every
|
|
run outcome for both managed tables, and the failure counter, at 0 when its
|
|
metrics exporter starts, so `increase()` counts the first failure and the first
|
|
run with a new outcome:
|
|
|
|
- `buzz_partition_audit_runs_total{table, outcome}` increments once per table
|
|
per audit. The outcome is `ok`, `degraded`, or `error`, including the opening
|
|
audit of each partition maintenance pass.
|
|
- `buzz_partition_audit_failures_total` counts failed explicit audit attempts:
|
|
the fallback audit that runs when a maintenance pass fails, and each periodic
|
|
audit when maintenance does not run. A maintenance failure is not counted by
|
|
itself. If the fallback audit then fails, that attempt counts once.
|
|
- `buzz_partition_create_attempts_total{table, outcome}` counts per-month
|
|
decisions in each maintenance pass: `created`, `error`, or `skipped_covered`
|
|
for a month that was already covered. Maintenance runs at startup, and on
|
|
every periodic audit when `BUZZ_PARTITION_MANAGER_ADVANCE_ENABLED` is on (the
|
|
default), so `skipped_covered` then grows on every pass in steady state.
|
|
|
|
## Alerts
|
|
|
|
Use all three rules below. Aggregate with `without (...)`, not `by (...)`, so
|
|
cluster, account, environment, namespace, and pod labels stay in the alert.
|
|
|
|
When a pod terminates, its scrape target disappears and its series go stale.
|
|
Instant selectors drop a stale series at once, but range selectors such as
|
|
`[30m]` keep reading its earlier samples until they leave the window. Each
|
|
range-based rule therefore also requires the counter to be exported now, which
|
|
it is for the whole life of a relay, so a terminated pod cannot keep or start an
|
|
alert.
|
|
|
|
`BuzzPartitionAuditNotRunning` also requires the run counter to have existed one
|
|
window ago. After a successful startup audit, the first periodic audit runs a
|
|
full interval later, so a new pod's counter stays flat until then and would
|
|
otherwise read as a stopped loop.
|
|
|
|
```yaml
|
|
# The last success is older than two default audit intervals. This also fires
|
|
# for a table that has never audited successfully, whose timestamp is 0, and
|
|
# keeps firing through sustained failures. Scale 1800 with
|
|
# BUZZ_PARTITION_AUDIT_INTERVAL_SECS.
|
|
- alert: BuzzPartitionAuditStale
|
|
expr: time() - buzz_partition_audit_last_success_timestamp_seconds > 1800
|
|
for: 10m
|
|
|
|
# The relay is running but its audit loop has stopped. Use a window of about
|
|
# two audit intervals, and keep the window plus `for` below the gauge idle
|
|
# timeout (2,700 s by default), so this fires before the stopped loop's
|
|
# timestamp is evicted and the stale rule resolves. Use the same duration for
|
|
# the window and the offset. A loop that stops in a pod's first window is
|
|
# caught once the pod is a window old, at about 35 minutes.
|
|
- alert: BuzzPartitionAuditNotRunning
|
|
expr: |
|
|
sum without (outcome) (increase(buzz_partition_audit_runs_total[30m])) == 0
|
|
and sum without (outcome) (buzz_partition_audit_runs_total)
|
|
and sum without (outcome) (buzz_partition_audit_runs_total offset 30m)
|
|
for: 5m
|
|
|
|
# Explicit audit attempts keep failing, even if a later attempt succeeds.
|
|
- alert: BuzzPartitionAuditFailing
|
|
expr: |
|
|
increase(buzz_partition_audit_failures_total[1h]) >= 2
|
|
and buzz_partition_audit_failures_total
|
|
```
|
|
|
|
The counters exist from relay start, and the success timestamp exists after the
|
|
first audit attempt, whether or not it succeeded. The startup audit runs within
|
|
seconds of the zero baselines, usually before the first scrape, so
|
|
`BuzzPartitionAuditFailing` can miss a failure in that one attempt; if it does
|
|
not recover, `BuzzPartitionAuditStale` fires. The run counters are exported
|
|
before the relay connects to the database, so a live relay that is stuck before
|
|
its first audit shows flat zero counters, which `BuzzPartitionAuditNotRunning`
|
|
catches.
|
|
|
|
`degraded` asks for operator attention; it does not mean rows cannot be
|
|
written. Historical legacy leaves no longer count, so a repaired catalog
|
|
returns to `ok` once the current month passes the repaired range and every
|
|
current and lookahead month is a bounded monthly partition. Months still covered
|
|
by a catch-all stay `degraded` until advancement moves the catch-all. Coverage
|
|
problems show directly in `buzz_partition_serving_safe` and
|
|
`buzz_partition_uncovered_months`.
|
|
|
|
The tests in `crates/buzz-relay/src/metrics.rs` (`partition_alert_tests`) check
|
|
that the production exporter and audit emit the series these rules read: zero
|
|
counter baselines before the first audit, a `0` timestamp before any success,
|
|
the last success kept through failures past the idle timeout, and run counters
|
|
that outlive an evicted timestamp. They do not
|
|
evaluate the rules, their `for` durations, or label matching.
|
|
`partition-catalog-monitoring.rules.test.yml` evaluates the rules in
|
|
`partition-catalog-monitoring.rules.yml`, including their `for` durations,
|
|
against a fresh pod, a stopped loop, a terminated pod, a table that never
|
|
succeeded, and repeated failures. CI does not run it. Keep the rules file
|
|
identical to the block above, and run it after changing either:
|
|
|
|
```bash
|
|
promtool test rules docs/partition-catalog-monitoring.rules.test.yml
|
|
```
|
|
|
|
Then validate label matching against your own alerting configuration.
|