Reconcile orphan webhook events against committed deliveries.
An orphan webhook event is one inserted by Mailglass.Webhook.Ingest
with delivery_id: nil + needs_reconciliation: true because the
matching mailglass_deliveries row had not yet committed when the
webhook arrived (a real race in production for low-latency
providers).
The canonical reconcile/2 function is compiled in every install so
operators can run the same maintenance sweep manually even when Oban is
absent. When Oban is present, this module also exposes an Oban worker
entrypoint via perform/1 so adopters can schedule background runs.
For each orphan older than 60 seconds (grace window for late commits):
Mailglass.Events.Reconciler.find_orphans/1returns the candidate batch (tenant-scoped, age-bounded, newestmax_age_minutesonly)Mailglass.Events.Reconciler.attempt_link/1looks up the Delivery via(provider, provider_message_id)from the orphan's metadata- On match: append a NEW
:reconciledevent (D-18 — append, never UPDATE the orphan row; preserves the SQLSTATE 45A01 append-only invariant) + callProjector.update_projections/2for the matched Delivery + post-commit broadcast on the Projector PubSub topic (Phase 3 D-04) - On no-match: leave the orphan row untouched; next sweep retries
After 7 days (max_age_minutes: 7 * 24 * 60), find_orphans/1 filters
the row out of the scan (admin LiveView shows it as "older than 7 days
— unlikely to reconcile" per D-19).
Optional-dep gating
available?/0 reports whether the Oban worker entrypoint is compiled.
Mailglass.Application uses that to decide whether to warn about
missing scheduled background workers. The manual CLI path still calls
reconcile/2 directly in both modes.
Concurrency
unique: [period: 60] dedupes overlapping cron runs when Oban is
present. concurrency: 1 is the implicit default for
:mailglass_reconcile queue; adopters who raise it accept the
reconciliation race (Ecto optimistic locking on the Delivery row still
makes the final write correct, but duplicate :reconciled events
become possible if two workers see the same orphan — the partial
UNIQUE index on idempotency_key ("reconciled:#{orphan.id}")
structurally prevents this anyway).
Summary
Functions
Returns true when the Oban worker entrypoint is compiled.
Run the reconciliation sweep for the given tenant (or all tenants
when tenant_id is nil).
Functions
@spec available?() :: true
Returns true when the Oban worker entrypoint is compiled.
@spec reconcile(String.t() | nil, pos_integer()) :: {:ok, %{scanned: non_neg_integer(), linked: non_neg_integer()}}
Run the reconciliation sweep for the given tenant (or all tenants
when tenant_id is nil).
Returns {:ok, %{scanned: n, linked: m}} on success.
Exposed as a public function so mix mailglass.reconcile can invoke
the same code path; also useful in tests and for ops engineers who
want to run a sweep out-of-band without waiting for the next cron tick.