On this page
DispatchWorker is the durable at-least-once dispatch loop: polls events past a cursor, matches persistent subscriptions, executes or accumulator-buffers actions, and records completions.
#dispatch.src.orxtra.dispatch._dispatch_worker
#dispatch.src.orxtra.dispatch._dispatch_worker
Dispatcher worker: durable event-processing loop.
Polls the events table via a DispatchBackend, matches events against persistent subscriptions, executes actions through the ActionExecutor protocol, and records per-event-action completion records for at-least-once delivery.
The worker lives in dispatch (orchestration layer) and depends only on protocols -- never on the concrete ServicesActionExecutor. Services wires the concrete implementation; CLI registers the command.
#DispatchWorker
Durable dispatcher worker with at-least-once delivery.
Constructor takes:
- backend: a DispatchBackend with cursor/completion/poll methods
(PgDispatchBackend or InMemoryDispatchBackend)
- action_executor: the ActionExecutor protocol (for WorkflowAction)
- flush_scheduler: the FlushScheduler protocol
- pool: an asyncpg Pool (for LISTEN/NOTIFY)
- cursor_name: identifies this worker's cursor position
- events_channel: the PG NOTIFY channel name
- poll_interval: fallback poll interval in seconds
- batch_size: max events per poll
The main loop:
- Read cursor position (or start from beginning)
- LISTEN on events_channel as a wake hint
- Poll for unprocessed events since cursor
- For each event: match subscriptions, check completion records,
execute action, record completion, advance cursor
- Wait for NOTIFY or poll interval, repeat
#run
async def run(self) -> NoneMain loop: poll, match, execute, advance, wait.
#stop
async def stop(self) -> NoneSignal the main loop to exit cleanly.
#_wait_for_wake_or_stop
async def _wait_for_wake_or_stop(self) -> NoneWait until either wake_event or stop_event is set.
#_setup_listen
async def _setup_listen(self) -> NoneAcquire a dedicated connection and LISTEN on the events channel.
#_teardown_listen
async def _teardown_listen(self) -> NoneRemove the listener and release the connection.
#_on_notify
def _on_notify(self, _connection: asyncpg.Connection[Any] | asyncpg.pool.PoolConnectionProxy[Any], _pid: int, _channel: str, _payload: object) -> NoneNOTIFY callback: set the wake event to trigger an immediate poll.
#_poll_and_process
async def _poll_and_process(self) -> intPoll for new events and process them. Returns count processed.
#_maybe_sweep_expired_inbox
async def _maybe_sweep_expired_inbox(self) -> NoneExpire pending inbox items with past deadlines, at most once per interval.
The min-interval guard ensures this does not fire per-batch under load (the loop continues immediately when events are available).
#_execute_action_immediate
async def _execute_action_immediate(self, sub_action: SubscriptionAction, event_id: UUID, event_type: str, principal_id: UUID | None, data: dict[str, Any] | None, owner_principal_id: UUID) -> NoneExecute a single action immediately (no accumulator).
#_buffer_or_flush
async def _buffer_or_flush(self, sub_action: SubscriptionAction, event_id: UUID, owner_principal_id: UUID) -> NoneBuffer event for accumulator; flush inline if count threshold reached.
- Buffer the event via the backend.
- If count threshold is set and pending >= threshold, claim and execute.
- If time threshold is set, schedule a deferred flush via FlushScheduler.
- Whichever fires first wins (standard accumulator contract).
#_make_flush_callback
def _make_flush_callback(self, sub_action: SubscriptionAction, owner_principal_id: UUID) -> AnyCreate a zero-arg async callback for FlushScheduler.
#_flush_action
async def _flush_action(self, sub_action: SubscriptionAction, owner_principal_id: UUID) -> NoneClaim a batch from the accumulator buffer and execute the action.
#_make_event_fire_callback
def _make_event_fire_callback(self, owner_principal_id: UUID) -> AnyCreate a callback for EventAction dispatch.
A derived event re-fired by an EventAction is attributed to the OWNING SUBSCRIPTION's principal: the subscription whose action triggered the re-fire is the actor behind the new event. Every event the worker processes flows through a subscription, so an owner is always in scope.