Skip to main content

Deferred publishing

Defer events until a flush​

DeferredEventPublisher holds the events published to it and passes them to another EventPublisher when it is flushed:

use Dirthara\Events\DeferredEventPublisher;

$deferred = new DeferredEventPublisher($publisher);

$deferred->publish(new UserWasCreated($userId));

// later:
$deferred->flush();

publish() only adds the event to a buffer in memory. Nothing reaches the wrapped publisher, and no listener runs, until flush() is called. Any EventPublisher can be wrapped: a SynchronousEventPublisher, a publisher from another package, or another DeferredEventPublisher.

DeferredEventPublisher implements Dirthara\Events\Contract\FlushableEventPublisher, an EventPublisher with a flush() method. Code that only publishes events depends on EventPublisher; the code that decides when the buffer is emptied depends on FlushableEventPublisher.

Deferred is not asynchronous​

Deferring changes when an event is handled, not where. Flushing into a SynchronousEventPublisher runs every listener inside the flush() call, in the same process, and a slow listener delays whatever called flush().

OperationWhat happens
dispatch()The listeners run now, and the caller may rely on their effects.
publish() on SynchronousEventPublisherA one-way notification, handled now by dispatching it.
publish() on DeferredEventPublisherThe event is buffered and handled later in the same process, when it is flushed.
publish() on an asynchronous publisherAnother package hands the event to a queue, to be handled in another process.

This package provides the first three. See asynchronous publishing.

Flush order​

flush() publishes the buffered events one at a time, in the order they were published, and returns once the buffer is empty. An event published twice is published twice. Flushing an empty buffer does nothing, and so does flushing again straight after a flush that succeeded.

An event published to the same DeferredEventPublisher while it is flushing, for example by a listener, joins the end of the buffer and is published before flush() returns:

$deferred->publish($a);
$deferred->publish($b);

// a listener for $a publishes $c to $deferred

$deferred->flush();
// publishes $a, $b, $c

A call to flush() made while the same publisher is already flushing returns straight away. The flush in progress publishes everything that is left.

When a flush fails​

An event leaves the buffer only once the wrapped publisher has returned from publishing it. When the wrapped publisher throws, flush() stops at once and the exception leaves flush() unchanged:

EventDuring the failed flushAfterwards
1PublishedRemoved from the buffer
2Threw an exceptionStill buffered
3Not attemptedStill buffered
4Not attemptedStill buffered

The next flush() starts again with event 2, followed by events 3 and 4 and anything published in between.

caution

A flush that fails is not a delivery guarantee. The wrapped publisher may have done part of its work before it threw: with a SynchronousEventPublisher, the listeners before the one that threw have already run, and run again when event 2 is retried. Listeners that must not repeat an effect have to guard against it themselves.

Events are kept as they are​

The buffer holds the event objects themselves. They are not cloned, serialised, or copied, and the object passed to publish() is the one the wrapped publisher later receives. A change made to a mutable event between publish() and flush() is therefore visible when it is flushed:

$event = new ProfileWasUpdated();
$event->changes = ['name'];

$deferred->publish($event);

$event->changes[] = 'email';

$deferred->flush();
// the listeners see ['name', 'email']

This is intended, and one more reason to publish immutable events.

Deciding when to flush​

DeferredEventPublisher does not flush itself. It knows nothing about requests, responses, commands, jobs, or the end of the process, and events still in the buffer when it is discarded are never published.

Whatever owns the unit of work calls flush() once that work is done:

handle the work
↓
produce the result
↓
finish the output that belongs to this kind of work
↓
flush the deferred events

A web runtime can flush after it has sent a response, a console runtime after a command has finished, and a worker after each job. This package leaves that decision to them.