PhoenixKit.Modules.Emails.EventTrackerReconcileWorker (phoenix_kit_emails v0.2.0)

Copy Markdown View Source

The correctness backbone of the tracker lifecycle (spec §4.3): a low-frequency Oban Cron entry that calls EventTrackerReconciler.reconcile/0 for every registered tracker.

The boot and settings-toggle reconcile calls are latency polish — they make an eligibility/toggle change feel instant. This Cron tick is what makes it correct no matter what: it is the only trigger that can resurrect a chain that died or was never started (e.g. spec §1.1 — a SendProfile change emits no PubSub today, so it's the dominant way a newly-eligible Brevo tracker actually gets picked up without an operator manually re-toggling).

Runs in its own :event_tracker_reconcile queue (not :sqs_polling/ :brevo_polling) — it must never be blocked behind either provider's own chain.

Multi-node

Oban.Plugins.Cron is not node-scoped by default — on a multi-node cluster, every node's Oban instance fires this Cron entry on its own schedule, so a single tick can produce N concurrent perform/1 calls (one per node), each running a full reconcile/0. This is safe and cheap, not a bug to work around: reconcile/0 is idempotent and its "ensure exactly one chain" step is enforced at the database by Oban's own unique (spec §8a) — N simultaneous reconciles collapse to the same single chain regardless. If a host genuinely needs only one node running this Cron entry (e.g. to avoid N redundant reconcile queries on a very large tracker registry), that requires a host-side node filter (e.g. Oban.Plugins.Cron's own node-targeting options, or an application-level leader check) — this worker itself does nothing to enforce single-node execution.

Oban queue + cron configuration

Add to your config/config.exs:

config :your_app, Oban,
  queues: [
    event_tracker_reconcile: 1,
    # ... your other queues
  ],
  plugins: [
    Oban.Plugins.Pruner,
    Oban.Plugins.Lifeline,
    {Oban.Plugins.Cron,
     crontab: [
       {"*/2 * * * *", PhoenixKit.Modules.Emails.EventTrackerReconcileWorker}
     ]}
  ]

Why Oban.Plugins.Lifeline belongs in that list

Reconcile can only resurrect a chain it can see is dead. EventTrackerReconciler.ensure_chain/1's unique covers :executing (deliberately — see its own comment), so a job orphaned in :executing by a node that died mid-cycle is a permanent conflict: reconcile never inserts a successor, and EventTracker.state/1 — which counts :executing too — keeps reporting :active / "Running normally" for a tracker that has in fact stopped polling. Nothing in this package can detect that from the outside; Oban.Plugins.Lifeline is what moves orphaned rows back to :available, at which point the next tick here behaves normally again. Oban.Plugins.Pruner is listed for the same "don't paste a plugins: list that silently drops your existing ones" reason, plus EventTracker.last_polled_at/1's own Pruner caveat.