Repository navigation
feat(db): label advisory-lock metrics with the locked event kind - #9472
Conversation
Advisory-lock waits cannot be attributed today: relay-owned lists (13535, 39000-39002) and user replaceables share the same generic replacement paths and the same `lock_type="replacement"` series, and the 13534 membership list's coordinate lock is recorded only as `lock_type="membership"`. Add a `kind` label to buzz_db_advisory_lock_wait_seconds and buzz_db_advisory_lock_acquisitions_total. Locks keyed by an event coordinate (every replacement lock and the 13534 list lock) record the coordinate's kind through observe_event_lock; other locks record `none`. buzz_core::kind::registered_kind_label bounds the value to the kind registry and maps anything else to `other`. Narrow the block#6700 source guard: `"kind" =>` may appear only in the advisory-lock observer, whose kind must come from the registry. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Ian Oberst <[email protected]>
joahg
left a comment
There was a problem hiding this comment.
🤖 Looks good. I read the full diff and found no correctness blockers.
- Cardinality: the
kindvalue isregistered_kind_label, which returns the decimal kind only if it is inALL_KINDS, otherwiseother. A client choosing arbitrary kinds therefore cannot create new series; the value set is the fixed registry plusotherandnone. Only replaceable/addressable kinds reachLockType::Replacement, so in practice the set is much smaller than the registry, and the series count matches the PR's estimate or is lower. - Locking behavior: every call site only swaps the observer. Lock keys, the
pg_advisory_xact_lockquery and its binds are unchanged. I checked that the constants used (KIND_NIP29_GROUP_MEMBERS= 39002,KIND_WORKFLOW_DEF,KIND_NIP43_MEMBERSHIP_LIST= 13534) match the old literals/values, and that allLockType::Replacementcallers on main now useobserve_event_lock. Thedebug_assertguards against a missed one. - Overhead: one linear scan of ~135
u32s and one smallStringallocation per lock acquisition, next to a Postgres round trip. Negligible. - Existing tests that match on
lock_typeonly (e.g. the push delivery test) are unaffected by the extra label. The source guard is narrowed to exactly the two"kind" =>uses inobserve_lock.
Not reviewed: Postgres tests were not run by me (the PostgreSQL Tests check is green); dashboards or alerts outside this repo that use exact label sets on these two metrics would need sum by (lock_type, outcome)-style aggregation now that there is a kind label. The "Run Codex Security Review" check shows a failure, which I did not investigate.
| /// | ||
| /// The registry bounds the label set, so the label is safe on metrics whose | ||
| /// kind comes from client events. | ||
| pub fn registered_kind_label(kind: i32) -> String { |
There was a problem hiding this comment.
🤖 Optional: this allocates a String on every lock acquisition, and observe_lock then clones the SharedString for the second metric. Returning Cow<'static, str> (static "other", owned only for registered kinds) or a precomputed table would avoid the per-call allocation. It is small next to the lock round trip, so fine to leave as is.
Why
On one deployment,
buzz_db_advisory_lock_wait_seconds{lock_type="replacement"}added up to about 245,000 seconds of wait in 7 days. The metric cannot say which coordinates wait. Replacement locks serialize writers of one(kind, pubkey[, d_tag | channel])coordinate. Relay-owned lists (kinds 13535, 39000–39002) go through the same generic replacement paths as user replaceables (kinds 0, 3, 10002, 30078, …). The 13534 membership list takes its coordinate lock underlock_type="membership". Before we change how these lists are stored, we need to know which kinds the wait comes from.What
buzz_db_advisory_lock_wait_secondsandbuzz_db_advisory_lock_acquisitions_totalget akindlabel.observe_event_lock(lock_type, kind, …). This covers all 4 replacement call sites (parameterized replace,take_replacement_lockfor addressable replace and group-state snapshots, the kind:39002 member snapshot, and workflow deletion) and the kind:13534 membership-list lock. The 13534 lock keepslock_type="membership", so totals bylock_typedo not change.kind="none", and their call sites do not change.observe_advisory_lockhas adebug_assertthat rejectsLockType::Replacement, so a replacement lock cannot lose its kind by mistake.buzz_core::kind::registered_kind_label(kind)gives the label value: the decimal kind when it is inALL_KINDS, otherwiseother.Change to the #6700 label contract
#6700 says no database metric carries event kinds, and
observability_source.rsenforces this with a blanket ban on"kind" =>. This PR makes one narrow exception: the two advisory-lock metrics.other. Non-replacement locks are alwaysnone. New series in the worst case: 39 kinds × 4 outcomes, for the histogram and the counter."kind" =>must appear exactly twice, inside the advisory-lock observerobserve_lock, whose kind must come fromregistered_kind_labelinsideobserve_event_lock. It is still forbidden everywhere else inobservability.rsand in the datastore tracing macro. The Add operation-aware database pool acquisition metrics #7195 guard that forbids a literal"other"inobservability.rsis unchanged, because the fallback lives inbuzz-core.Dashboards and alerts
Both metrics gain a label, so existing selectors keep working:
lock_type="replacement"still matches every replacement series, because Prometheus and Datadog selectors match on a subset of labels. A consumer is affected only if it depends on the exact label set:sum by (lock_type, outcome) (...)to keep the old shape.on(lock_type, outcome)orignoring(...)now has more series on one side. Aggregate before the join.To read the new attribution, group by
kind, for examplesum by (lock_type, kind) (rate(buzz_db_advisory_lock_wait_seconds_sum[5m])). No consumer in this repository depends on the exact label set.Tests
buzz-core:registered_kind_label_bounds_unregistered_kindschecks that a registered kind passes through and that an unregistered or negative kind becomesother.buzz-dbrecorder test: checks the replacement series for a registered kind (13534) and an unregistered kind (other), and thekind="none"series for a membership lock, on both metrics.kindless_observer_rejects_replacement_locks: thedebug_assertguard panics.membership_list_lock_records_the_roster_kind(Postgres): a 13534 list write records exactly onemembershipacquisition withkind="13534".group_state_replacement_takes_its_lock_once(Postgres): now counts acquisitions only under the written kind. It expectskind="39000"for the group-state snapshot path andkind="0"forreplace_addressable_event, so iftake_replacement_lockdrops or swaps the kind, the count becomes 0 and the test fails.observability_source.rs: the narrowed guard described above.Commands run:
cargo clippy --workspace --all-targets -- -D warningscargo test -p buzz-core --libcargo test -p buzz-db --libcargo test -p buzz-db --test observability_sourcescripts/postgres-test-run.sh -p buzz-db --lib --tests(the CI PostgreSQL lane, run locally)Local relay check
The relay ran from this branch against an isolated database.
/metricsshowed one series per locked kind after each write:users set-profile:replacement,kind="0"users set-status:replacement,kind="30315"channels create:replacement,kind="39000","39001","39002"channels add-member:replacement,kind="39002"agents archive:replacement,kind="13535"moderation ban:replacement,kind="30622", plusmembership,kind="none"e2e_relay::test_invite_mint_and_claim_admits_new_pubkey):membership,kind="13534"deletionandpartition_maintenance,kind="none"buzz_db_advisory_lock_wait_seconds_countmatchedbuzz_db_advisory_lock_acquisitions_totalfor every series. The author also tested it against the local relay.buzz-review-completed
🤖 Generated with Claude Code