Mailglass.Webhook.Reconciler (Mailglass v1.0.0)

Copy Markdown View Source

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):

  1. Mailglass.Events.Reconciler.find_orphans/1 returns the candidate batch (tenant-scoped, age-bounded, newest max_age_minutes only)
  2. Mailglass.Events.Reconciler.attempt_link/1 looks up the Delivery via (provider, provider_message_id) from the orphan's metadata
  3. On match: append a NEW :reconciled event (D-18 — append, never UPDATE the orphan row; preserves the SQLSTATE 45A01 append-only invariant) + call Projector.update_projections/2 for the matched Delivery + post-commit broadcast on the Projector PubSub topic (Phase 3 D-04)
  4. 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

available?()

(since 0.1.0)
@spec available?() :: true

Returns true when the Oban worker entrypoint is compiled.

reconcile(tenant_id \\ nil, limit \\ 1000)

@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.