Skip to content

09. Object Kind and Activation

Spot And Actor topic index · Spec table of contents · Previous: 08. Spot/Actor Routing · Next: 10. Spot Timer

Spot model owns Spot kinds and shutdown reasons; execution contract §2–§3 owns gates, turns, and Yield; Spot/Actor routing owns generation usage; Failure and failover policy owns outcomes after owner failure. This chapter describes implementation structure satisfying those contracts and lifecycle-boundary violations. It adds or changes no application-observable behavior.

This chapter covers how, in code, the three kinds of Spot — the execution unit holding Actors and handlers — are distinguished, when a missing object is built, and how a message sent to a stale owner is filtered out.

1. Don't Distinguish Kind by a True/False Marker

Spot kind is a closed value set — Invalid = 0, Entry = 1, User = 2, Instance = 3 (Glossary). The three kinds behave differently.

Kind When it's created Move
Entry Spot One per node when the Object Server starts Never
User Spot When creation is explicitly requested Yes
Instance Spot When a call explicitly declaring intent to create first arrives Yes

Yield availability follows execution contract §16.

Represent the three kinds as separate types. Attaching a marker like entry_spot or instance_spot to one type means the type can't prevent a combination in which both are true at once, and rules that differ per kind scatter into conditionals. Rules such as whether return-wait is allowed are also decided at each kind's type boundary.

Defining three sibling types on a common base collects per-kind differences at the type boundary.

Per-language discretion — how to represent it. Each language may use inheritance, composition, or a tagged union. The criterion is "can an impossible combination be constructed."

Internal confirmation condition. Representing the three kinds as separate types so that a value where two kinds are true at once can't be constructed in code is a white-box condition confirmed only by looking directly at the type definitions.

2. What "Entry Spot Doesn't Move" Means

The Entry Spot instance belongs to that Object Server's lifecycle, so it doesn't go into the move-candidate list (Spot Model 「4.2 Entry Spot's Actor Lifecycle」).

A frequent mistake here is to overlook that an Actor that was on the Entry Spot does move. What doesn't move is the Entry Spot itself. If "Actors belonging to an Entry Spot" are excluded wholesale when picking move candidates, those Actors disappear when the node goes down.

3. When to Build a Missing Object

An ordinary message never builds a missing object. Only a Spot-specific call that explicitly declares intent to create can build a new one; an ordinary message and a lookup call only target an already-ready object (Spot/Actor Routing 「2.4 When There's No Object」).

Pass Spec States into the Activation State Machine

The public behavior is defined by Failure And Failover Policy §4.4. The resolver converts its result into one of the closed internal states below. These states include Ready, which is reached once Spot creation/initialization is complete and a record has been written to the Location Store — the store that lets multiple nodes confirm each Spot's current owner and state together — so the Spot can accept application messages. The activation state machine passes that state to exactly one responsible component, so a later stage doesn't infer the Store result again.

Internal state Fence preserved Next component
Missing Lookup version proving authority absence Creation coordinator
Creating Attempt and reservation fence Waiter for the same attempt
Ready Route and authority/owner-lease fences Route admission
Unavailable Authority and invalid-owner evidence Terminal completion adapter

Unavailable means authority remains but the current owner can't be used. It isn't the same state as Missing, which means no authority exists. Only after an explicit Close, IdleEvicted cleanup, or another formal lifecycle operation completes authority release can the resolver produce a new Missing input.

Stored creation intent resumes only an incomplete first cold-activation operation on the same target node and lifecycle. It isn't used for takeover or queue recovery after a steady Ready owner failure.

Without this distinction, one typo builds an object. A message sent with a wrong ID would create a new object for that ID, and no one would ever clean it up.

When Multiple Attempt to Create at Once

If several callers try to build the same object at once, only the caller that secures the creation authority first records itself as owner and creates it. The rest target the already-created object. The factory runs exactly once.

Don't cache the in-progress-creation state. Since "being created" is a state about to change, it isn't put into Spot/Actor Routing 「2.2 The Condition For Using A Recent Ready Route」's positive route cache. Caching it would keep showing "being created" for the cache lifetime even after creation finished.

%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
    participant A as caller A
    participant B as caller B
    participant S as Location Store
    participant F as factory

    A->>S: claims creation authority
    B->>S: claims creation authority
    S-->>A: secured
    S-->>B: already claimed
    A->>F: runs
    Note over B: waits. Not put into cache
    F-->>A: object
    A->>S: records itself as owner · Ready
    S-->>B: the Ready object
    Note over A,B: the factory runs only once<br/>the losing side targets the created object

The Publication Order of the Ready Record and the Target Route

An Instance Spot requires one more step after Ready is recorded in the Location Store. The target runtime must immediately reflect the same route in the local view used for application-message admission. This local view is the instance intent projection. It doesn't replace Store authority; it's a value that lets the process look up an already-validated Ready route.

So the order is as follows.

  1. The target node commits Ready authority to the Location Store.
  2. In the same synchronous continuation in which the commit succeeds, the target runtime registers the instance intent.
  3. After that, the activation continuation enqueues the first application message.

If step 2 is delayed, Ready exists in the Store while the target runtime has no route. The first application message arriving during this gap can end in NotFound or a stale-route error. A later continuation may register the same route again to recover an omission, but that registration must also finish before the first admission. Registering the same route more than once is handled idempotently so it doesn't create duplicate execution.

If the losing side caches "being created," the last two lines of this diagram get delayed by the cache lifetime.

If Creation Fails Midway

If creation fails midway, the activation state machine must define who cleans up the leftover record and when. Without that responsibility, a failed creation permanently occupies that ID.

4. Filtering Out a Message Sent to a Stale Owner

Since owner info is cached, the owner the sender knows may have already changed. The receiving side must filter this out.

The filtering criterion is owner identity and validity period. Not object generation.

ObjectGeneration is not a targeting condition for an ordinary message (Spot/Actor Routing 「2.6 Where ObjectGeneration Is Used And Where It's Not」). Also checking object generation as a condition for ordinary messages would reject every normal message right after an object is recreated. Object generation is used to filter lifecycle changes and move relay.

Checked What it filters
Owner identity This node is no longer owner
Owner validity period It's owner, but past the deadline
Object generation Applies only to lifecycle change and move relay
%%{init: {'flowchart': {'nodeSpacing': 32, 'rankSpacing': 40, 'padding': 8, 'wrappingWidth': 180}, 'themeVariables': {'fontSize': '18px'}}}%%
flowchart TB
    M["message arriving via a stale route"] --> O{"is this node<br/>still owner"}
    O -- "no" --> X["stale-route error<br/>no automatic retry"]
    O -- "yes" --> L{"is owner validity<br/>period remaining"}
    L -- "no" --> X
    L -- "yes" --> G["object generation isn't checked"]
    G --> Q["enqueued"]
    G -. "if generation were checked too" .-> W["every normal message<br/>right after recreation would be rejected"]
    K["lifecycle change<br/>move relay"] --> GEN["object generation is checked only here"]

The left axis is the path an ordinary message goes through. The point where you'd want to add a generation check at G is the pitfall, and the right-side K is the only path where generation is checked.

A filtered message ends in a stale-route error. The runtime doesn't automatically retry. Retrying would let the sender see success while the request may actually have executed twice. The application can start a new call, and at that point the risk of duplicate execution is judged by the application.

5. When to Clean Up an Active Object, and What Bounds It

Idle Cleanup Targets Only Instance Spots

The runtime's internal object catalog is responsible for cleaning up idle-and-unused state. The .NET mapping names this owner ZLinkSpotNodeCatalog. When the configured InstanceSpotIdleTimeout is positive, the catalog periodically checks candidates. Each check examines at most 64, and the last check position carries over to the next cycle, so maintenance work doesn't monopolize application dispatch even with a large number of Spots.

Candidates are limited to Instance Spots. An activation becomes a candidate only if it has no Actor membership, isn't participating in relocation or Message Follow, and has no application work pending. It's accepted as a candidate only after the timeout has elapsed since the last application work finished.

Once a cleanup transaction starts, the catalog merges it with other close requests for the same Spot ID. After reconfirming serial quiescence, it calls the closing callback with reason IdleEvicted, disposes the activation, and releases the Spot location in the Location Store. So no new work is accepted while the callback is running, and the location isn't cleared before the callback finishes.

If an Instance intent request — one carrying the caller's explicit choice to let a new Instance Spot be built when the target Spot is missing — uses the previous route while the location row is still being released, the runtime may invalidate the route and re-read until the close transaction finishes authority release, yielding Missing, or until a current Ready route is confirmed. This isn't a retry that resends an already-accepted application request; it's an owner-route refresh that confirms the result of explicit idle cleanup.

The resolver passes two different tags into the activation state machine: one for completed idle-cleanup authority release and one for a change only in owner-availability evidence. The creation coordinator receives only the former tag, while the latter is wired to the terminal completion adapter. The rule against resubmitting an accepted request is defined by Failure And Failover Policy §2.

Language mappings may name the catalog differently, but each implements the same shutdown conditions and verifies them with independent process evidence. A structural explanation for one language mapping doesn't substitute for verification evidence from another.

The Ceiling Applies to Local Activation, Not Just the Placement Stage

The active-object-count ceiling must be applied at both placement selection and local activation. At placement selection, it excludes a node near the ceiling from candidates; at local activation, it rejects new object creation. If reducing already-created objects and new activation are treated as separate judgments, a request already pointing at that node can bypass the ceiling.

The ceiling is used at two points.

Point What it does
Placement selection Excludes a node near the ceiling from candidates for a new object
Local activation If over the ceiling, rejects activation at that node

Blocking only at the placement stage lets a request already pointing at that node, or an object incoming via a move, simply pass through the ceiling.

What the Cleanup Criterion Is

The cleanup targets are Instance Spots only. The formal spec added IdleEvicted as a shutdown reason limited to Instance Spots (Spot Model). The reason User Spot isn't cleaned up is that an ordinary message doesn't recreate a cleaned-up User Spot. Only a call explicitly declaring Instance intent can build a missing object (§3). Entry Spot belongs to that Object Server's lifecycle, so it's never a target to begin with (§2).

The cleanup criterion must satisfy both "elapsed time since last activity" and "no work currently in progress." Looking at time alone would delete an object with a long-waiting operation still on it.

Framework doesn't preserve application state on cleanup. State that needs to be kept is saved by the application itself directly in the shutdown callback (Spot Model 「6.2 Cleaning Up An Idle Instance Spot」). For Framework to save state on the application's behalf, it would need to know what to save, and that's the application's job.

6. Which Unit Memory Accounting Uses

Process-unit byte accounting and Spot-unit byte accounting are different accounting units. Process-unit byte accounting bounds pending-receive payload in bytes, and Spot-unit byte accounting bounds per-lane work count and bytes together. One side's number isn't reused as the other side's ceiling.

Execution queues keep application and lifecycle work in separate FIFO lanes, each with count and byte reservations. The application-lane defaults are 1,024 items and 64 MiB; the lifecycle-lane defaults are 128 items and 4 MiB. Accepted application work reserves its payload size plus a fixed retained cost of 256 bytes per work item. The reservation is returned at handler terminal completion. The relocation hold has no relocation-specific item-count or byte bound.

So the Spot queue can saturate first even when the process HWM isn't exhausted, and conversely, process inbound admission can stop first even when the Spot queue isn't full. The two results aren't merged into one CapacityExceeded situation — they're distinguished by the queue whose admission actually failed.

The Queue Bound Is Set by Accumulated Payload Size

The execution queue's bound enforces both the count and byte axes, and applies whichever is hit first. The formal spec mandates both axes (Framework API).

A single axis can be circumvented via the other axis. Count alone lets a few large payloads fill memory; bytes alone lets empty payloads pile up indefinitely without hitting the bound.

Byte accounting doesn't count only payload size. It includes the envelope, metadata, and queue node occupied by one pending work item. In a language where this can't be calculated exactly, use a value with a fixed per-work cost added. Even when the payload is empty, one work item isn't 0 bytes.

The queue bound exists for two reasons — deciding how much memory stays tied up, and judging how much work is backed up. Count alone tells you neither.

The same count of 1,024 items is about 100 KB at 100 bytes each, and 1 GiB at 1 MiB each. Memory differs by 10,000x while the bound triggers the same. The time to drain is the same story — throughput tracks bytes per second more closely than items per second, so measuring the backlog requires measuring in bytes.

A count bound is wrong in both directions.

Situation Result of a count bound
Small messages pile up Rejects on hitting the bound despite memory headroom
Large messages pile up Doesn't hit the bound while memory runs out

Process-unit accounting is already in bytes (§6 first paragraph). Applying the same standard at the Spot unit works, and because both layers use the same unit, the layer that triggered the bound can also be distinguished.

Do not use an unbounded execution queue. Each lane must have both count and byte reservations (Framework API).

The result of exceeding a bound varies by submission family and queue location, so an implementation must not lump the results together. The error model §5 owns error selection for Request queues.

Two cases outside the Request queue classification above each result in CapacityExceeded — the worker scheduler queue and batch capacity. The latter is an admission judgment, not queue saturation.

The relocation hold has no relocation-specific count or byte bound. An execution-lane reservation for already owned work and transport, deadline, and cancellation limits aren't reused as a separate relocation-hold ceiling. This is a rule the formal spec specifies, so it is followed as written (Complete Host Relocation Flow 「9. Moving Pending Messages, Timers, And Sessions」).

7. Boundary with Other Topics

Per-object bounded queues are for order guarantees and owner isolation, and don't replace the host-shared queue. Permit and fairness are owned by Receive And Dispatch Loop, and pre-start terminal lease cleanup is owned by Payload Ownership. Admission to the host-shared capacity itself and its backpressure are defined by Application Job Queue And Backpressure.

8. Verification Requirements

Confirm the following using only the explicit creation call, ordinary message send result, relocation candidate list, idle-cleanup closing callback, and execution-queue admission result. Each item maps to one test.

Kind And Creation Target

  • Entry Spot isn't in the relocation candidate list, and an Actor that was on an Entry Spot is included in the relocation candidates.
  • Sending an ordinary message with a missing ID doesn't build the object; only a call that explicitly declares intent to create builds a missing object.

Concurrent Creation

  • Even if several callers request creation of the same object at the same time, the factory runs only once, and the remaining callers target the object it created.
  • A message sent right after creation finishes is processed immediately, not delayed by the cache lifetime.
  • When only the fact that the owner can't be used is confirmed (Unavailable), a new creation isn't started, and that result is delivered only as the terminal completion of the in-progress request.

Stale Owner Filtering

  • Even right after an object is recreated, an ordinary message isn't rejected for a generation mismatch.
  • A call sent to a stale owner ends in an error without automatic retry.

Active-Object Ceiling And Idle Cleanup

  • Once the active-object count reaches the ceiling, new activation is rejected at that node.
  • Cleanup targets are limited to Instance Spot — Entry Spot and User Spot aren't cleaned up.
  • An Instance Spot with work in progress isn't cleaned up even after idle time has passed.
  • When cleaned up, the closing callback is called with shutdown reason IdleEvicted.

Execution Queue Bound

  • The execution queue is bounded on both the count and byte axes, and whichever is hit first applies.
  • When large messages pile up, the byte bound is hit before the count bound.
  • When empty payloads pile up, the count bound is hit before the byte bound.
  • Byte accounting includes a fixed per-work cost, so even an empty payload counts toward the bound.
  • Every execution lane has both a count and a byte bound — there's no unbounded execution queue.

Spot And Actor topic index · Spec table of contents · Previous: 08. Spot/Actor Routing · Next: 10. Spot Timer