1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! Post-commit fan-out of already-persisted [`AuditEvent`]s to the
//! runtime's installed [`cratestack_core::AuditSink`] (cratestack#473).
use AuditEvent;
use crateSqlxRuntime;
/// Fan a batch of already-committed [`AuditEvent`]s out to the
/// runtime's installed [`cratestack_core::AuditSink`].
///
/// `pub`, not `pub(crate)` (cratestack#534): every `run()` call site in
/// this crate calls it internally, right after its own `tx.commit()`
/// succeeds — that usage is unaffected. What's new is that a caller
/// composing `run_in_tx` writes across a transaction *they* own can
/// now call this too, once *their* commit succeeds, passing the
/// `AuditEvent`s each call's [`super::RunInTxOutcome`] handed back.
/// Nothing about the dispatch itself changed: it is still a plain
/// sequential fan-out with no DB I/O of its own (see below) — only who
/// is allowed to invoke it changed.
///
/// **Deliberately called after `tx.commit()`, never before or from
/// inside the transaction.** Two reasons:
///
/// 1. **No double-write of the source of truth.** The only DB write is
/// [`super::enqueue_audit_event`], run once, in-transaction, before
/// this is ever reached. This function performs no DB I/O — it only
/// invokes `AuditSink::record`, an out-of-band, best-effort
/// projection. There is exactly one write to `cratestack_audit` per
/// event either way; this cannot cause a second one.
/// 2. **The transaction must not wait on downstream I/O.** A sink can
/// be a Kafka publish, a Redis command, or an HTTP webhook — any of
/// which can be slow or hang. Running that call while still holding
/// the mutation's row locks would turn an unrelated outage (the
/// Kafka broker is down) into a long-held Postgres lock, which is
/// far worse than a late or dropped downstream projection. Waiting
/// for commit also guarantees a sink is only ever invoked for
/// events that actually happened — a rolled-back transaction never
/// reaches this call, so the sink can't observe a mutation the
/// database itself discarded.
///
/// Errors are logged, not propagated: by the time this runs the
/// mutation already committed, so failing the caller's request over a
/// downstream sink hiccup would be strictly worse than a best-effort
/// delivery. This mirrors `run()`'s existing `let _ =
/// self.runtime.drain_event_outbox().await;` treatment of its own
/// post-commit, best-effort fan-out.
///
/// **Still not called from any `run_in_tx` variant — that remains a
/// deliberate omission, not an oversight (cratestack#534).** `run_in_tx`
/// hands the transaction back to the caller uncommitted, so this crate
/// has no reliable "after commit" point of its own to run at — same
/// reason `run_in_tx` never drains the event outbox on its own either
/// (see `crate::query::write::create`'s doc comment, and
/// [`crate::SqlxRuntime::drain_event_outbox`] for that mechanism's own
/// equivalent, now-public opt-in). What changed is that a `run_in_tx`
/// caller now genuinely *can* opt in: every `run_in_tx` variant returns
/// a [`super::RunInTxOutcome`] carrying the `AuditEvent`(s) it built and
/// already persisted, and this function is `pub` so the caller can pass
/// them straight through — after their own `tx.commit()` succeeds, never
/// before. The generated `Cratestack::dispatch_audit_sink` method is the
/// ergonomic surface for that; this free function is what it forwards
/// to. Skipping the call (or forgetting it) means exactly what option
/// (c) in cratestack#534 describes: `cratestack_audit` still gets the
/// row, but the installed `AuditSink` observes nothing for that
/// transaction — silently, same as before this fix, just opt-out now
/// instead of impossible-to-opt-into.
/// `crates/cratestack-pg/tests/banking_chained_audit_tx.rs` is the shape
/// this closes: two `run_in_tx` writes chained in one caller-managed
/// transaction, both audited, both now observable by a sink the caller
/// dispatches to once after their single `tx.commit()`.
///
/// **Dispatch is sequential, not concurrent**, and that amplifies with
/// batch size: the `for` loop below `.await`s each `AuditSink::record`
/// call one at a time, so an `update_many`/`delete_many`/`batch_*` call
/// touching N rows makes N sequential post-commit sink calls before the
/// response returns — a slow sink's added latency is per-row, not
/// per-request. Deliberately not parallelised here: concurrent
/// dispatch's ordering guarantees and per-event error semantics are a
/// design question of their own, not a cleanup.
pub async