PhoenixKit.Modules.Emails.BrevoPollingJob (phoenix_kit_emails v0.1.14)

Copy Markdown View Source

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.SendProfile actually 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 while brevo_events_enabled stays 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 (see Event.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

Cancels all scheduled Brevo polling jobs. Called when polling is disabled to immediately clean up pending jobs.

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

Functions

cancel_scheduled()

@spec cancel_scheduled() :: {:ok, non_neg_integer()}

Cancels all scheduled Brevo polling jobs. Called when polling is disabled to immediately clean up pending jobs.

worker_name()

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