Side effects inside a database transaction that the database cannot undo.
Argus.Analyses.Purity checks a contract someone wrote down. This one
checks a contract nobody writes down, because it is imposed by context:
passing a closure to Repo.transaction/1 silently accepts an obligation
that whatever the closure does is something the database can take back.
Three ways that goes wrong, none of them visible in review because the offending call looks completely ordinary:
- Rollback leaves the effect behind. The transaction aborts, the rows vanish, and the webhook has already fired. The system is in a state its own database says never existed.
- Retry repeats it. Serialization failures are retried by design — that is what an isolation level costs. Each retry re-runs the closure, so one logical operation sends two emails.
- The connection is held throughout. A pooled connection stays checked out for the whole closure, so an HTTP call inside a transaction couples database capacity to a third party's latency. A slow dependency stops being a slow feature and becomes pool exhaustion.
The third is what takes systems down and is the least obvious: the code is correct, it just holds a scarce resource while waiting on something it does not control.
Scope
Not every effect — logging is fine inside a transaction and is by far the
most common one there; a clock read needs no undoing. What counts is
effects that escape the database's control, taken from the shared effect
model in Argus.Purity.Effects.
Repos are found by Ecto.Repo behaviour rather than by name, so an app's
own MyApp.Repo is caught without knowing what it is called.