The Execution Model¶
Guide Home | Previous: Monitoring | Next: Backpressure — When Arrival Outpaces Processing
View in another language — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
What you get from this chapter
You can tell what runs together inside one Spot, what queues up behind it, and what decides that boundary. The code comes from the Bingo sample README in the examples repository; follow its Download, Build, and Run sections to reproduce the execution-model example.
Spot and Actor summarized it as "work addressed to it is handled one at a time." This chapter covers how far that sentence holds — which work shares one line, which work is split, and what decides the boundary.
1. The Queues Work Waits In¶
Work arriving at a Spot waits in two queues. Packets addressed to the Spot itself and timers go into the Spot queue; payloads addressed to an Actor that belongs to the Spot go into the Actor queue.
A message addressed to an Actor does not pass through the Spot queue. No Spot callback receives it and hands it on; it enters the Actor queue from the start.
| Queue | Goes in | Does not go in |
|---|---|---|
| Spot queue | Payloads addressed to the Spot, matching Logical Multicast payloads, timer callbacks, Actor join and leave, lifecycle callbacks | Actor business payloads |
| An Instance Spot's queue | Payloads addressed to the Spot and timer callbacks | Anything about Actors. Rejected at registration |
| Actor queue | Actor business payloads | — |
Registering Actor membership or a Logical Multicast subscription on an Instance Spot is rejected when it is registered or when the Spot is prepared, not while it runs.
2. What Decides the Serialization Boundary¶
Whether work from different queues may run at the same time is decided by the kind of Spot and its execution mode.
| Serialization boundary | State ownership | |
|---|---|---|
| Entry Spot | Serializes the Spot queue and the Actor queue separately. Different queues may run at the same time | Each Actor owns its own. State shared between Actors belongs in an external store |
User Spot, SpotWide (default) |
Serializes Spot handlers, member Actor handlers, timers and lifecycle callbacks through one common gate | The Spot instance owns it. State shared with Actors needs no separate synchronization |
User Spot, PerActor |
Serializes per Actor and per Spot lane. Different lanes may run at the same time | Each Actor owns its own. State shared between lanes belongs in an external store |
| Instance Spot | Serializes the Spot queue's handlers and timers. There is no Actor queue | The Spot instance owns it |
The execution mode is fixed when the factory is registered and is not changed while running.
3. SpotWide — Execution Through One Gate¶
SpotWide is the default. Every callback bound for that Spot — another Actor's message, a timer,
a lifecycle callback — passes one common gate and runs one turn at a time in a single lane.
No two turns run at the same instant, so a handler touches the state of the Spot and of its member Actors directly, in ordinary code, without locks. In relocation the Spot and its Actors move together as one unit. In exchange, one long callback delays every following callback of that Spot.
Being the default, it can be left out; writing it down makes the mode the Spot runs in visible at the registration.
// SPOT_WIDE is the default. Naming it here keeps the choice visible:
// every callback of this room runs through one gate.
.addSpotFactory(
SampleNames.RoomSpotType,
BingoRoomSpot.class,
factory -> {
factory.executionMode(ZLinkUserSpotExecutionMode.SPOT_WIDE);
factory.relocationCoordinationMode(
ZLinkSpotRelocationCoordinationMode.APPLICATION_SIGNALED);
factory.preserveStateWith(BingoRoomRelocationAdapter.class);
})
4. PerActor — One Lane Each¶
PerActor is chosen when throughput requires each Actor to run independently. The Spot itself is
used as a shell that holds no state.
Different lanes run at the same time, so state that several Actors change together, and
schedules kept per Spot, belong in an external store such as Redis or a database. The factory's
relocation policy can only be RecreateOnRelocation.
An Entry Spot follows the same model. Its Spot queue and Actor queue are separate, so it carries the same constraints.
5. Serial Execution and Thread Occupancy¶
Serial execution does not mean holding one thread throughout. When a handler reaches a suspension
point the executing thread handles other work, but the turn is held until the handler
completes. Under SpotWide the next callback of the same Spot does not start in the meantime.
Async and Yield both submit an operation and wait for its application result. await on
Async releases only the thread and keeps the current turn. Use Async only for short waits.
Use Yield when the next turn in the same Spot has to run during a long I/O wait.
Yield is a terminator that ends the current turn and gives back the shared gate that lets one
thing run at a time in the Spot when it starts waiting for a result. While it waits, queued turns
in the same Spot run. When the reply arrives, its continuation gets that gate again and continues
in a new turn at the tail of the queue.
Another turn may have changed state across a Yield, so check again before changing state after
resuming. Copy values handed to the operation inside the current turn before Yield. When a
member Actor in SpotWide uses Yield, it gives back only the shared Spot gate. Its own Actor
queue position remains reserved, so its next message cannot run first, while another Actor, Spot
handler, or timer can run.
Yield is available where there is a shared turn: a SpotWide User Spot and an Instance Spot.
An Entry Spot and a PerActor User Spot have no shared turn and therefore do not offer Yield.
For calls such as requests and workers that offer it, the terminator name is:
| Language | Yield terminator |
|---|---|
| C#/.NET | Yield(...) |
| C++ | .yield() |
| Java | .yield(...) |
| Kotlin | .yield() |
| Node/TypeScript | .yield() |
This is the player-record request in a Bingo room join. In every tab, the marked Yield
terminator line is where the room Spot gives its turn back.
.yield(Messages.GetPlayerRecordRes.class) gives the room Spot turn back.
return context.outbound()
.requestToChannel(
SampleNames.ApiChannel, BingoMessages.getPlayerRecordReq(actor.actorId()))
.timeout(SampleTimings.RequestTimeout)
.yield(Messages.GetPlayerRecordRes.class)
.thenCompose(
record -> {
if (pendingJoins.get(actor.actorId()) != request
|| game == null
|| !game.canAcceptPlayer()) {
pendingJoins.remove(actor.actorId());
return context.leaveActor(actor);
}
pendingJoins.remove(actor.actorId());
join(actor, request, record.getWins(), record.getLosses());
logger.info(
"bingo-record fetched actor={} wins={} losses={}",
actor.actorId(),
record.getWins(),
record.getLosses());
return CompletableFuture.completedFuture((Void) null);
});
6. Related Documents¶
- State objects called by id — Spot · Actor
- Creation points and lifecycle callbacks per kind — Activation and Lifetime
- The contract that yields a turn — this section
- When arrival outpaces processing — Backpressure