Skip to content

05. Actor relocation

Reference index

This category covers the external entry points actor_manager_t/actor_client_t provide, the entry point for joining a Spot from inside Actor code via actor_context_t, and relocation policy selection. The exact signatures are owned by the Actor exact interface (Korean-only).


actor_manager_t::create

Always creates a new Actor.

zlink::framework::actor_create_result_t created = co_await actor_manager
  .create(zlink::framework::actor_id_t{"player-1"}, "player")
  .in_mesh("play")
  .creation_request(spawn_player_t{"player-1"})
  .submit();

Options. This call carries the following modifiers.

Modifier Default Meaning
.in_mesh(mesh_name) Optional if exactly one Mesh has Object Client/Server role The Mesh to create the Actor in. Omitting it with two or more candidates completes with invalid_operation; none completes with not_configured; a nonexistent specified Mesh completes with not_found
.creation_request(message_t) / .creation_request<TCreation>(TCreation) None (empty request) The request passed at Actor factory creation time
.timeout(milliseconds) 5 seconds The deadline covering resolve/reservation/factory/Ready barrier altogether
.submit() terminal (pick one) Waits until creation completes
.yield() terminal (pick one) Only valid inside a spot_wide handler

Completion result. actor_create_result_t (a std::variant) completes as one of actor_create_created_t (newly created) or actor_create_rejected_t (the factory rejected it). If a Ready incarnation of the same ActorId already exists, it completes with an already_exists error rather than either alternative — actor_create_existing_t only exists for get_or_create. If a Ready incarnation exists but its stable type differs, it is type_mismatch.

When to use. Use this when a new Actor is always needed. Use get_or_create to reuse an existing one and only create when there is none.


actor_manager_t::get_or_create

Returns the Ready Actor with the same ActorId if it exists, and creates a new one otherwise.

zlink::framework::actor_create_result_t existing_or_created = co_await actor_manager
  .get_or_create(zlink::framework::actor_id_t{"player-1"}, "player")
  .in_mesh("play")
  .creation_request(spawn_player_t{"player-1"})
  .submit();

Options. The same as create.in_mesh(...), .creation_request(...), .timeout(...), terminal .submit() or .yield().

Completion result. actor_create_existing_t returns the already-existing Actor and ignores creation_request. Contending with a creating attempt waits for that result and joins it; a distinct operation receives actor_create_existing_t after Ready and does not share the earlier reply.

When to use. Use this when an idempotent "use if it exists, create if it doesn't" by ActorId is needed.


find / find_spot / destroy (manager)

Queries an existing Actor, queries the Spot it currently participates in, or terminates the exact incarnation.

std::optional<zlink::framework::actor_ref_t> actor =
  co_await actor_manager.find(zlink::framework::actor_id_t{"player-1"});
std::optional<zlink::framework::spot_ref_t> spot =
  co_await actor_manager.find_spot(zlink::framework::actor_id_t{"player-1"});

if (actor) {
    bool destroyed = co_await actor_manager.destroy(*actor);
}

Options. None of the three calls has modifiers — all only take the target identifier.

Completion result. find returns std::nullopt if there is no Ready Actor. find_spot returns std::nullopt if there is no current User Spot membership. destroy returns false if the incarnation does not exist, completes with invalid_operation if the generation differs, and unavailable while a pre-commit seal is in progress.

When to use. Use this when you need to check current existence/membership, or explicitly terminate an Actor.


send / request (actor_client_t)

Sends a one-way message, or exchanges a typed request/reply, to a single global ActorId. Used from an external client.

co_await actor_client
  .send(zlink::framework::actor_id_t{"player-1"}, grant_item_t{"sword"})
  .submit();

inventory_t reply = co_await actor_client
  .request(zlink::framework::actor_id_t{"player-1"}, get_inventory_t{})
  .timeout(std::chrono::seconds{3})
  .submit<inventory_t>();

Options. send only has .metadata(...) and terminal .submit(). request additionally has the following.

Modifier Default Meaning
.timeout(milliseconds) The MeshNode's request default timeout The upper bound for waiting on the reply
.submit<TReply>() terminal (pick one) Waits until the reply arrives
.yield<TReply>() terminal (pick one) Only valid inside a spot_wide handler
.submit_message() / .yield_message() terminal (pick one) Receives a raw message_t instead of a typed reply

Completion result. No ActorId completes with not_found. The remaining completion kinds follow the same common rules as the messaging-execution category.

When to use. Use send if no reply is needed, and request if one is.


join_spot / join_entry_spot (inside Actor code)

Joins the current Actor to a User Spot or an Entry Spot. Called via actor_context_t::join_spot(...)/join_entry_spot(...) — unlike other entries, the only terminal here is defer(), not submit/yield.

context_
  .join_spot("room-42", join_room_request_t{"player-1"})
  .timeout(std::chrono::seconds{5})
  .defer();

Options. This call carries the following modifiers.

Modifier Default Meaning
.timeout(milliseconds) 5 seconds A monotonic absolute deadline
.defer() Required terminal A synchronous call with no result. Only registers the join intent and an inactive barrier — it does not start the target lookup immediately

Completion result. defer() itself has no return value. If the current handler ends normally, the barrier activates and executes the Join; if the handler fails, the barrier is discarded. The actual result (accepted/rejected/failed) is delivered asynchronously via the actor_t::on_join_completed(...) callback carrying the same 128-bit operation ID — one of the std::variant alternatives actor_join_accepted_t/actor_join_rejected_t/ actor_join_failed_t.

When to use. Use this to move an Actor to a different Spot, or return it to an Entry Spot. Calling it from an Actor in an Entry Spot or a per_actor User Spot completes with invalid_operation.


Relocation policy selection (at Actor factory registration time)

Choose exactly one, in the configure callback of add_actor_factory<TActor, TFactory>(...) (topology-discovery category).

Policy Behavior on cross-node move When to use
disable_relocation() Rejects the move itself before Capture When this Actor must never be moved to another node
recreate_on_relocation() Recreates the same logical identity via the target factory. Does not restore application state When an Actor may be recreated without state
preserve_state_with<TAdapter>() Moves an opaque byte vector via actor_relocation_adapter_t<TActor>::capture/restore When state must be preserved across the move

Completion result. preserve_state_with's capture(...) result is capped at 64 MiB. Capture/Restore can each be called multiple times within the same relocation, so both callbacks must be retry-safe — they must not depend on an external side effect executing exactly once.

When to use. Which of the three policies you choose determines this Actor type's entire relocation behavior — it is decided once, at factory registration time, and cannot be changed per call afterward.


See the Actor exact interface (Korean-only) for the full rationale.