Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/modules/emails/supervisor.ex
defmodule PhoenixKit.Modules.Emails.Supervisor do
@moduledoc """
Supervisor for PhoenixKit email tracking system.
This module manages all processes necessary for email tracking:
- Kicks off Oban-based SQS and Brevo polling at boot (see
`SQSPollingManager` / `BrevoPollingManager`) when the corresponding
settings say the chain should be alive
- Registers the unified email provider
- Additional processes (metrics, archiving, etc.)
## Integration into Parent Application
Add supervisor to your application's supervision tree:
# In lib/your_app/application.ex
def start(_type, _args) do
children = [
# ... your other processes
# PhoenixKit Email Tracking
PhoenixKit.Modules.Emails.Supervisor
]
opts = [strategy: :one_for_one, name: YourApp.Supervisor]
Supervisor.start_link(children, opts)
end
## Configuration
Supervisor automatically reads settings from PhoenixKit Settings:
- `sqs_polling_enabled` / SES events / queue URL — SQS chain boot gate
- `brevo_events_enabled` — Brevo chain boot gate
- `email_enabled` — system switch (both chains)
- polling intervals and other provider settings
## Process Management
Both pollers are driven entirely by Oban and can be toggled at runtime
without an application restart:
PhoenixKit.Modules.Emails.SQSPollingManager.enable_polling()
PhoenixKit.Modules.Emails.SQSPollingManager.disable_polling()
PhoenixKit.Modules.Emails.BrevoPollingManager.enable_polling()
PhoenixKit.Modules.Emails.BrevoPollingManager.disable_polling()
Boot re-inserts each enabled chain via `enable_polling/0` so a dead
chain (no queued job after a crash or bad deploy) self-heals when the
setting is still on. If a next tick is already scheduled, the managers'
unique/replace insert moves it to run now rather than appending a
second row.
## Monitoring
Supervisor provides information about process state:
# Get list of child processes
Supervisor.which_children(PhoenixKit.Modules.Emails.Supervisor)
# Get process count
Supervisor.count_children(PhoenixKit.Modules.Emails.Supervisor)
"""
use Supervisor
require Logger
alias PhoenixKit.Modules.Emails
alias PhoenixKit.Modules.Emails.BrevoPollingManager
alias PhoenixKit.Modules.Emails.SQSPollingManager
@doc """
Starts supervisor for email tracking system.
## Options
- `:name` - supervisor process name (defaults to `__MODULE__`)
## Examples
{:ok, pid} = PhoenixKit.Modules.Emails.Supervisor.start_link()
"""
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
Supervisor.start_link(__MODULE__, opts, name: name)
end
@doc false
def init(_opts) do
# Register unified email provider before starting children
alias PhoenixKit.Modules.Emails.ApplicationIntegration
ApplicationIntegration.register()
# Polling runs as Oban jobs (see build_oban_starter/0 + the managers),
# so the supervisor itself has no long-running polling child of its own.
#
# Emails.aws_ses_credentials/0's cache — relies on `PhoenixKit.Cache.Registry`
# already being started, which core's own `PhoenixKit.Supervisor` starts
# before any module's children (see core's build_children/1). Missing in
# a standalone context (e.g. this package's own test suite, which never
# boots core's supervision tree at all) is fine: `PhoenixKit.Cache.get/put`
# already no-op gracefully when their target instance isn't registered.
children = [
Supervisor.child_spec(
{PhoenixKit.Cache, name: :emails_aws_credentials, ttl: 60_000},
id: :emails_aws_credentials_cache
)
| build_oban_starter()
]
# Use :one_for_one strategy - if one process crashes,
# only that one is restarted
Supervisor.init(children, strategy: :one_for_one)
end
@doc """
Returns information about email tracking system status.
## Examples
iex> PhoenixKit.Modules.Emails.Supervisor.system_status()
%{
supervisor_running: true,
polling_status: %{enabled: true, pending_jobs: 1, ...},
brevo_polling_status: %{enabled: true, pending_jobs: 1, ...},
children_count: 0
}
"""
def system_status(supervisor \\ __MODULE__) do
child_count = Supervisor.count_children(supervisor)
polling_status =
try do
SQSPollingManager.status()
catch
_, _ -> %{error: "polling_status_unavailable"}
end
brevo_polling_status =
try do
BrevoPollingManager.status()
catch
_, _ -> %{error: "polling_status_unavailable"}
end
%{
supervisor_running: true,
polling_status: polling_status,
brevo_polling_status: brevo_polling_status,
children_count: child_count.active
}
catch
_, _ ->
%{
supervisor_running: false,
error: "supervisor_not_accessible"
}
end
## --- Helper Functions for Integration ---
@doc """
Returns child spec for integration into parent supervisor.
This function is used when you want more precise control
over email tracking integration in your application.
## Examples
# In lib/your_app/application.ex
def start(_type, _args) do
children = [
# ... other processes
PhoenixKit.Modules.Emails.Supervisor.child_spec([])
]
Supervisor.start_link(children, strategy: :one_for_one)
end
"""
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :supervisor,
restart: :permanent,
shutdown: :infinity
}
end
## --- Boot gates (public @doc false for unit tests) ---
@doc false
# Whether boot should re-seed the SQS self-scheduling chain. Requires the
# system switch, SES events, the SQS polling toggle, and a queue URL —
# without a queue URL enable would only insert a job that immediately
# backs off on misconfig.
def should_start_sqs_polling? do
Emails.enabled?() and
Emails.ses_events_enabled?() and
Emails.sqs_polling_enabled?() and
has_sqs_configuration?()
end
@doc false
# Whether boot should re-seed the Brevo self-scheduling chain. Mirrors
# SQS's role: if the toggle is on across a restart, the chain must come
# back even when no Oban row survived (crash before schedule_next_poll,
# wiped jobs table, etc.). Zero active Brevo profiles is fine — the job
# no-ops each cycle and still records last_polled_at (see
# BrevoPollingJob), same as a live chain with no senders.
def should_start_brevo_polling? do
Emails.enabled?() and Emails.brevo_events_enabled?()
end
## --- Private Functions ---
# Checks for minimum SQS configuration
defp has_sqs_configuration? do
sqs_config = Emails.get_sqs_config()
not is_nil(sqs_config.queue_url) and
sqs_config.queue_url != ""
end
# One-off supervised Task that waits for Oban, then re-seeds every chain
# whose settings say it should be alive. Empty list when neither poller
# is enabled at boot (toggles still work later via the managers).
defp build_oban_starter do
if should_start_sqs_polling?() or should_start_brevo_polling?() do
[
{Task, fn -> start_polling_when_oban_ready() end}
]
else
[]
end
end
defp start_polling_when_oban_ready do
case wait_for_oban(10, 500) do
:ok ->
maybe_start_sqs_polling()
maybe_start_brevo_polling()
:timeout ->
Logger.warning(
"Email Supervisor: Oban not available after timeout, skipping poller bootstrap"
)
end
end
defp maybe_start_sqs_polling do
if should_start_sqs_polling?() do
Logger.info("Email Supervisor: Starting initial SQS polling job via Oban")
case SQSPollingManager.enable_polling() do
{:ok, job} ->
Logger.info("Email Supervisor: Initial SQS polling started", %{job: inspect(job)})
{:error, reason} ->
Logger.warning("Email Supervisor: Failed to start initial SQS polling job", %{
reason: inspect(reason)
})
end
end
end
defp maybe_start_brevo_polling do
if should_start_brevo_polling?() do
Logger.info("Email Supervisor: Starting initial Brevo polling job via Oban")
case BrevoPollingManager.enable_polling() do
{:ok, job} ->
Logger.info("Email Supervisor: Initial Brevo polling started", %{job: inspect(job)})
{:error, reason} ->
Logger.warning("Email Supervisor: Failed to start initial Brevo polling job", %{
reason: inspect(reason)
})
end
end
end
defp wait_for_oban(0, _delay), do: :timeout
defp wait_for_oban(attempts, delay) do
case Oban.Registry.config(Oban) do
%Oban.Config{} -> :ok
end
catch
_, _ ->
Process.sleep(delay)
wait_for_oban(attempts - 1, delay)
end
end