Oban worker for polling the Brevo transactional-email events API.
Mirrors SQSPollingJob's self-scheduling chain (see that module's
@moduledoc for the general shape/rationale — the self-reschedule
dedup, unique window, and misconfig back-off all apply identically
here) with two differences dictated by Brevo's API instead of SQS's:
- Sender-aware gate: SQS polls unconditionally once enabled; Brevo
only fetches events while at least one enabled
PhoenixKit.Email.SendProfileactually points at a"brevo_api"integration — an idle Brevo integration with no active profile has nothing to correlate events against, so polling it is pure waste (and, unlike SQS's long-poll, would burn Brevo API quota on a timer). The chain still keeps re-scheduling itself whilebrevo_events_enabledstays on even when there are zero active profiles right now — a profile added later must not require a manual re-trigger to be picked up. - No message ack step: SQS deletes each message after processing to
avoid redelivery. Brevo's events endpoint is a plain paginated report
with no consumption side-effect — the SAME date window (yesterday +
today) is re-fetched every cycle. Re-processing an already-seen event
is safe and cheap:
Event.create_event/1's partial unique indexes (seeEvent.changeset/2) make it a no-op, not a duplicate row.
Multi-integration
More than one Brevo Integrations connection can exist (e.g. two
distinct Brevo accounts, each with its own SendProfile). Each cycle
polls every distinct integration referenced by an enabled Brevo
profile — not once per profile, since profiles may share an
integration and its API key. If any integration in the cycle turns
out to be misconfigured (missing/invalid credentials), the whole
cycle's next poll backs off to @misconfig_backoff_ms rather than the
configured interval — same rationale as SQSPollingJob, and a
deliberate choice, not an oversight: a per-integration backoff would
need per-integration scheduling state this self-scheduling chain
doesn't have, and a broken integration is expected to be rare/
transient enough that briefly slowing the whole cycle is an acceptable
trade for the simplicity of one shared interval.
Per-cycle event cap
Each integration is capped at @max_pages_per_integration (10) pages
of @default_page_limit (2500, overridable — see page_limit/0)
events per cycle — at most 25,000 events per integration per poll. A
sender busy enough to exceed that in one polling_interval_ms window
will lag (the cap logs a warning and picks up the remainder next
cycle) rather than the poll cycle growing unbounded. Not expected to
matter at typical volumes; flagged here because it's silent otherwise.
Oban queue configuration
Requires a :brevo_polling Oban queue in the host app's config,
same as :sqs_polling already does:
config :your_app, Oban,
queues: [
brevo_polling: 1 # concurrency MUST stay 1 — the self-scheduling
# chain assumes only one cycle runs at a time;
# see SQSPollingJob's queue config docs for why
]
Summary
Functions
@spec cancel_scheduled() :: {:ok, non_neg_integer()}
Cancels all scheduled Brevo polling jobs. Called when polling is disabled to immediately clean up pending jobs.
@spec worker_name() :: String.t()
Returns the Oban worker column value for this job. Single source of
truth for callers that query Oban.Job by worker name (e.g.
BrevoPollingManager).