Consolidate the duplicated JSON codec into `transport` and relocate the iroh edge-transport pieces into `iroh-driver`, dissolving the `mvp-system` transport shim. - `transport`: add a canonical `json_codec::JsonCodec<M>` (re-exported from the crate root) as the single JSON codec for serde message types - `distribution`/`datastream`: drop the per-crate `JsonCodec` copies and the `impl_json_codec!` macro; register SWIM/gossip and publisher messages against the shared `swactor_transport::JsonCodec` - `iroh-driver`: move `driver_pumps` and `endpoint_advertisement` out of `mvp-system/src/transport/`, re-exporting `EndpointAddrMask`/`advertised_endpoint`/`MVP_IROH_ENDPOINT_ADDR_MASK_ENV`, and relocate the endpoint guarantee test to `iroh-driver/tests/endpoint_advertisement.rs` - `mvp-system`: delete the `transport/` module and keep codec aggregation in a new `codecs.rs` (`register_mvp_actor_codecs`) - `mvp-system/node`: shrink `worker_node_runtime.rs` (~260 lines) by adopting the relocated modules and collapsing verbose `emit_stdio_node_event` calls into local `boot()`/`worker_evt()` closures Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
291 lines
11 KiB
Rust
291 lines
11 KiB
Rust
//! Protocol messages for SWIM membership and the standalone gossip frames
|
|
//! (registry, node metadata, directory), plus codec registration.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use swactor::Error;
|
|
use swactor_transport::{CodecRegistry, JsonCodec, NetworkMessage};
|
|
|
|
use crate::node_metadata::NodeMetadataEntry;
|
|
use crate::registry::RegistryEntry;
|
|
use crate::types::{DirectoryEntry, MemberState, NodeId, NodeRecord};
|
|
|
|
// ─── SWIM Protocol Messages ────────────────────────────────────────────────
|
|
|
|
/// SWIM ping — "are you alive?"
|
|
///
|
|
/// Carries piggybacked membership gossip so that SWIM dissemination
|
|
/// propagates cluster state changes on every protocol message.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Ping {
|
|
pub from: NodeId,
|
|
pub sequence: u64,
|
|
#[serde(default)]
|
|
pub piggyback: Vec<u8>,
|
|
}
|
|
|
|
impl NetworkMessage for Ping {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::Ping"
|
|
}
|
|
}
|
|
|
|
/// SWIM ack — "yes, I'm alive"
|
|
///
|
|
/// Carries piggybacked membership gossip (same as Ping).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Ack {
|
|
pub from: NodeId,
|
|
pub sequence: u64,
|
|
#[serde(default)]
|
|
pub piggyback: Vec<u8>,
|
|
}
|
|
|
|
impl NetworkMessage for Ack {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::Ack"
|
|
}
|
|
}
|
|
|
|
/// SWIM indirect ping request — "please ping target on my behalf"
|
|
///
|
|
/// Carries piggybacked membership gossip (same as Ping).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PingReq {
|
|
pub from: NodeId,
|
|
pub target: NodeId,
|
|
pub sequence: u64,
|
|
#[serde(default)]
|
|
pub piggyback: Vec<u8>,
|
|
}
|
|
|
|
impl NetworkMessage for PingReq {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::PingReq"
|
|
}
|
|
}
|
|
|
|
/// SWIM indirect ack — "the target you asked me to ping is alive"
|
|
///
|
|
/// Sent by a relay node back to the original prober after the relay
|
|
/// receives an ack from the indirect-ping target.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct IndirectAck {
|
|
pub target: NodeId,
|
|
pub sequence: u64,
|
|
#[serde(default)]
|
|
pub piggyback: Vec<u8>,
|
|
}
|
|
|
|
impl NetworkMessage for IndirectAck {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::IndirectAck"
|
|
}
|
|
}
|
|
|
|
/// SWIM join request — "I want to join the cluster"
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JoinRequest {
|
|
pub from: NodeId,
|
|
}
|
|
|
|
impl NetworkMessage for JoinRequest {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::JoinRequest"
|
|
}
|
|
}
|
|
|
|
/// SWIM join response — "here's the current member list"
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct JoinResponse {
|
|
pub members: Vec<NodeRecord>,
|
|
}
|
|
|
|
impl NetworkMessage for JoinResponse {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::JoinResponse"
|
|
}
|
|
}
|
|
|
|
// ─── Membership Dissemination ───────────────────────────────────────────────
|
|
|
|
/// A single membership update, piggybacked on protocol messages.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MembershipUpdate {
|
|
pub node_id: NodeId,
|
|
pub state: MemberState,
|
|
pub incarnation: u64,
|
|
}
|
|
|
|
// ─── Standalone gossip (decoupled from the SWIM piggyback) ───────────────────
|
|
|
|
/// Cluster-registry gossip — a batch of name→actor CRDT entries disseminated
|
|
/// independently of SWIM membership.
|
|
///
|
|
/// Replaces the old combined SWIM piggyback (registry entries packed onto
|
|
/// Ping/Ack): once SWIM is membership-only, the registry owns its own wire
|
|
/// frame and its own dissemination cadence.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RegistryGossip {
|
|
pub entries: Vec<RegistryEntry>,
|
|
}
|
|
|
|
impl NetworkMessage for RegistryGossip {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::RegistryGossip"
|
|
}
|
|
}
|
|
|
|
/// Node-metadata gossip — a batch of per-node metadata entries (relay URL,
|
|
/// node name) disseminated independently of SWIM membership.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MetadataGossip {
|
|
pub entries: Vec<NodeMetadataEntry>,
|
|
}
|
|
|
|
impl NetworkMessage for MetadataGossip {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::MetadataGossip"
|
|
}
|
|
}
|
|
|
|
/// Directory gossip — a batch of signed actor→host claims (`DIRECTORY.md` §3),
|
|
/// disseminated independently of SWIM membership exactly like `RegistryGossip` /
|
|
/// `MetadataGossip`. Each `DirectoryEntry` is the spec's `Claim`: it carries its
|
|
/// own `generation` and signature, so merge is self-describing and a forged claim
|
|
/// is rejected on receipt.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DirectoryGossip {
|
|
pub claims: Vec<DirectoryEntry>,
|
|
}
|
|
|
|
impl NetworkMessage for DirectoryGossip {
|
|
fn type_tag() -> &'static str {
|
|
"swactor_dist::DirectoryGossip"
|
|
}
|
|
}
|
|
|
|
/// Build a `CodecRegistry` with all distribution protocol messages registered.
|
|
pub fn distribution_codec_registry() -> CodecRegistry {
|
|
let mut cr = CodecRegistry::new();
|
|
cr.register::<Ping, _>(JsonCodec::<Ping>::default());
|
|
cr.register::<Ack, _>(JsonCodec::<Ack>::default());
|
|
cr.register::<PingReq, _>(JsonCodec::<PingReq>::default());
|
|
// §6.1 / §14.5: `IndirectAck` is folded into the shared registry so all six
|
|
// SWIM message types decode through one uniform path; concrete drivers no
|
|
// longer need to hand-dispatch it by tag.
|
|
cr.register::<IndirectAck, _>(JsonCodec::<IndirectAck>::default());
|
|
cr.register::<JoinRequest, _>(JsonCodec::<JoinRequest>::default());
|
|
cr.register::<JoinResponse, _>(JsonCodec::<JoinResponse>::default());
|
|
cr.register::<RegistryGossip, _>(JsonCodec::<RegistryGossip>::default());
|
|
cr.register::<MetadataGossip, _>(JsonCodec::<MetadataGossip>::default());
|
|
cr.register::<DirectoryGossip, _>(JsonCodec::<DirectoryGossip>::default());
|
|
cr
|
|
}
|
|
|
|
/// Build the `CodecRegistry` for the **actor transport**.
|
|
///
|
|
/// Distinct from [`distribution_codec_registry`] in one key way: here every wire
|
|
/// `type_tag` decodes directly into the matching actor `Incoming` *enum variant*
|
|
/// (e.g. `"swactor_dist::Ping"` → [`SwimIn::Ping`]), so a decoded frame can be
|
|
/// handed straight to the target actor's mailbox via `Runtime::deliver_raw`.
|
|
/// Symmetrically, each actor `Incoming` enum registers a variant-multiplexing
|
|
/// encoder (one Rust type → several wire tags). Local-only control variants
|
|
/// (`Tick`, `Subscribe`, …) deliberately fail to encode — they never cross the
|
|
/// wire.
|
|
///
|
|
/// Mixing these decoders into [`distribution_codec_registry`] would clobber its
|
|
/// `type_tag → concrete-type` decoders, so the actor transport keeps its own.
|
|
///
|
|
/// Grows one actor at a time as the migration lands; today it covers SWIM.
|
|
pub fn actor_codec_registry() -> CodecRegistry {
|
|
use crate::swim::actor::SwimIn;
|
|
|
|
let mut cr = CodecRegistry::new();
|
|
|
|
// ── SWIM: SwimIn ⇄ the six network type_tags ──
|
|
cr.register_encoder::<SwimIn>(|msg| {
|
|
let json = |r: Result<Vec<u8>, serde_json::Error>| {
|
|
r.map_err(|e| Error::from(format!("encode: {e}")))
|
|
};
|
|
let (tag, bytes) = match msg {
|
|
SwimIn::Ping(p) => ("swactor_dist::Ping", json(serde_json::to_vec(p))?),
|
|
SwimIn::Ack(a) => ("swactor_dist::Ack", json(serde_json::to_vec(a))?),
|
|
SwimIn::PingReq(pr) => ("swactor_dist::PingReq", json(serde_json::to_vec(pr))?),
|
|
SwimIn::IndirectAck(ia) => ("swactor_dist::IndirectAck", json(serde_json::to_vec(ia))?),
|
|
SwimIn::JoinRequest(jr) => ("swactor_dist::JoinRequest", json(serde_json::to_vec(jr))?),
|
|
SwimIn::JoinResponse(jr) => {
|
|
("swactor_dist::JoinResponse", json(serde_json::to_vec(jr))?)
|
|
}
|
|
// Local-control variants (§6.2) never leave the node.
|
|
SwimIn::Tick { .. }
|
|
| SwimIn::SendFailed { .. }
|
|
| SwimIn::Join { .. }
|
|
| SwimIn::Leave
|
|
| SwimIn::Subscribe { .. } => {
|
|
return Err(Error::from(
|
|
"SwimIn: local-only control variant is not network-encodable",
|
|
));
|
|
}
|
|
};
|
|
Ok((tag.to_string(), bytes))
|
|
});
|
|
|
|
// A generic free fn (not a closure — closures are monomorphic and we decode
|
|
// into six different inner types).
|
|
fn d<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, Error> {
|
|
serde_json::from_slice(bytes).map_err(|e| Error::from(format!("decode: {e}")))
|
|
}
|
|
cr.register_decoder::<SwimIn>("swactor_dist::Ping", |b| Ok(SwimIn::Ping(d(b)?)));
|
|
cr.register_decoder::<SwimIn>("swactor_dist::Ack", |b| Ok(SwimIn::Ack(d(b)?)));
|
|
cr.register_decoder::<SwimIn>("swactor_dist::PingReq", |b| Ok(SwimIn::PingReq(d(b)?)));
|
|
cr.register_decoder::<SwimIn>("swactor_dist::IndirectAck", |b| {
|
|
Ok(SwimIn::IndirectAck(d(b)?))
|
|
});
|
|
cr.register_decoder::<SwimIn>("swactor_dist::JoinRequest", |b| {
|
|
Ok(SwimIn::JoinRequest(d(b)?))
|
|
});
|
|
cr.register_decoder::<SwimIn>("swactor_dist::JoinResponse", |b| {
|
|
Ok(SwimIn::JoinResponse(d(b)?))
|
|
});
|
|
|
|
// ── Registry: RegistryIn::Gossip ⇄ RegistryGossip frame ──
|
|
use crate::registry_actor::RegistryIn;
|
|
cr.register_encoder::<RegistryIn>(|msg| match msg {
|
|
RegistryIn::Gossip(g) => Ok((
|
|
RegistryGossip::type_tag().to_string(),
|
|
serde_json::to_vec(g).map_err(|e| Error::from(format!("encode: {e}")))?,
|
|
)),
|
|
_ => Err(Error::from("RegistryIn: only Gossip is network-encodable")),
|
|
});
|
|
cr.register_decoder::<RegistryIn>(RegistryGossip::type_tag(), |b| {
|
|
Ok(RegistryIn::Gossip(d(b)?))
|
|
});
|
|
|
|
// ── Metadata: MetadataIn::Gossip ⇄ MetadataGossip frame ──
|
|
use crate::node_metadata_actor::MetadataIn;
|
|
cr.register_encoder::<MetadataIn>(|msg| match msg {
|
|
MetadataIn::Gossip(g) => Ok((
|
|
MetadataGossip::type_tag().to_string(),
|
|
serde_json::to_vec(g).map_err(|e| Error::from(format!("encode: {e}")))?,
|
|
)),
|
|
_ => Err(Error::from("MetadataIn: only Gossip is network-encodable")),
|
|
});
|
|
cr.register_decoder::<MetadataIn>(MetadataGossip::type_tag(), |b| {
|
|
Ok(MetadataIn::Gossip(d(b)?))
|
|
});
|
|
|
|
// ── Directory: DirectoryIn::Gossip ⇄ DirectoryGossip frame ──
|
|
use crate::directory_actor::DirectoryIn;
|
|
cr.register_encoder::<DirectoryIn>(|msg| match msg {
|
|
DirectoryIn::Gossip(g) => Ok((
|
|
DirectoryGossip::type_tag().to_string(),
|
|
serde_json::to_vec(g).map_err(|e| Error::from(format!("encode: {e}")))?,
|
|
)),
|
|
_ => Err(Error::from("DirectoryIn: only Gossip is network-encodable")),
|
|
});
|
|
cr.register_decoder::<DirectoryIn>(DirectoryGossip::type_tag(), |b| {
|
|
Ok(DirectoryIn::Gossip(d(b)?))
|
|
});
|
|
|
|
cr
|
|
}
|