Outbox
The outbox holds messages to other systems that must not get lost: a mail to a customer, a call to an ERP, a webhook. A message is written in the same transaction as the change. If the change is rolled back, the message is gone as well. If it is saved, the message survives a crash and is delivered later, with retries.
anvil:tick delivers the messages every minute. Schedule it as described in
Scheduled commands, there is no extra worker to run.
Durable event handlers
Section titled “Durable event handlers”The simplest way: let a domain event go through the outbox.
public array $outboxHandlers = [ 'invoice-mail' => \App\Outbox\SendInvoiceMail::class,];
public array $durableEventHandlers = [ 'invoice.sent' => ['invoice-mail'],];use Anvil\Outbox\OutboxException;use Anvil\Outbox\OutboxHandler;use Anvil\Outbox\OutboxMessage;
final class SendInvoiceMail implements OutboxHandler{ public function handle(OutboxMessage $message): ?array { // $message->payload is DomainEvent::toArray(): name, subject, subject_uuid, data, changes … $invoice = (new InvoiceModel())->findByUuid((string) $message->subjectUuid) ?? throw OutboxException::permanent('The invoice has been deleted.');
// ... send the mail
return ['to' => $invoice->email]; }}Unlike a listener of $domainEventListeners, the handler runs later, in the next
tick, and again after an error.
Own messages
Section titled “Own messages”Push a message where you change the data, inside the same transaction:
Transaction::run(db_connect(), static function () use ($order): void { (new OrderModel())->update($order->id, ['state' => 'shipped']);
service('outbox')->push('erp-shipment', ['order' => $order->uuid], 'erp-shipment:' . $order->uuid, label: 'Shipment to the ERP');});The idempotency key is unique per tenant. Pushing the same key again returns the existing message and queues nothing.
Handlers
Section titled “Handlers”- At least once. A message can arrive twice, for example when the process ends
after the call but before the result is saved. Make handlers idempotent, for
example with the message UUID (
$message->uuid) as key at the receiver. - In the tenant of the message, as system. Audit entries name the system.
- Errors.
OutboxException::permanent()fails the message at once.OutboxException::temporary()and every other exception are retried. The text of anOutboxExceptionis shown to administrators, other errors only say “unexpected error” and go to the log. - No order. Messages can overtake each other. Use the time of the event.
- No secrets in the payload. Payloads are stored in plain text and kept for
$outboxDays.
Retries
Section titled “Retries”| Setting | Default | Meaning |
|---|---|---|
$outboxMaxAttempts |
8 | attempts before a message fails |
$outboxBackoffMinutes |
1, 5, 15, 60, 180, 360, 720 | pause before the next attempt; the last value repeats |
$outboxBatch |
50 | messages per tick |
$outboxTimeBudget |
40 | seconds per tick at most |
$outboxDays |
30 | days finished messages are kept (anvil:prune) |
Several servers can run anvil:tick at the same time. A message is claimed before
it is handled, and a claim that is older than five minutes is taken over.
Command line
Section titled “Command line”php spark anvil:outbox # deliver the due messages nowphp spark anvil:outbox --failed # failed messages of all tenantsphp spark anvil:outbox --pending # messages still waitingphp spark anvil:outbox --retry <uuid> # queue a failed or cancelled message againphp spark anvil:outbox --cancel <uuid> # cancel a pending or failed messageRetries, cancellations and failed messages are recorded in the
audit log as outbox.retried, outbox.cancelled and
outbox.failed.