# `PhoenixKit.Modules.Emails.EventTrackerReconciler`
[🔗](https://github.com/BeamLabEU/phoenix_kit_emails/blob/0.4.2/lib/phoenix_kit/modules/emails/event_tracker_reconciler.ex#L1)

Stateless reconcile — the single code path that starts/stops a
tracker's self-scheduling Oban chain, enforcing
`EventTracker.should_run?/1`. See spec §4.2/§4.3.

**Not a GenServer** (decision §9.1): the correctness backbone is the
periodic reconcile Cron (`EventTrackerReconcileWorker`), not a
long-lived orchestrator process. `reconcile/0`/`reconcile_tracker/1`
are plain functions, safe to call from anywhere (boot, a settings
toggle, the Cron tick, a future admin panel) — idempotent and
cluster-safe:

- "ensure exactly one chain" = an Oban `unique` insert — a genuinely
  dead chain gets a fresh immediate job (`schedule_in: 0`); a chain
  that's already alive (`:available`/`:scheduled`/`:executing`) hits
  the unique conflict and is left completely untouched, its own
  `scheduled_at` unchanged (no `replace:` — see `ensure_chain/1`'s own
  comment for why touching it would silently override the operator's
  own polling interval). Enforced at the DATABASE by Oban's own
  uniqueness, so two nodes reconciling simultaneously cannot create
  two chains (spec §8a).
- "ensure none" = `Oban.cancel_all_jobs/1` scoped to `available`/
  `scheduled` only (never `executing` — a running cycle is never
  interrupted; it dies on its own next time it checks `should_run?/0`
  and doesn't self-reschedule, same mechanism `SQSPollingJob`/
  `BrevoPollingJob` already rely on for the disable path — see #21).

A duplicate/racing insert at worst costs one extra no-op cycle
(`should_run?/0`'s per-cycle gate inside the job itself is the real
safety net) — never two live chains.

# `reconcile`

```elixir
@spec reconcile() :: [{module(), term()}]
```

Reconcile every registered tracker. Returns a list of
`{tracker, result}` pairs, `result` being whatever
`reconcile_tracker/1` returns for that tracker — never raises on a
single tracker's failure (one broken tracker must not stop the rest
from reconciling).

# `reconcile_tracker`

```elixir
@spec reconcile_tracker(module()) ::
  {:ok, Oban.Job.t() | :not_running} | {:error, term()}
```

Reconcile a single tracker against `EventTracker.should_run?/1`.

- `should_run? == true` → `{:ok, %Oban.Job{}}` (the chain's current job
  — the existing one, untouched, if the chain is already alive; a
  freshly inserted one, running immediately, if it wasn't).
- `should_run? == false` → `{:ok, :not_running}` (any queued job for
  this tracker's worker was cancelled; a still-`executing` one is left
  to finish and die on its own).
- `{:error, reason}` on an insert/cancel failure — logged, never
  raised, so a single tracker's transient DB hiccup during a Cron tick
  doesn't take down the rest.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
