Skip to main content

Module notification

Module notification 

Source
Expand description

Delivery of NotificationRequested rows: the consumer side of the notification contract.

A hook trigger’s send_email / notify and a workflow’s send_email / notify / send_notification / send_sms never tell anyone anything inside the write’s transaction: they stage a NotificationRequested row in the module’s outbox, which commits with the write. This module is what reads those rows back out:

  • NotificationDelivery is the one trait a notification module (mail, push, SMS) implements: deliver one request, and say whether it was delivered, should be tried again or can never be delivered.
  • NotificationDispatcher is the outbox relay’s sink: it routes NotificationRequested records to the registered delivery, claims each with inbox::once so a redelivered row is not delivered twice, and maps the outcome onto the relay’s own retry and dead-letter semantics.
  • RecordingDelivery is a test double a notification module’s own tests, and the framework’s, drive the dispatcher with.
ⓘ
let dispatcher = Arc::new(NotificationDispatcher::new(pool.clone(), "hr", Arc::new(MailDelivery::new(..))));
// the relay: every staged event goes to the sink; a record that is not a request is accepted and ignored
runner::supervise(pool, RelayConfig::new("hr"), dispatcher.sink(), shutdown);

§Guarantees

At least once, and one effect when the provider honours the id. The claim and the delivery share one transaction: it is opened, the (consumer, event id) is claimed, the delivery runs, and the transaction commits only when the delivery answered Delivered. A redelivery of a delivered row finds the claim and does nothing; two relays handing the same row at once serialize on the claim; a Retry or a Reject rolls the claim back, so the row is offered again or dead. What no database can do is make a mail provider’s send atomic with a commit: a crash after the provider accepted the message and before the commit delivers it again, which is why PendingNotification::id is handed to the delivery as the idempotency key to give the provider.

Correlation and causation are preserved. The delivery runs inside the staging write’s correlation id (so anything it writes or logs carries it), and PendingNotification carries the record’s correlation id and causation id; PendingNotification::id is the id to cite as the causation of anything the delivery stages in turn.

Retry is counted here, not in the table. The relay re-hands a pending row on every pass and keeps no attempt count, so the dispatcher counts Retry results per record and waits between them (RetryPolicy): within the backoff window a record is left pending without calling the delivery, and after RetryPolicy::max_attempts the dispatcher reports the record exhausted, which the relay marks dead (failed_at, failure_reason) and relay::reemit hands back once the cause is fixed. The count and the wait live in this process: a restart forgets them, and each instance of a service counts for itself. A Reject is dead at once, with the delivery’s reason.

Structs§

NotificationDispatcher
The outbox relay’s sink for NotificationRequested rows: see the module docs.
PendingNotification
One staged request, as a delivery sees it.
RecordingDelivery
A NotificationDelivery for tests: records every request it is handed and answers from a script.
RetryPolicy
How many times, and how far apart, a dispatcher offers a request that answers DeliveryOutcome::Retry.

Enums§

DeliveryOutcome
What a delivery says about one request.

Traits§

NotificationDelivery
What a notification module implements: perform one request.

Type Aliases§

SinkFuture
The future a relay sink returns.