AshDispatch.Workers.Stranded (AshDispatch v0.6.7)
View SourceWhat to do with a receipt that is stuck in :scheduled.
:scheduled means "a job is coming". Nothing has ever checked whether one
actually is. A receipt whose job died, was pruned, or never got enqueued
sits there forever: the retry sweep only queries status == :failed, so it
never looks, and no surface counts it. One production deployment carried
17 such receipts across six months — four order confirmations (two of
them to a customer, not staff), four reseller applications and nine product
announcements. Every one of them showed a 429 from the mail provider, was
moved to :scheduled by a retry, and was then never seen again.
The decision has three outcomes, and the middle one is the whole reason the other two need care:
:leave— younger than the grace period.:scheduledis a legitimate transient state; a receipt enqueued a minute ago is not stranded, it is working. Acting here would fight the normal path and could double-send.:rescue— past the grace period and still worth sending. Moved to:failed, which hands it to machinery that already exists: the retry sweep picks up:failed, re-enqueues it, andSendEmaileventually marks it:failed_permanentif the attempts run out. No new terminal state, no second send path.:close— past the staleness ceiling. NOT sent. An order confirmation for an order completed in January, delivered in August, is worse than silence: the recipient has to work out whether something went wrong. Marked:failed_permanentwith a reason so the books stop lying, since an invisible debt is the actual defect here — not the unsent mail.
Both bounds are configurable, and the ceiling is deliberately generous. An app that genuinely wants old mail delivered can raise it; the default refuses to make that choice on someone's behalf.
Summary
Functions
Decides the fate of one stranded receipt.
The reason written onto a receipt that is closed rather than sent.
Default staleness ceiling in hours, past which a receipt is closed instead of sent.
Default grace period in minutes, before a :scheduled receipt counts as stranded.
Types
Functions
@spec action( DateTime.t() | NaiveDateTime.t(), keyword() ) :: action()
Decides the fate of one stranded receipt.
age_reference is when the receipt last moved — updated_at where the
resource keeps one, otherwise inserted_at. Using the last movement rather
than creation matters: a receipt that was retried an hour ago is an hour
stranded, not however old its first attempt was.
Both bounds are in the same unit the config uses (minutes and hours), and are converted here so callers never have to.
@spec close_reason( DateTime.t() | NaiveDateTime.t(), keyword() ) :: String.t()
The reason written onto a receipt that is closed rather than sent.
Spelled out on the record because six months from now the only thing left is this string, and "failed" alone would send someone back to the logs.
@spec default_stale_after_hours() :: pos_integer()
Default staleness ceiling in hours, past which a receipt is closed instead of sent.
@spec default_stuck_after_minutes() :: pos_integer()
Default grace period in minutes, before a :scheduled receipt counts as stranded.