[PM-XXXXX] fix: Stop a failed event write from failing its caller - #8399
withinfocus wants to merge 2 commits into
Conversation
An event write went straight to the caller when it failed, so a saturated Service Bus topic surfaced as a 500 on every request that logs an event, including token issuance. The event was lost either way, so the exception preserved nothing and only cost the request. Every write service except the no-op is now wrapped so a failure is counted and logged instead of thrown. The keyed "persistent" registration the listeners use stays unwrapped, since a listener has to see a failed write to retry the message. The counters replace the signal the 500s were providing. They are collected with no further wiring, because Bitwarden.Server.Sdk already registers the Bitwarden.* meter prefix with the OTLP exporter.
The Events host now opts out. Recording the event is what a request to its collect endpoint is for, and the events it carries have no server-side equivalent, so a dropped write has to reach the client instead of reading as success. The dropped counter gains an event type dimension, so an incident responder can tell whether authentication events were among a batch that was lost. EventType is a closed enum and carries no organization, user, or vault data, so cardinality stays bounded. CreateManyAsync materializes inside the try. A deferred sequence that throws while being enumerated was escaping a service that must not throw, uncounted by either signal.
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Reviewed the On the self-hosted question the description raises: wrapping Code Review DetailsNo code findings. PR Metadata Assessment
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8399 +/- ##
==========================================
+ Coverage 64.61% 64.64% +0.02%
==========================================
Files 2482 2484 +2
Lines 106349 106419 +70
Branches 9659 9663 +4
==========================================
+ Hits 68720 68797 +77
+ Misses 35285 35277 -8
- Partials 2344 2345 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
🎟️ Tracking
Follows https://bitwarden.atlassian.net/wiki/spaces/EN/pages/3406037038/How+Authentication+Is+Coupled+to+Audit+Event+Delivery. Related to #8364, which covered dead letter retention on the integration tier. This one covers the producer side of the event tier.
📔 Objective
A failed event write went straight to its caller. When the
event-loggingtopic hit its quota during incident 044,AzureServiceBusService.PublishEventAsyncthrew, nothing caught it, and/connect/tokenreturned a 500 for every user on every client. The event was lost either way, so the exception preserved nothing and only cost the request.IEventWriteServicenow resolves to aNonThrowingEventWriteServicethat counts and logs a failed write instead of throwing it. One wrapper at the chokepoint covers all 149 event call sites and any added later.Two registrations opt out on purpose:
"persistent"registration the listeners use stays bare.EventRepositoryHandlerhas to see a failure so the message is redelivered rather than silently acked.surfaceWriteFailures: true. Recording the event is what a request to/collectis for, and the events it carries (client vault exports, copied passwords, viewed ciphers) have no server-side equivalent, so returning 200 on a dropped write would lose them. Clients buffer and retry on a 500.RepositoryEventWriteServiceis wrapped, so self-hosted now drops an audit write rather than failing the request. Review flagged this: nothing retries behind that path, and OTLP export is off by default on self-hosted, leaving the error log as the only signal. I kept it wrapped because a self-hosted login should not fail on an Event insert, and the database is already a hard dependency of login, so the practical change is small. Worth a second opinion.Two counters on the
Bitwarden.Eventsmeter replace the signal the 500s were giving us:bitwarden.events.write_failuresper failed operation andbitwarden.events.droppedper event lost. Both carry the exception type, and the dropped counter also carries the event type, so a responder can tell whether authentication events were in a lost batch.EventTypeis a closed enum and carries no organization, user, or vault data.Additional setup
Two things this needs that are not in the diff.
Alerting on the counters. Collection and OTLP export happen automatically, since
Bitwarden.Server.Sdkalready registers theBitwarden.*meter prefix, so nothing needs configuring to get the data flowing. What is missing is an alert. A sustained nonzero rate on either counter means we are dropping audit events. Someone needs to own that alert before this merges, because the PR removes the signal we have now. Today a failed publish shows up as a 500 rate on/connect/token. After this change it shows up nowhere unless the counter is being watched.Service Bus alerting, out of scope here but needed before we can call 044 closed: topic size as a percentage of quota, which would have caught the saturation well before logins started failing, and subscription lag or oldest message age against whatever recovery window we settle on. Neither is a code change.