C++ Actor Per-Language Interface¶
C++ per-language interface table of contents · Actor Model · Spot · Actor Membership
Session–Actor binding §8.2 owns relocation route updates for a bound Session.
1. Identity And Maintenance Policy¶
The declaration of actor_context_t, which an Actor uses, is
owned by the Spot interface.
namespace zlink::framework {
class actor_id_t final {
public:
explicit actor_id_t(std::string value);
std::string_view value() const noexcept;
auto operator<=>(const actor_id_t &) const = default;
};
class actor_ref_t final {
public:
actor_ref_t(actor_id_t actor_id,
std::uint64_t object_generation,
std::string mesh_name,
node_rid_t node_rid);
const actor_id_t &actor_id() const noexcept;
std::uint64_t object_generation() const noexcept;
std::string_view mesh_name() const noexcept;
const node_rid_t &node_rid() const noexcept;
};
struct actor_join_accepted_t {
std::uint64_t operation_id_high;
std::uint64_t operation_id_low;
actor_ref_t actor;
std::optional<message_t> reply;
};
struct actor_join_rejected_t {
std::uint64_t operation_id_high;
std::uint64_t operation_id_low;
std::optional<message_t> reply;
};
struct actor_join_failed_t {
std::uint64_t operation_id_high;
std::uint64_t operation_id_low;
framework_error_kind_t error_kind;
};
using actor_join_completion_t = std::variant<
actor_join_accepted_t,
actor_join_rejected_t,
actor_join_failed_t>;
class actor_t {
public:
virtual ~actor_t() = default;
virtual actor_context_t &context() noexcept = 0;
virtual const actor_context_t &context() const noexcept = 0;
virtual void configure() {}
virtual task_t<void> on_join_completed(
const actor_join_completion_t &completion);
};
template <typename TActor>
requires std::derived_from<TActor, actor_t>
class actor_factory_t {
public:
virtual ~actor_factory_t() = default;
virtual task_t<std::shared_ptr<TActor>> create(
actor_context_t context,
std::stop_token operation_cancellation) = 0;
};
template <typename TActor>
class actor_relocation_adapter_t {
public:
virtual ~actor_relocation_adapter_t() = default;
virtual task_t<std::vector<std::byte>> capture(
TActor &actor,
std::stop_token operation_cancellation) = 0;
virtual task_t<void> restore(
TActor &actor,
std::vector<std::byte> payload,
std::stop_token operation_cancellation) = 0;
};
template <typename TActor>
class actor_factory_builder_t {
public:
void disable_relocation();
void recreate_on_relocation();
template <typename TAdapter>
requires std::derived_from<TAdapter, actor_relocation_adapter_t<TActor>>
void preserve_state_with();
};
} // namespace zlink::framework
actor_id_t is a UTF-8 1..255-byte global identity. The
constructor rejects an invalid value with std::invalid_argument and
doesn't apply trim, case folding, or Unicode normalization.
actor_ref_t is an immutable location snapshot holding the global
ActorId, a non-zero 1..9223372036854775807 ObjectGeneration, and the
MeshName/NodeRid at lookup time. It isn't used as an ordinary message
target. A separate actor_ref_snapshot_t isn't provided.
actor_t is a typed lifecycle base that owns the actor_context_t the
Framework wired in. ActorId and ObjectGeneration are read from
context(), and a separate identity value isn't stored independently.
The Framework builds the concrete Actor with
actor_factory_t<TActor>::create(...) and then calls configure().
The factory only uses the context and cancellation it received,
and doesn't take ActorId, a different owner RID, relocation phase, or
Store token as a duplicate input.
Join completion's 128-bit operation ID is a completion idempotency ID
— it isn't RelocationId, a reservation ID, or an aggregate commit ID.
Same-node and cross-node completion retry is bounded to the current
source and target process lifetime. After process exit, a different
runtime doesn't automatically replay the completion.
Every Actor factory configure callback selects exactly one policy.
preserve_state_with<TAdapter>()'s TAdapter must implement
actor_relocation_adapter_t<TActor>, and a different adapter type
fails as a configuration error before socket bind. The adapter
exchanges application state only as an opaque byte vector, and doesn't
receive typed state, a separate contract identifier, a message
wrapper, authority, relocation reference, relocation phase, or
operation ID.
The Framework only calls the adapter in
preserve_state_with<TAdapter>()'s cross-node Actor materialization.
This includes maintenance handoff, remote User/Entry Spot join, and
each Actor participant of a whole User Spot relocation. It doesn't
call the adapter on a same-node join/relocation, and a
DisableRelocation cross-node operation is rejected before
capture(...). A RecreateOnRelocation policy also doesn't
capture/restore the application payload. A whole User Spot relocation
uses spot_relocation_adapter_t<TSpot> for the Spot root, and this
Actor adapter for each Actor participant.
capture(...)'s result has no relocation-adapter-specific size cap, and
an empty vector is valid. The Framework doesn't record the payload in
the Relocation Store; it splits the payload in source memory into
chunks of at most relocation_payload_chunk_limit_bytes and sends
them directly over the source–target ordered mesh connection.
Ownership of the returned byte vector moves to the Framework, and the
byte vector passed to restore(...) is owned by that async call. If
capture throws or ends as a failed task, admission is
restored after durable abort and source normalization. A failed
restore's instance is discarded, and the same immutable payload is
applied to the instance the new attempt's factory built. If the
Framework cancels a callback due to the operation deadline, it's
classified as deadline_exceeded. Because a retry within the same
source and target process can call either method more than once, the
implementation must be retry-safe. A different target isn't
automatically selected. The Framework doesn't guarantee exactly-once
for the adapter's external side effect.
2. ID-Only Messaging¶
namespace zlink::framework {
class actor_send_call_t {
public:
actor_send_call_t &metadata(std::string key, std::string value);
task_t<void> async();
};
class actor_request_call_t {
public:
actor_request_call_t &timeout(std::chrono::milliseconds timeout);
actor_request_call_t &metadata(std::string key, std::string value);
template <typename TReply>
task_t<TReply> async();
template <typename TReply>
task_t<TReply> yield();
task_t<message_t> async_message();
task_t<message_t> yield_message();
};
class actor_client_t {
public:
virtual ~actor_client_t() = default;
template <typename TMessage>
actor_send_call_t send(actor_id_t actor_id, TMessage message);
template <typename TRequest>
actor_request_call_t request(actor_id_t actor_id, TRequest request);
};
} // namespace zlink::framework
Actor send and request only take a global actor_id_t as target. There's
no overload that takes MeshName,
ActorRef, owner NodeRid, or the
current SpotId. The runtime only caches a positive Ready route and
doesn't keep a negative cache. A missing route is distinguished as
not_found, and an -ref generation mismatch as
invalid_operation.
3. Single-Use Manager Operation¶
namespace zlink::framework {
struct actor_create_existing_t {
actor_ref_t actor;
};
struct actor_create_created_t {
actor_ref_t actor;
std::optional<message_t> reply;
};
struct actor_create_rejected_t {
std::optional<message_t> reply;
};
using actor_create_result_t = std::variant<
actor_create_existing_t,
actor_create_created_t,
actor_create_rejected_t>;
class actor_create_call_t {
public:
actor_create_call_t(actor_create_call_t &&) noexcept;
actor_create_call_t &operator=(actor_create_call_t &&) noexcept;
actor_create_call_t(const actor_create_call_t &) = delete;
actor_create_call_t &operator=(const actor_create_call_t &) = delete;
actor_create_call_t &in_mesh(std::string mesh_name);
actor_create_call_t &creation_request(message_t request);
template <typename TCreation>
actor_create_call_t &creation_request(TCreation request);
actor_create_call_t &timeout(std::chrono::milliseconds timeout);
task_t<actor_create_result_t> async();
task_t<actor_create_result_t> yield();
};
class actor_manager_t {
public:
virtual ~actor_manager_t() = default;
virtual actor_create_call_t create(
actor_id_t actor_id, std::string stable_type) = 0;
virtual actor_create_call_t get_or_create(
actor_id_t actor_id, std::string stable_type) = 0;
virtual task_t<std::optional<actor_ref_t>> find(actor_id_t actor_id) = 0;
virtual task_t<std::optional<spot_ref_t>> find_spot(
actor_id_t actor_id) = 0;
virtual task_t<bool> destroy(actor_ref_t actor) = 0;
};
} // namespace zlink::framework
Actor Model §6.2 owns the single-use rules, duplicate-option handling, and terminal
re-invocation errors of actor_create_call_t returned by create and get_or_create.
When in_mesh is omitted, if there
is exactly one object-role Mesh, it's auto-selected; with zero it's
not_configured, and with more than one it's invalid_operation. An
unknown Mesh is not_found.
Create returns already_exists for an existing identity, and returns
actor_create_created_t or actor_create_rejected_t for a new
attempt. GetOrCreate returns a
Ready Actor of the same stable
type as actor_create_existing_t without a callback. If it's Creating,
it waits for the authority change, and a CAS loser doesn't start a
separate factory or callback. A different operation receives
actor_create_existing_t after Ready, races a new reservation after
cleanup, and doesn't share the preceding application reply. Only a
resend with the same source Node RID/lifecycle generation/OperationId
reads the correlation-free creation-operation-terminal-v1 envelope
and re-encodes the reply with the current correlation/reply route. The
terminal is kept for 5 minutes after the original deadline. A callback
exception isn't a rejected result — it's a typed creation failure. A
different type is type_mismatch.
Deadline applies across
resolve, reservation, factory, and Ready as a whole. Find only
returns a Ready ref and doesn't create one. FindSpot only returns the
Ready spot_ref_t of the current User Spot membership, and returns an
empty optional for Entry
membership or a Missing
Actor. Destroy only changes the ActorRef.
With no matching incarnation it's false, a different generation is
invalid_operation, and while moving it's unavailable.
A public Actor directory and local Actor bind overload aren't
provided.
Actor creation confirms the selected owner MeshNode's Entry Spot membership together inside the Ready barrier. Actor work payload is delivered directly to the Actor queue regardless of membership kind, without going through the Entry Spot callback. The original creation payload and an ordinary message aren't hidden-retried to a different owner or new incarnation. If the caller receives a timeout, cancellation, or moving result, it must explicitly start a new operation.
4. STREAM -Ref Binding¶
namespace zlink::framework {
class session_actor_t {
public:
const actor_ref_t &ref() const noexcept;
task_t<void> relay(const message_t &payload);
task_t<void> relay(std::string packet_name, const message_t &payload);
relay_request_call_t relay_request(const message_t &payload);
relay_request_call_t relay_request(std::string packet_name, const message_t &payload);
task_t<void> notify_disconnected();
};
`relay(...)` is the common contract's one-way push. `relay_request(...)` and the `packet_name`
overloads are C++-only surfaces; the common [Session–Actor binding](../../../04-session/02-session-actor-binding.en.md)
defines no Actor-to-session request. Aligning this surface with the other languages is a 0.18.0 candidate.
class session_actor_manager_t {
public:
std::vector<session_actor_t> bound() const;
std::optional<session_actor_t> find(actor_id_t actor_id) const;
request_call_t<session_actor_t> bind(actor_ref_t actor_ref);
request_call_t<session_actor_t> bind_or_get(actor_ref_t actor_ref);
};
} // namespace zlink::framework
Bind sends a control request only once, to the ActorRef location
the caller submitted. On a stale/moving result, it doesn't look up the
global ActorId again or auto-bind to a fresh incarnation. find(...)
only looks up an Actor already bound to that STREAM session — it isn't
the global Actor directory.
notify_disconnected() is a logical disconnect that doesn't close the
physical connection. It runs the binding's Spot callback at most once,
then commits the binding as a tombstone and removes it after terminal. The
physical STREAM connection and Actor/Spot membership are kept, and no new
public unbind API is provided. Rebind completes as soon as the new identity
becomes current and does not wait for the previous session. The previous
session may notify the client in on_actor_binding_replaced(...). The framework closes the
connection 100 ms after the callback reaches a successful or failed terminal; an empty outbound
queue does not shorten this delay.
Callback or close failure doesn't remove the new binding or restore the old one.
Once Actor relocation commits, session_actor_t::ref() returns a
current location snapshot with the same ActorId/ObjectGeneration and
the target MeshName/NodeRid, and the stored binding route is also
updated at the same point. This same-generation route update isn't a rebind
and doesn't run the disconnect callback. A previous actor_ref_t value the caller
copied and kept doesn't change. The application doesn't call
bind(...) again to learn about relocation.
A one-way push through the current STREAM binding is a connection-bound operation. If there's no valid binding or the connection generation changed, it ends with a session-not-bound or stale result, and the Framework doesn't find a different session and resubmit. Connection close doesn't change the Actor's Spot membership or automatically end the Actor.
5. Public Trace Category¶
The declarations in this document belong to public trace's
actor-relocation category. The common meaning is owned by
Actor Model,
Spot · Actor Membership, and
Session Actor Dispatch.
yield() and yield_message() declared in this document are only
valid while the current Actor handler is running in a SpotWide User
Spot's shared execution gate. If an Entry Spot Actor or a PerActor
User Spot's Actor calls them, they complete with invalid_operation
without submitting the operation or returning the turn. async() can
be used in every Actor execution context.