SquatchMail.Capture.Recorder (SquatchMail v0.1.0)

Copy Markdown View Source

Persists captured emails off the process that sent them.

SquatchMail.Capture's telemetry handler runs synchronously in the caller's process — the same process that just called Mailer.deliver/2 — so it must return immediately. It hands each captured email's attrs to this GenServer with GenServer.cast/2 (never call/2, which would block the caller waiting for a reply) and this process does the actual SquatchMail.Tracker.record_email/1 write, off the caller.

Backpressure

Two independent limits bound the work this GenServer takes on, so a burst of sends can't turn into unbounded memory growth or a flooded database connection pool:

  • SquatchMail.Config.max_queue/0 (default 10_000) bounds how many captures may be waiting (queued in this process's own FIFO queue, not yet handed to a worker). Once the number of queued-plus-in-flight captures reaches this limit, new captures are dropped outright: [:squatch_mail, :capture, :dropped] fires and a warning logs — at most once a minute, so a sustained overload doesn't itself become a logging flood.
  • SquatchMail.Config.max_concurrency/0 (default 50) bounds how many captures are being persisted at once. Each in-flight persist checks out a connection from the host's Repo pool; without this cap, a burst large enough to exceed :max_queue would also be large enough to try to check out thousands of connections simultaneously and starve every other query the host app is running. Workers are unlinked (Task.Supervisor.async_nolink/2) so one crashing persist can't take this GenServer down with it.

Queued work is drained into new workers as running ones finish, so the effective throughput is :max_concurrency persists in flight at any given moment, with up to :max_queue more waiting their turn.

Summary

Functions

Returns a specification to start this module under a supervisor.

Enqueues an email's attrs for persistence.

Starts the recorder. Only one is expected per application (registered under this module's name).

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

record(attrs)

@spec record(map()) :: :ok

Enqueues an email's attrs for persistence.

Non-blocking: returns immediately regardless of whether the attrs were accepted, queued, or dropped for being over capacity.

start_link(opts \\ [])

@spec start_link(Keyword.t()) :: GenServer.on_start()

Starts the recorder. Only one is expected per application (registered under this module's name).