crafty_proto/actor.rs
1//! Cross-node actor messaging + directory wire types (cross-node-actors, cluster-routing).
2
3use serde::{Deserialize, Serialize};
4
5use crate::NodeId;
6
7/// A compile-time actor type tag. In v1 this is the Rust type name of the
8/// `UserActor`, which is stable within a build; two nodes running the same
9/// binary agree on it (cross-node-actors).
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11pub struct ActorTypeId(pub String);
12
13/// A globally-unique address for a single actor instance in the cluster
14/// (cross-node-actors). `generation` is bumped on respawn/migration so stale references
15/// are detectable.
16#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct ActorId {
18 /// Node currently hosting the instance.
19 pub node: NodeId,
20 /// Logical group / pool name (e.g. `"workers"`).
21 pub name: String,
22 /// Instance index within the group (`0` for a singleton).
23 pub instance: u32,
24 /// Bumped on respawn / migration to invalidate stale references.
25 pub generation: u64,
26}
27
28/// A directory entry describing one live actor instance, replicated across the
29/// cluster via `/actor/register` (cross-node-actors).
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct ActorRegistration {
32 /// The instance's address.
33 pub id: ActorId,
34 /// The actor's type tag.
35 pub actor_type: ActorTypeId,
36 /// Whether the actor carries migratable state (cross-node-actors migration).
37 pub migratable: bool,
38 /// Instantaneous mailbox depth on the hosting node (Observer / dashboard).
39 #[serde(default)]
40 pub mailbox_depth: u64,
41 /// Seconds since the instance was spawned on the hosting node.
42 #[serde(default)]
43 pub uptime_secs: u64,
44 /// Recent message rate (messages/s) on the hosting node (Observer / dashboard).
45 #[serde(default)]
46 pub messages_per_sec: f64,
47}
48
49impl ActorRegistration {
50 /// Build a directory entry; runtime stats default to zero.
51 #[must_use]
52 pub fn new(id: ActorId, actor_type: ActorTypeId, migratable: bool) -> Self {
53 Self {
54 id,
55 actor_type,
56 migratable,
57 mailbox_depth: 0,
58 uptime_secs: 0,
59 messages_per_sec: 0.0,
60 }
61 }
62}
63
64/// A state-based directory update: node `node`'s **complete** set of local
65/// registrations at monotonic `epoch` (cross-node-actors publish/revoke). Receivers
66/// replace everything they hold for `node`, applying an update only if its
67/// `epoch` is newer — so updates are idempotent and reorder-safe. Publishing an
68/// empty `registrations` revokes all of `node`'s entries (e.g. on leave).
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct DirectoryUpdate {
71 /// The node whose local registrations this snapshot describes.
72 pub node: NodeId,
73 /// Monotonic per-node version; higher supersedes lower.
74 pub epoch: u64,
75 /// The node's full set of local registrations at this epoch.
76 pub registrations: Vec<ActorRegistration>,
77}
78
79/// Acknowledgement for a [`DirectoryUpdate`] delivered to `/actor/register`.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct RegisterAck {
82 /// Whether the update was applied (`false` if it was stale/superseded).
83 pub applied: bool,
84}
85
86/// A reference used to route a message to an actor or actor group.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ActorRef {
89 /// Logical group / pool name (e.g. `"workers"`).
90 pub group: String,
91 /// Optional routing key for consistent-hash routing (cluster-routing); when
92 /// `None`, round-robin routing is used.
93 pub key: Option<String>,
94 /// Pin to a specific node; when `None`, the registry chooses placement.
95 pub node: Option<NodeId>,
96}
97
98/// An actor message crossing a node boundary via `/actor/deliver` (cross-node-actors).
99///
100/// The sender has already resolved the logical target (group + RR/keyed
101/// selection) to a concrete instance `to` via the cluster directory (E7), so
102/// the receiving node delivers straight to that instance.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct ActorEnvelope {
105 /// The concrete destination instance.
106 pub to: ActorId,
107 /// The sending instance, when the message originates from an actor (for
108 /// replies / tracing); `None` for messages sent from outside the fabric.
109 pub from: Option<ActorId>,
110 /// The node that originated this envelope. Combined with [`req_id`](Self::req_id)
111 /// it forms a cluster-unique key the receiver uses to deduplicate an
112 /// at-least-once resend, so a side-effecting `ask` handler runs at most once
113 /// per logical request. `None` disables dedup (legacy / intra-fabric sends).
114 pub origin: Option<NodeId>,
115 /// Per-sender correlation id, used to match a reply to its request and,
116 /// with [`origin`](Self::origin), to deduplicate a resend.
117 pub req_id: u64,
118 /// Application-encoded (`postcard`) message body.
119 pub payload: Vec<u8>,
120 /// Whether the sender awaits a reply (`ask`) versus fire-and-forget
121 /// (`cast`). When `true` the receiver decodes via
122 /// `UserActor::decode_ask` and returns the reply in [`DeliverAck::reply`].
123 pub reply_expected: bool,
124}
125
126/// Acknowledgement for an [`ActorEnvelope`] delivered to `/actor/deliver`.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct DeliverAck {
129 /// Whether the message reached a live local mailbox.
130 pub delivered: bool,
131 /// A human-readable reason when `delivered` is `false` (unknown group,
132 /// no such instance, closed mailbox, not remotely addressable).
133 pub error: Option<String>,
134 /// For an `ask` (`reply_expected`), the application-encoded (`postcard`)
135 /// reply the handler produced; `None` for a fire-and-forget `cast` or when
136 /// delivery failed.
137 pub reply: Option<Vec<u8>>,
138}
139
140/// A request to spawn an actor on a target node's registry (`/actor/spawn`,
141/// cross-node-actors). The target looks up a factory registered for `actor_type`,
142/// decodes `config`, and starts the actor under `name`.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct SpawnRequest {
145 /// The group name to register the actor under.
146 pub name: String,
147 /// The actor's type tag; the target must have a factory registered for it.
148 pub actor_type: ActorTypeId,
149 /// `postcard`-encoded `A::Config`.
150 pub config: Vec<u8>,
151 /// Generation for the new instance (bumped on respawn/migration).
152 pub generation: u64,
153}
154
155/// Reply to a [`SpawnRequest`]: the spawned instance's id, or an error string.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct SpawnReply {
158 /// The spawned instance's id on success.
159 pub id: Option<ActorId>,
160 /// A human-readable reason on failure (unknown type, config decode, name
161 /// collision, start failure).
162 pub error: Option<String>,
163}
164
165/// A request to drive a group to a cluster-wide instance count on the leader
166/// (`/actor/scale`, cross-node-actors, supervisor-leader). Sent when `scale_cluster` is called on a
167/// follower: the leader owns cluster-wide placement, so the follower forwards
168/// the intent (with the committed voter set it observed) rather than planning
169/// locally. The target reconstructs each placement via the `actor_type`
170/// factory, exactly like a [`SpawnRequest`].
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ScaleRequest {
173 /// The group name to scale.
174 pub name: String,
175 /// The actor's type tag; every hosting node must have a factory for it.
176 pub actor_type: ActorTypeId,
177 /// Desired cluster-wide instance count (one worker per node, one-worker-per-vps).
178 pub total: u64,
179 /// `postcard`-encoded `A::Config` used to construct new instances.
180 pub config: Vec<u8>,
181 /// The live/voter node set the requester observed (committed Raft
182 /// membership), against which the plan is computed.
183 pub live_nodes: Vec<NodeId>,
184}
185
186/// Reply to a [`ScaleRequest`]: `None` error on success.
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188pub struct ScaleReply {
189 /// A human-readable reason on failure (planning error or a spawn failure);
190 /// `None` when the scale was applied.
191 pub error: Option<String>,
192}
193
194/// A request to stop a group on a target node (`/actor/stop`, cross-node-actors, supervisor-leader).
195/// Sent by the leader when a scale-down (or reconcile) plans a *removal* on
196/// another node: the one-worker-per-node model (one-worker-per-vps) means "remove on node
197/// N" is "stop this group on node N". The target stops the named group
198/// idempotently.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct StopRequest {
201 /// The group name to stop on the target node.
202 pub name: String,
203}
204
205/// Reply to a [`StopRequest`]: `None` error on success (stopping an absent
206/// group is a success — it is already gone).
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208pub struct StopReply {
209 /// A human-readable reason on failure; `None` when the group was stopped
210 /// (or was already absent).
211 pub error: Option<String>,
212}
213
214/// A request to migrate a stateful actor to a target node (`/actor/migrate`,
215/// cross-node-actors). The departing node captures the instance's migration snapshot,
216/// then asks the target to spawn a replacement under `name` and restore the
217/// snapshot into it before it handles any message.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct MigrateRequest {
220 /// The instance being migrated away (its current address on the source).
221 pub from: ActorId,
222 /// The group name to register the replacement under (usually `from.name`).
223 pub name: String,
224 /// The actor's type tag; the target must have a factory registered for it.
225 pub actor_type: ActorTypeId,
226 /// `postcard`-encoded `A::Config` for constructing the replacement.
227 pub config: Vec<u8>,
228 /// Migration snapshot from [`migration_snapshot`]; empty for a stateless
229 /// actor (the target simply spawns a fresh instance).
230 ///
231 /// [`migration_snapshot`]: https://docs.rs/crafty-actor
232 pub snapshot: Vec<u8>,
233 /// Generation for the replacement instance (bumped past the source's).
234 pub generation: u64,
235}
236
237/// Reply to a [`MigrateRequest`]: the replacement instance's id, or an error.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct MigrateReply {
240 /// The replacement instance's id on success.
241 pub id: Option<ActorId>,
242 /// A human-readable reason on failure (unknown type, config decode, restore
243 /// failure, start failure).
244 pub error: Option<String>,
245}