Activation and Lifetime¶
Guide Home | Previous: Backpressure — When Arrival Outpaces Processing | Next: Actor Membership
View in another language — C++ · C#/.NET · Java · Kotlin · Node/TypeScript
What you get from this chapter
You can tell when each of the three kinds of Spot is created, which callbacks it receives, and how long a service injected into it lives. The code in this chapter comes from the TicTacToe sample in the per-language example repositories.
What Spot created was the Spot an application creates explicitly. This chapter covers how the other two kinds differ, which lifecycle callbacks each kind receives, and the injection scope that lasts as long as the Spot.
1. The Kinds of Spot¶
All three carry an id and state and run their callbacks in order. They differ in when they are created, in Actor membership, and in how they are closed.
| Entry Spot | User Spot | Instance Spot | |
|---|---|---|---|
| Created | By the Framework when the Object Server starts | Explicitly by the application through the spot manager | When the first message arrives for that id |
| Spot id | Issued by the Framework | Issued by the Framework on create; named by the caller on get-or-create | Named by the caller as the message's target id |
| Stable type | Not registered | Required | Required |
| Actor membership | Supported. The default place an Actor runs right after creation | Supported. Actors move in and out by join and leave | Not supported |
| Closing from the application | Not offered | Through a close call or from its own context | From its own handler or timer context |
| Where it is used | The default place for an Actor that belongs to no User Spot yet | Rooms, stages, zones | A unit that handles requests per id, such as a matchmaking worker |
An Instance Spot has no create call of its own. Name the instance type on the first message and the Framework selects an existing instance or creates one where it is needed, then handles that same message.
1.1 Instance Spot Single Activation Scope¶
Nodes sharing the location records in one Location Store do not activate the same Instance Spot id in two places at once. The node currently handling the Spot is its owner. The Framework lets only the node that first secures creation rights in the location store run the factory, then handles messages after creation commits. This guarantee relies on those location records and owner eligibility. Deployments using independent Location Stores do not form one serial execution unit. An owner lease, renewed periodically, establishes how long the owner is eligible to accept work.
Single activation does not mean processing continues through every failure. Some operations wait or fail instead of creating a duplicate.
| Situation | Message processing result |
|---|---|
| First messages for the same id arrive at different nodes together | Only the node that secures creation rights creates the Spot. A request at a losing node ends with Unavailable, or retains its timeout if the original deadline has passed. Failure of a one-way send whose outbound admission has completed is recorded in diagnostics. |
| Activation is in progress at the same target | Later operations join that activation. After Ready, they enter the queue in arrival order; activation failure follows their original completion contracts. A call whose resolver finds creation in progress also waits for activation. |
A Ready owner is forcibly stopped or its owner lease is invalid |
A call with new-creation intent encountering an invalid owner lease follows Location runtime §6.1. A Disabled type satisfying the reclamation conditions creates a new incarnation after committed release. Ordinary calls do not reclaim authority. The new incarnation does not inherit previous state or accepted operations. |
| Location Store connection or mutation response is lost | The Framework confirms the mutation result before assuming success; it does not create another node's instance to process the operation. storeFailureGrace, the grace period retaining the existing node list, does not extend owner eligibility. A valid owner can process work until its admission deadline; after that, new message and timer callbacks and state changes are blocked. Completion and cleanup of already accepted work can continue. A request waiting for confirmation retains its original timeout, cancellation, and failure completion conditions. |
| Planned relocation is in progress | The source finishes the current callback and stops starting new callbacks. Pending work and new messages are transferred and held at the target, which starts execution after restore and owner change commit. Depending on the failure boundary, the source is preserved or the operation fails; both nodes do not execute handlers together. Relocation explains the failure results. |
Missing means there is no location record; only this state permits an Instance intent message to
create a new instance. An existing Ready instance becomes Missing after explicit close completes
its location release.
Recovery before Ready can resume within the same target lifecycle and can invoke the factory again
with the same input. A reservation whose target lifecycle ends during creation becomes Missing
after release. External store changes in a factory therefore need to account for repeated calls.
Single activation after manually clearing the location store or losing location and generation records is outside this guarantee. That condition cannot be treated as normal close or owner failure recovery.
1.2 Durable State and Public Generation Values¶
Durable financial state, such as a shared jackpot, also needs protection in the application store. An Instance Spot runs its callbacks serially. It does not put another service's DB writes or external I/O already in progress into one transaction. A jackpot balance update and the record of its business request id belong in the same DB transaction, with a row lock or conditional update protecting other write paths too. After timeout or connection loss, the application queries the previous result or prevents duplicate effects with the same business request id before starting another operation.
The public Instance Spot context exposes ObjectGeneration to distinguish recreation of the
same id.
Read it through ZLinkInstanceSpotContext.objectGeneration().
Relocation moves the same object and does not change ObjectGeneration. This value cannot serve
as a fencing token that distinguishes owner changes. Fencing means the store rejects a mutation
carrying an old write authorization. When needed, the application store issues that authorization
and validates it on every write. The Framework context does not supply an owner-change token.
Ordering against earlier generation values cannot be assumed after location-store generation
records are lost either.
2. Lifecycle Callbacks per Kind¶
The names follow each language; the conditions and the order are the same.
| Callback | Entry | User | Instance | When |
|---|---|---|---|---|
configure |
O | O | O | The configuration step where handlers are registered |
onCreate |
X | O | X | Examines a request to create a new User Spot and decides whether to accept it. Not called when an existing Spot was found |
onInitialize |
O | O | O | Initialization of the created instance. An Instance Spot receives only this, with no onCreate |
onClosing |
O | O | O | Before a still-valid local instance is torn down |
onActorJoin |
X | O※ | X | Accepts or refuses an existing Actor that wants to enter this User Spot |
onCreateActor |
O※ | X | X | Accepts or refuses a new Actor's first Entry Spot membership |
onJoinedActor |
O※ | O※ | X | Tells the receiving Spot that the join commit finished |
onLeaveActor |
O※ | O※ | X | Tells the departing Spot after the commit. It does not mean the Actor is gone |
onDisconnectActor |
O※ | O※ | X | When the connection of an Actor in that Spot drops |
※ Only for a Spot that names an actor type and so supports Actor membership.
Membership callbacks are split between the departing Spot and the receiving Spot. So when an
Actor in a User Spot returns to its Entry Spot, the Entry Spot's onCreateActor and
onActorJoin are not called — returning to the Entry Spot is the default membership and has no
admission step. In both directions only the receiving side's onJoinedActor and the departing
side's onLeaveActor run after the commit.
Relocation is not an arrival or a departure
When relocation restores an Actor into another node's Entry Spot these callbacks are not called. Membership is left as it is and only the execution site moves — covered by Relocation.
3. Entry Spot — Created by the Framework¶
There is one per Object Server. It accepts or refuses Actor creation requests and handles the lifecycle of Actors arriving and leaving.
class PlayEntrySpot(
override val context: ZLinkEntrySpotContext,
private val settings: SampleSettings,
) : ZLinkSuspendingEntrySpot<PlayActor>() {
private val milestoneObservers = mutableListOf<PlayActor>()
init {
// send: JoinGameMsg를 받고 join 완료 뒤 current session으로 결과를 push한다.
context.handlers().addHandler<PlayActorJoinGameHandler>()
// request: ObserveMilestoneReq에 ObserveMilestoneRes로 응답한다.
context.handlers().addHandler<PlayActorObserveMilestoneHandler>()
// subscribe: PlayerWinMilestoneEvent를 받아 observer session에 알린다.
context.handlers().addHandler<PlayerWinMilestoneEventHandler>()
}
override suspend fun onCreateActorSuspending(
actor: PlayActor,
createRequest: ZLinkMessage,
): ZLinkActorCreateResponse {
if (createRequest.isEmpty) {
return ZLinkActorCreateResponse.accept()
}
val request = createRequest.decode<PlayerActorCreateReq>()
actor.applyPlayer(request.player)
return ZLinkActorCreateResponse.accept()
}
override suspend fun onJoinedActorSuspending(actor: PlayActor) {
if (actor.destroyAfterEntrySpotJoin) {
context.destroyActor(actor).await()
logger.info("tictactoe-lifecycle actor-destroy-complete actor={}", actor.actorId)
}
}
override suspend fun onLeaveActorSuspending(actor: PlayActor) {
milestoneObservers.removeIf { it.actorId == actor.actorId }
}
override suspend fun onDisconnectActorSuspending(actor: PlayActor) {
actor.markDisconnected()
milestoneObservers.removeIf { it.actorId == actor.actorId }
}
fun observeMilestone(actor: PlayActor): ObserveMilestoneRes {
rememberObserver(actor)
return ObserveMilestoneRes(true)
}
suspend fun notifyMilestone(event: PlayerWinMilestoneEvent) {
val payload =
WinMilestoneNotify(
roomId = event.roomId,
actorId = event.actorId,
displayName = event.displayName,
wins = event.wins,
)
milestoneObservers.toList().forEach { observer ->
try {
observer.context().boundSession().kotlin().send(payload).await()
} catch (_: RuntimeException) {
// A stale observer cannot prevent later best-effort notifications.
}
}
}
private fun rememberObserver(actor: PlayActor) {
milestoneObservers.removeIf { it.actorId == actor.actorId }
milestoneObservers += actor
}
private companion object {
val logger = LoggerFactory.getLogger(PlayEntrySpot::class.java)
}
}
This code shows that the Entry Spot provides the lifecycle callbacks needed to admit and destroy Actors.
3.1 No Per-Actor State in the Entry Spot¶
Keep no per-Actor state in an Entry Spot. An Actor's state belongs to the Actor, and the Entry Spot provides only handlers and membership callbacks. There is one Entry Spot per Object Server, so per-Actor values accumulated here grow with the number of Actors that Object Server serves.
3.2 Actor Creation and Destruction Happen in the Entry Spot¶
The Entry Spot accepts or refuses Actor creation requests. Its context also provides the call that destroys an Actor. An Actor in a User Spot first returns to the Entry Spot before it is destroyed; Actor Membership handles that move.
The destroy call takes the current instance and does not run membership callbacks again. It clears the Framework's registration record and the bound session route.
4. User Spot — Created by the Application¶
It is created by naming a stable type, and the id that comes back is the address for every later call. Refuse in the create callback and the call fails, leaving no Spot behind.
val created =
kotlinSpots
.create("tictactoe.game") // 이 stable type을 등록한 node가 후보가 된다.
.inMesh(SampleNames.SpotMesh) // Spot을 만들 mesh를 고른다.
.request(TicTacToeGameCreateReq(gameName, SampleNames.RequiredLevel))
.timeout(SampleNames.RequestTimeout)
.await() // Kotlin의 비동기 완료 terminal이다.
The id returned by this call is the address for later calls to the User Spot.
5. The Lifetime of an Injected Service¶
When the Framework activates a Spot it creates one injection scope and resolves the dependencies of the Spot itself and of its handlers from that scope. The scope is torn down when the Spot closes or moves to another node. So a service registered as scoped is one instance for as long as that Spot lives. That differs from one created per HTTP request.
Actor handlers use a separate Actor activation scope. Different Actors share neither handlers nor scoped dependencies. When an Actor leaves, is destroyed, or moves, the source scope is torn down and a new one is created at the destination.
Injecting an ORM context into the constructor of a Spot or a Spot handler causes trouble. A room that lives for hours keeps that context alive for hours.
| Symptom | What happens |
|---|---|
| Growing memory | The change tracker keeps tracking every entity it has read |
| Stale reads | Reading the same key again returns the tracked earlier instance |
| A stuck error state | A failed save leaves the context soiled for the rest of the Spot's life |
Registering the handler type as transient or singleton does not change the lifetime the Framework sets, because the Framework creates the handler and resolves only its dependencies from the activation scope.
The first choice is not to reach a store from a Spot at all. Saving and reading are requested from a service that owns a channel handler, and the Spot owns only in-memory state and execution order. A channel handler has a scope per dispatch, so it may take an ORM in its constructor — the handler in Channel Messaging is that place.
6. Related Documents¶
- State objects called by id — Spot · Actor
- What runs together — The Execution Model
- The rules for moving an Actor between Spots — Actor Membership
- Moving the execution site — Relocation