Executor

class Executor(kernel: Kernel, laneCapacity: Int = DEFAULT_LANE_CAPACITY, maxBatch: Int = DEFAULT_MAX_BATCH, views: SharedStates? = null, limit: InFlightLimit = InFlightLimit(), clock: Clock = Clock.systemUTC(), fatals: NodeFatals = NodeFatals.NONE)

Runs commands through kernel, one at a time per aggregate and in arrival order, without one aggregate ever waiting for another (step 3).

Each aggregate with commands in flight has a lane: a queue of its commands in arrival order, run by one of the callers' coroutines at a time, the lane's leader, so a decide function suspended on I/O holds up only its own aggregate. A lane exists only while it has commands; at most laneCapacity may wait on one, and the next is refused as busy rather than queued without bound.

The leader runs its own command, the head of the queue, and when others queue behind it, up to maxBatch of them with it as one batch (Kernel.executeBatch): decided in turn and committed with one append, so a hot aggregate commits a batch per append instead of a command. It never waits to fill a batch: an idle aggregate's command runs alone, as Kernel.execute runs it. The commands the batch did not finish (BatchOutcome.Again) go back to the head of the queue, in order, and the leader hands the lane to the command at the head, which leads the next batch in its own coroutine: a caller waits for its own command, never for others'. A maxBatch of 1 runs every command alone.

The node bounds its commands in flight as a whole too: every executor of a node shares one InFlightLimit, counted when a command enters, so a store that slows down (rather than fails) turns the commands beyond it into wow.command.busy (429, with Retry-After) instead of an unbounded queue spread over many lanes. A command that waits in its lane with a WaitContext whose deadline passes before a batch takes it is withdrawn: it never enters the kernel (CommandWithdrawnException), so the caller's timed-out wait is not followed by an execution nobody waits for, and a retry with the same request id runs it afresh.

close stops taking commands at once (a late command fails instead of waiting on a lane nobody will run) and returns when the commands already taken have finished, or its timeout passed.

A fatal error (NodeFatals.isFatal: out of memory, say) a batch meets is recorded as the node's (fatals) and fails the batch's leader; the node then hands its partitions off. A StackOverflowError fails the batch as any error does.

When views are given, each commit's shared state is recorded in them before the lane moves on to the aggregate's next batch, while nothing can change the state: the context's state consumers (the snapshot subscription, the subscriptions of annotated processors) read it. It is taken just before the append (SharedStates.expect), since the log may hand the commit to this node's subscriptions before the kernel returns, and recorded once the append committed. A view is only an optimisation, so failing to take one never fails the committed command: the failure is logged, and the consumers rebuild the state instead.

Constructors

Link copied to clipboard
constructor(kernel: Kernel, laneCapacity: Int = DEFAULT_LANE_CAPACITY, maxBatch: Int = DEFAULT_MAX_BATCH, views: SharedStates? = null, limit: InFlightLimit = InFlightLimit(), clock: Clock = Clock.systemUTC(), fatals: NodeFatals = NodeFatals.NONE)

Types

Link copied to clipboard

Properties

Link copied to clipboard

The aggregates with commands in flight.

Link copied to clipboard

The commands taken and not finished, waiting in their lanes or in the kernel.

Functions

Link copied to clipboard
suspend fun close(timeout: Duration): Boolean

Refuses new commands from now on, and returns once the commands in flight have finished: true, or false when timeout passed first, logging the lanes still busy.

Link copied to clipboard
suspend fun execute(model: AggregateModel<*>, command: Command<*>, wait: WaitContext? = null): CommandOutcome

Runs command through the kernel in its aggregate's lane; wait is passed on to Kernel.execute.