Outbox.Idempotency (Outbox v0.1.0-beta.2)

Copy Markdown View Source

Exactly-once execution guard for at-least-once delivery.

Subscribers receive each event at least once, so a retried job can run handle_event/3 more than once. Wrap the side-effect in run_once/3 to make duplicate deliveries a no-op:

def handle_event(name, payload, meta) do
  Outbox.Idempotency.run_once(__MODULE__, meta.event_id, fn ->
    Repo.insert(%AuditLog{action: name, ...})
  end)
end

The guard claims (consumer, event_id) in the outbox_consumed_events table and runs fun only if the claim was newly inserted. The claim and fun run in one transaction: if fun returns {:error, _} or raises, the claim is rolled back so the next delivery re-runs it. This requires the outbox_consumed_events table (see mix outbox.gen.migration).

Summary

Functions

Run fun at most once per (consumer, event_id).

Functions

run_once(consumer, event_id, fun)

@spec run_once(String.t() | module(), String.t(), (-> any())) :: any()

Run fun at most once per (consumer, event_id).

Returns:

  • {:ok, :already_processed} — this (consumer, event_id) was claimed by a prior successful run; fun is not called.
  • whatever fun returns (:ok, {:ok, value}) — on the first run.
  • {:error, reason}fun returned an error; the claim is rolled back so a retry re-runs it.

A raise inside fun propagates (after rolling back the claim).