mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Rewrite protocol flow around compiled routing
Compile routing prefixes once per endpoint, restore minimal pending-to-active hook transitions, and route call/data/fault packets from the header before decoding payloads for local delivery only. Document the remaining protocol-level pressure points in src/protocol/PROTOCOL_CHANGES.md.
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# Protocol Change Pressure
|
||||||
|
|
||||||
|
This document records protocol-spec changes that are worth considering after the
|
||||||
|
runtime rewrite in `src/protocol`.
|
||||||
|
|
||||||
|
The current rewrite intentionally keeps the existing wire model from
|
||||||
|
`/home/astatin3/Documents/GitHub/unshell/PROTOCOL.md` wherever possible. The main
|
||||||
|
goal was to remove avoidable runtime work without silently drifting the protocol.
|
||||||
|
|
||||||
|
The implementation now does the following:
|
||||||
|
|
||||||
|
- compiles child routing prefixes once instead of scanning child paths on every packet
|
||||||
|
- routes from the header first, then decodes payloads only on local delivery
|
||||||
|
- keeps pending hook state minimal and active hook state directly indexed
|
||||||
|
- separates local typed send paths from framed transport-facing send paths
|
||||||
|
|
||||||
|
Those are implementation changes. They do not require a protocol update.
|
||||||
|
|
||||||
|
## No Immediate Wire Change Required
|
||||||
|
|
||||||
|
The current runtime rewrite does **not** require a wire-format break.
|
||||||
|
|
||||||
|
The following parts of `PROTOCOL.md` remain worth keeping as-is:
|
||||||
|
|
||||||
|
- path-based routing remains the canonical behavior
|
||||||
|
- pending call context remains distinct from active hook state
|
||||||
|
- `Fault` remains upstream-only
|
||||||
|
- unknown or expired `hook_id` still drops returned traffic
|
||||||
|
- hook closure still requires both sides to send `end_hook = true`, or one `Fault`
|
||||||
|
|
||||||
|
Those rules keep the protocol boring and interoperable.
|
||||||
|
|
||||||
|
## Change 1: Framing That Guarantees Archive Alignment
|
||||||
|
|
||||||
|
### Current problem
|
||||||
|
|
||||||
|
`PROTOCOL.md` Section 8 fixes a framed format with a 4-byte big-endian length
|
||||||
|
prefix before each archived section.
|
||||||
|
|
||||||
|
That is simple, but it has one hard performance downside in the current Rust
|
||||||
|
implementation:
|
||||||
|
|
||||||
|
- the start of the archived section is not guaranteed to satisfy `rkyv` alignment
|
||||||
|
- the decoder therefore has to copy header bytes into an `AlignedVec` before safe access
|
||||||
|
- local payload decode also copies the payload bytes into another `AlignedVec`
|
||||||
|
|
||||||
|
This means the runtime still performs unavoidable memory copies during decode even
|
||||||
|
after the architectural cleanup.
|
||||||
|
|
||||||
|
### Recommended protocol change
|
||||||
|
|
||||||
|
Revise the framing rules so each archived section begins at a guaranteed aligned
|
||||||
|
offset.
|
||||||
|
|
||||||
|
Two viable options:
|
||||||
|
|
||||||
|
1. Add explicit padding after each length field so the archived section begins at
|
||||||
|
the required alignment boundary.
|
||||||
|
2. Replace the current two-section frame with one canonical aligned envelope type
|
||||||
|
whose internal layout already satisfies the archive alignment rules.
|
||||||
|
|
||||||
|
### Why this is objectively better
|
||||||
|
|
||||||
|
- removes the forced alignment-copy step on decode
|
||||||
|
- makes zero-copy or near-zero-copy archived access actually achievable
|
||||||
|
- reduces local delivery latency for all packet types
|
||||||
|
- reduces transient allocation pressure in the decoder
|
||||||
|
|
||||||
|
### Tradeoff
|
||||||
|
|
||||||
|
This is a wire-format change. Every compliant implementation would need to adopt
|
||||||
|
the new framing.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
This is the strongest protocol-level change to consider first, because the current
|
||||||
|
framing directly blocks further copy removal.
|
||||||
|
|
||||||
|
## Change 2: Compact Path Representation for a Future v2
|
||||||
|
|
||||||
|
### Current problem
|
||||||
|
|
||||||
|
`PROTOCOL.md` Sections 5, 6, 10, 11, and 13 make paths canonical on the wire as
|
||||||
|
`Vec<String>` values.
|
||||||
|
|
||||||
|
That is easy to understand and debug, but it imposes real cost:
|
||||||
|
|
||||||
|
- path routing requires segment-wise string comparison
|
||||||
|
- hook state keys carry owned path vectors
|
||||||
|
- packets repeat full path strings over and over
|
||||||
|
- the runtime must repeatedly compare or clone path structures at boundaries
|
||||||
|
|
||||||
|
The new implementation minimizes those costs internally, but it cannot eliminate
|
||||||
|
them while the wire format remains path-string based.
|
||||||
|
|
||||||
|
### Recommended protocol change
|
||||||
|
|
||||||
|
For a future protocol version, consider separating:
|
||||||
|
|
||||||
|
- the canonical human-readable control/discovery layer
|
||||||
|
- the compact transport/runtime layer
|
||||||
|
|
||||||
|
The compact transport/runtime layer would use stable numeric endpoint IDs instead
|
||||||
|
of repeated `Vec<String>` path payloads.
|
||||||
|
|
||||||
|
### Why this is objectively better
|
||||||
|
|
||||||
|
- routing becomes integer-based instead of string-prefix based
|
||||||
|
- hook keys become compact and cheap to index
|
||||||
|
- packets shrink
|
||||||
|
- path comparisons and many path clones disappear from the hot path
|
||||||
|
|
||||||
|
### Tradeoff
|
||||||
|
|
||||||
|
This is a full protocol-versioning decision, not a local cleanup.
|
||||||
|
|
||||||
|
It adds coordination costs:
|
||||||
|
|
||||||
|
- peers must agree on endpoint IDs
|
||||||
|
- topology updates become more structured
|
||||||
|
- the protocol becomes less self-describing on the wire
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
Do **not** make this change as a silent update to the current protocol.
|
||||||
|
|
||||||
|
If pursued, it should be introduced explicitly as a `v2` protocol, because it is
|
||||||
|
no longer behaviorally equivalent to the current path-based wire model.
|
||||||
|
|
||||||
|
## Change 3: Clarify Caller-Side Hook Activation Semantics
|
||||||
|
|
||||||
|
### Current problem
|
||||||
|
|
||||||
|
`PROTOCOL.md` Section 13 is explicit about callee-side pending call context, but
|
||||||
|
it leaves more room for interpretation on the caller side after a `Call` is sent.
|
||||||
|
|
||||||
|
The current runtime keeps caller-side hook state available immediately after send
|
||||||
|
so it can validate returned traffic efficiently.
|
||||||
|
|
||||||
|
That is practical, but the spec could be clearer about whether the caller's local
|
||||||
|
hook record is considered active immediately, or merely reserved until the callee
|
||||||
|
accepts.
|
||||||
|
|
||||||
|
### Recommended protocol change
|
||||||
|
|
||||||
|
Clarify caller-side wording in Section 13 so implementations know whether the
|
||||||
|
caller may allocate directly into active host state after sending a `Call`, as
|
||||||
|
long as early returned `Data` for an actually inactive hook is still discarded per
|
||||||
|
Section 14.1.
|
||||||
|
|
||||||
|
### Why this is objectively better
|
||||||
|
|
||||||
|
- removes ambiguity for optimized runtimes
|
||||||
|
- makes caller-side hook bookkeeping more consistent across implementations
|
||||||
|
- avoids accidental spec drift through inference
|
||||||
|
|
||||||
|
### Tradeoff
|
||||||
|
|
||||||
|
This is a clarification change, not necessarily a wire-format change.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The runtime rewrite shows that most of the original performance problems were
|
||||||
|
architectural, not inherent to the protocol.
|
||||||
|
|
||||||
|
The current protocol can support a much lower-loop implementation than before.
|
||||||
|
|
||||||
|
The main remaining protocol-level blocker is the framing/alignment rule. That is
|
||||||
|
the one change most worth making if the next goal is to reduce unavoidable memory
|
||||||
|
copies further.
|
||||||
@@ -78,6 +78,12 @@ impl<'a> ParsedFrame<'a> {
|
|||||||
self.header.clone()
|
self.header.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Consumes the parsed frame and returns its owned header and borrowed payload.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_parts(self) -> (PacketHeader, &'a [u8]) {
|
||||||
|
(self.header, self.payload_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Deserializes the payload as a [`CallMessage`].
|
/// Deserializes the payload as a [`CallMessage`].
|
||||||
pub fn deserialize_call(&self) -> Result<CallMessage, FrameError> {
|
pub fn deserialize_call(&self) -> Result<CallMessage, FrameError> {
|
||||||
deserialize_archived_bytes::<ArchivedCallMessage, CallMessage>(self.payload_bytes)
|
deserialize_archived_bytes::<ArchivedCallMessage, CallMessage>(self.payload_bytes)
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
use alloc::{collections::BTreeSet, string::String, vec::Vec};
|
use alloc::{collections::BTreeSet, string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::protocol::tree::ActiveHook;
|
use crate::protocol::tree::{ActiveHook, HookKey};
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
CallMessage, DataMessage, FrameBytes, HookTarget, PacketHeader, PacketType, ValidationError,
|
CallMessage, DataMessage, FrameBytes, HookTarget, PacketHeader, PacketType, ValidationError,
|
||||||
encode_packet, validate_call, validate_header, validate_procedure_id,
|
encode_packet, validate_call, validate_header, validate_procedure_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::RouteDecision;
|
use super::super::{CompiledRoutes, RouteDecision};
|
||||||
use super::core::{ChildRoute, EndpointError, EndpointOutcome, ProtocolEndpoint};
|
use super::core::{ChildRoute, EndpointError, EndpointOutcome, ProtocolEndpoint};
|
||||||
use crate::protocol::tree::LeafSpec;
|
use crate::protocol::tree::LeafSpec;
|
||||||
|
|
||||||
@@ -63,6 +63,8 @@ impl ProtocolEndpoint {
|
|||||||
peer_path: header.dst_path.clone(),
|
peer_path: header.dst_path.clone(),
|
||||||
procedure_id: call.procedure_id.clone(),
|
procedure_id: call.procedure_id.clone(),
|
||||||
dst_leaf: header.dst_leaf.clone(),
|
dst_leaf: header.dst_leaf.clone(),
|
||||||
|
local_ended: false,
|
||||||
|
peer_ended: false,
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
@@ -114,9 +116,15 @@ impl ProtocolEndpoint {
|
|||||||
children: Vec<ChildRoute>,
|
children: Vec<ChildRoute>,
|
||||||
leaves: Vec<LeafSpec>,
|
leaves: Vec<LeafSpec>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let registered_children = children
|
||||||
|
.iter()
|
||||||
|
.filter(|child| child.state == super::core::ConnectionState::Registered)
|
||||||
|
.map(|child| child.path.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
routing: CompiledRoutes::new(&path, ®istered_children, parent_path.is_some()),
|
||||||
path,
|
path,
|
||||||
parent_path,
|
|
||||||
children,
|
children,
|
||||||
leaves: leaves
|
leaves: leaves
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -202,6 +210,13 @@ impl ProtocolEndpoint {
|
|||||||
) -> Result<EndpointOutcome, EndpointError> {
|
) -> Result<EndpointOutcome, EndpointError> {
|
||||||
let (header, message) = self.prepare_data(dst_path, hook_id, procedure_id, data, end_hook)?;
|
let (header, message) = self.prepare_data(dst_path, hook_id, procedure_id, data, end_hook)?;
|
||||||
|
|
||||||
|
if end_hook {
|
||||||
|
let key = HookKey::new(self.path.clone(), hook_id);
|
||||||
|
if self.hooks.mark_local_end(&key) {
|
||||||
|
self.hooks.remove_active(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Local => self.handle_local_data(header, message),
|
RouteDecision::Local => self.handle_local_data(header, message),
|
||||||
route => Ok(EndpointOutcome::forward(route, encode_packet(&header, &message)?)),
|
route => Ok(EndpointOutcome::forward(route, encode_packet(&header, &message)?)),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::protocol::{
|
|||||||
CallMessage, DataMessage, FaultMessage, FrameBytes, FrameError, PacketHeader, ValidationError,
|
CallMessage, DataMessage, FaultMessage, FrameBytes, FrameError, PacketHeader, ValidationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::{HookTable, RouteDecision};
|
use super::super::{CompiledRoutes, HookTable, RouteDecision};
|
||||||
|
|
||||||
/// Local connection state used for child route eligibility.
|
/// Local connection state used for child route eligibility.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -182,8 +182,8 @@ pub trait Endpoint {
|
|||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct ProtocolEndpoint {
|
pub struct ProtocolEndpoint {
|
||||||
pub(crate) path: Vec<String>,
|
pub(crate) path: Vec<String>,
|
||||||
pub(crate) parent_path: Option<Vec<String>>,
|
|
||||||
pub(crate) children: Vec<ChildRoute>,
|
pub(crate) children: Vec<ChildRoute>,
|
||||||
|
pub(crate) routing: CompiledRoutes,
|
||||||
pub(crate) leaves: BTreeMap<String, LeafSpec>,
|
pub(crate) leaves: BTreeMap<String, LeafSpec>,
|
||||||
pub(crate) endpoint_procedures: BTreeSet<String>,
|
pub(crate) endpoint_procedures: BTreeSet<String>,
|
||||||
pub(crate) hooks: HookTable,
|
pub(crate) hooks: HookTable,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::protocol::{
|
|||||||
DataMessage, FaultMessage, PacketHeader, PacketType, ProtocolFault, encode_packet,
|
DataMessage, FaultMessage, PacketHeader, PacketType, ProtocolFault, encode_packet,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::{HookKey, RouteDecision, route_destination};
|
use super::super::{HookKey, RouteDecision};
|
||||||
use super::core::{EndpointError, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint};
|
use super::core::{EndpointError, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint};
|
||||||
|
|
||||||
impl ProtocolEndpoint {
|
impl ProtocolEndpoint {
|
||||||
@@ -23,6 +23,7 @@ impl ProtocolEndpoint {
|
|||||||
return Ok(EndpointOutcome::dropped());
|
return Ok(EndpointOutcome::dropped());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.hooks.remove_pending(&key);
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
|
|
||||||
let header = PacketHeader {
|
let header = PacketHeader {
|
||||||
@@ -82,7 +83,7 @@ impl ProtocolEndpoint {
|
|||||||
return Ok(EndpointOutcome::dropped());
|
return Ok(EndpointOutcome::dropped());
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.end_hook {
|
if message.end_hook && self.hooks.mark_peer_end(&key) {
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,16 @@ impl ProtocolEndpoint {
|
|||||||
header.hook_id.expect("validated"),
|
header.hook_id.expect("validated"),
|
||||||
&header.src_path,
|
&header.src_path,
|
||||||
) else {
|
) else {
|
||||||
return Ok(EndpointOutcome::dropped());
|
let key = HookKey::new(self.path.clone(), header.hook_id.expect("validated"));
|
||||||
|
let matches_pending = self
|
||||||
|
.hooks
|
||||||
|
.pending(&key)
|
||||||
|
.is_some_and(|pending| pending.caller_src_path == header.src_path);
|
||||||
|
if !matches_pending {
|
||||||
|
return Ok(EndpointOutcome::dropped());
|
||||||
|
}
|
||||||
|
self.hooks.remove_pending(&key);
|
||||||
|
return Ok(EndpointOutcome::event(LocalEvent::Fault { header, message }));
|
||||||
};
|
};
|
||||||
|
|
||||||
self.hooks.remove_active(&key);
|
self.hooks.remove_active(&key);
|
||||||
@@ -110,18 +120,7 @@ impl ProtocolEndpoint {
|
|||||||
|
|
||||||
/// Chooses the next hop using the protocol's longest-prefix routing rule.
|
/// Chooses the next hop using the protocol's longest-prefix routing rule.
|
||||||
pub(crate) fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
|
pub(crate) fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
|
||||||
let child_paths = self
|
self.routing.route(dst_path)
|
||||||
.children
|
|
||||||
.iter()
|
|
||||||
.filter(|child| child.state == super::core::ConnectionState::Registered)
|
|
||||||
.map(|child| &child.path);
|
|
||||||
|
|
||||||
route_destination(
|
|
||||||
&self.path,
|
|
||||||
child_paths,
|
|
||||||
self.parent_path.is_some(),
|
|
||||||
dst_path,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates whether a source path is attributable to the ingress side.
|
/// Validates whether a source path is attributable to the ingress side.
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
//! the `Call`, `Data`, and `Fault` sections of `PROTOCOL.md`.
|
//! the `Call`, `Data`, and `Fault` sections of `PROTOCOL.md`.
|
||||||
|
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
CallMessage, PacketType, ProtocolFault, decode_frame, introspection::INTROSPECTION_PROCEDURE_ID,
|
CallMessage, PacketType, ProtocolFault, decode_frame, deserialize_archived_bytes,
|
||||||
validate_call, validate_header,
|
introspection::INTROSPECTION_PROCEDURE_ID, validate_call, validate_header,
|
||||||
};
|
};
|
||||||
|
use crate::protocol::types::{ArchivedCallMessage, ArchivedDataMessage, ArchivedFaultMessage};
|
||||||
|
|
||||||
use super::super::{ActiveHook, HookKey, RouteDecision};
|
use super::super::{HookKey, PendingHook, RouteDecision};
|
||||||
use super::core::{
|
use super::core::{
|
||||||
Endpoint, EndpointError, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint,
|
Endpoint, EndpointError, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint,
|
||||||
};
|
};
|
||||||
@@ -57,18 +58,26 @@ impl ProtocolEndpoint {
|
|||||||
|
|
||||||
if let Some(hook) = &message.response_hook
|
if let Some(hook) = &message.response_hook
|
||||||
&& hook.return_path != self.path
|
&& hook.return_path != self.path
|
||||||
&& self
|
{
|
||||||
|
if self
|
||||||
.hooks
|
.hooks
|
||||||
.insert_active(ActiveHook {
|
.insert_pending(PendingHook {
|
||||||
return_path: hook.return_path.clone(),
|
return_path: hook.return_path.clone(),
|
||||||
hook_id: hook.hook_id,
|
hook_id: hook.hook_id,
|
||||||
peer_path: header.src_path.clone(),
|
caller_src_path: header.src_path.clone(),
|
||||||
procedure_id: message.procedure_id.clone(),
|
procedure_id: message.procedure_id.clone(),
|
||||||
dst_leaf: header.dst_leaf.clone(),
|
dst_leaf: header.dst_leaf.clone(),
|
||||||
})
|
})
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return self.emit_fault_if_possible(key, ProtocolFault::INTERNAL_ERROR);
|
return self.emit_fault_if_possible(key, ProtocolFault::INTERNAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = &key
|
||||||
|
&& self.hooks.activate_pending(key).is_none()
|
||||||
|
{
|
||||||
|
return self.emit_fault_if_possible(Some(key.clone()), ProtocolFault::INTERNAL_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(EndpointOutcome::event(LocalEvent::Call { header, message }))
|
Ok(EndpointOutcome::event(LocalEvent::Call { header, message }))
|
||||||
@@ -95,12 +104,10 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
|
|
||||||
match header.packet_type {
|
match header.packet_type {
|
||||||
PacketType::Call => {
|
PacketType::Call => {
|
||||||
let message = parsed.deserialize_call()?;
|
|
||||||
if !matches!(ingress, Ingress::Parent | Ingress::Local) {
|
if !matches!(ingress, Ingress::Parent | Ingress::Local) {
|
||||||
return Ok(EndpointOutcome::dropped());
|
return Ok(EndpointOutcome::dropped());
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_call(header, &message)?;
|
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Child(index) => {
|
RouteDecision::Child(index) => {
|
||||||
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
||||||
@@ -109,14 +116,24 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
Ok(EndpointOutcome::forward(RouteDecision::Parent, frame))
|
Ok(EndpointOutcome::forward(RouteDecision::Parent, frame))
|
||||||
}
|
}
|
||||||
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
|
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
|
||||||
RouteDecision::Local => self.handle_local_call(parsed.deserialize_header(), message),
|
RouteDecision::Local => {
|
||||||
|
let (header, payload) = parsed.into_parts();
|
||||||
|
let message =
|
||||||
|
deserialize_archived_bytes::<ArchivedCallMessage, CallMessage>(payload)?;
|
||||||
|
validate_call(&header, &message)?;
|
||||||
|
self.handle_local_call(header, message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PacketType::Data => {
|
PacketType::Data => {
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Local => {
|
RouteDecision::Local => {
|
||||||
let message = parsed.deserialize_data()?;
|
let (header, payload) = parsed.into_parts();
|
||||||
self.handle_local_data(parsed.deserialize_header(), message)
|
let message = deserialize_archived_bytes::<
|
||||||
|
ArchivedDataMessage,
|
||||||
|
crate::protocol::DataMessage,
|
||||||
|
>(payload)?;
|
||||||
|
self.handle_local_data(header, message)
|
||||||
}
|
}
|
||||||
RouteDecision::Child(index) => {
|
RouteDecision::Child(index) => {
|
||||||
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
||||||
@@ -130,8 +147,12 @@ impl Endpoint for ProtocolEndpoint {
|
|||||||
PacketType::Fault => {
|
PacketType::Fault => {
|
||||||
match self.decide_route(&header.dst_path) {
|
match self.decide_route(&header.dst_path) {
|
||||||
RouteDecision::Local => {
|
RouteDecision::Local => {
|
||||||
let message = parsed.deserialize_fault()?;
|
let (header, payload) = parsed.into_parts();
|
||||||
self.handle_local_fault(parsed.deserialize_header(), message)
|
let message = deserialize_archived_bytes::<
|
||||||
|
ArchivedFaultMessage,
|
||||||
|
crate::protocol::FaultMessage,
|
||||||
|
>(payload)?;
|
||||||
|
self.handle_local_fault(header, message)
|
||||||
}
|
}
|
||||||
RouteDecision::Child(index) => {
|
RouteDecision::Child(index) => {
|
||||||
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
|
||||||
|
|||||||
+122
-26
@@ -30,13 +30,18 @@ pub struct ActiveHook {
|
|||||||
pub peer_path: Vec<String>,
|
pub peer_path: Vec<String>,
|
||||||
pub procedure_id: String,
|
pub procedure_id: String,
|
||||||
pub dst_leaf: Option<String>,
|
pub dst_leaf: Option<String>,
|
||||||
|
pub local_ended: bool,
|
||||||
|
pub peer_ended: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peer-scoped index key used by the non-host side of a hook.
|
/// Pending hook context used only for fault attribution before activation.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct PeerHookKey {
|
pub struct PendingHook {
|
||||||
hook_id: u64,
|
pub return_path: Vec<String>,
|
||||||
peer_path: Vec<String>,
|
pub hook_id: u64,
|
||||||
|
pub caller_src_path: Vec<String>,
|
||||||
|
pub procedure_id: String,
|
||||||
|
pub dst_leaf: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Duplicate hook insertion error.
|
/// Duplicate hook insertion error.
|
||||||
@@ -46,14 +51,16 @@ pub struct HookConflict;
|
|||||||
/// Durable hook state tables.
|
/// Durable hook state tables.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HookTable {
|
pub struct HookTable {
|
||||||
active: BTreeMap<HookKey, ActiveHook>,
|
pending: BTreeMap<u64, BTreeMap<Vec<String>, PendingHook>>,
|
||||||
active_by_peer: BTreeMap<PeerHookKey, HookKey>,
|
active: BTreeMap<u64, BTreeMap<Vec<String>, ActiveHook>>,
|
||||||
|
active_by_peer: BTreeMap<u64, BTreeMap<Vec<String>, Vec<String>>>,
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HookTable {
|
impl Default for HookTable {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
pending: BTreeMap::new(),
|
||||||
active: BTreeMap::new(),
|
active: BTreeMap::new(),
|
||||||
active_by_peer: BTreeMap::new(),
|
active_by_peer: BTreeMap::new(),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
@@ -73,35 +80,101 @@ impl HookTable {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts a pending hook created by a received call.
|
||||||
|
pub fn insert_pending(&mut self, pending: PendingHook) -> Result<(), HookConflict> {
|
||||||
|
if self.pending(&HookKey::new(pending.return_path.clone(), pending.hook_id)).is_some()
|
||||||
|
|| self.active(&HookKey::new(pending.return_path.clone(), pending.hook_id)).is_some()
|
||||||
|
{
|
||||||
|
return Err(HookConflict);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pending
|
||||||
|
.entry(pending.hook_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(pending.return_path.clone(), pending);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Inserts an already-active hook flow.
|
/// Inserts an already-active hook flow.
|
||||||
pub fn insert_active(&mut self, active: ActiveHook) -> Result<(), HookConflict> {
|
pub fn insert_active(&mut self, active: ActiveHook) -> Result<(), HookConflict> {
|
||||||
let key = HookKey::new(active.return_path.clone(), active.hook_id);
|
let key = HookKey::new(active.return_path.clone(), active.hook_id);
|
||||||
let peer_key = PeerHookKey {
|
if self.pending(&key).is_some() || self.active(&key).is_some() {
|
||||||
hook_id: active.hook_id,
|
|
||||||
peer_path: active.peer_path.clone(),
|
|
||||||
};
|
|
||||||
if self.active.contains_key(&key) || self.active_by_peer.contains_key(&peer_key) {
|
|
||||||
return Err(HookConflict);
|
return Err(HookConflict);
|
||||||
}
|
}
|
||||||
self.active_by_peer.insert(peer_key, key.clone());
|
|
||||||
self.active.insert(key, active);
|
self.active_by_peer
|
||||||
|
.entry(active.hook_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(active.peer_path.clone(), active.return_path.clone());
|
||||||
|
self.active
|
||||||
|
.entry(active.hook_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(active.return_path.clone(), active);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Promotes one pending hook into active state after local acceptance.
|
||||||
|
pub fn activate_pending(&mut self, key: &HookKey) -> Option<()> {
|
||||||
|
let pending = self.remove_pending(key)?;
|
||||||
|
self.insert_active(ActiveHook {
|
||||||
|
return_path: pending.return_path,
|
||||||
|
hook_id: pending.hook_id,
|
||||||
|
peer_path: pending.caller_src_path,
|
||||||
|
procedure_id: pending.procedure_id,
|
||||||
|
dst_leaf: pending.dst_leaf,
|
||||||
|
local_ended: false,
|
||||||
|
peer_ended: false,
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a pending hook entry.
|
||||||
|
pub fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook> {
|
||||||
|
let hooks = self.pending.get_mut(&key.hook_id)?;
|
||||||
|
let pending = hooks.remove(key.return_path.as_slice())?;
|
||||||
|
if hooks.is_empty() {
|
||||||
|
self.pending.remove(&key.hook_id);
|
||||||
|
}
|
||||||
|
Some(pending)
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes an active hook entry.
|
/// Removes an active hook entry.
|
||||||
pub fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook> {
|
pub fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook> {
|
||||||
let active = self.active.remove(key)?;
|
let hooks = self.active.get_mut(&key.hook_id)?;
|
||||||
self.active_by_peer.remove(&PeerHookKey {
|
let active = hooks.remove(key.return_path.as_slice())?;
|
||||||
hook_id: active.hook_id,
|
if hooks.is_empty() {
|
||||||
peer_path: active.peer_path.clone(),
|
self.active.remove(&key.hook_id);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if let Some(peer_index) = self.active_by_peer.get_mut(&key.hook_id) {
|
||||||
|
peer_index.remove(active.peer_path.as_slice());
|
||||||
|
if peer_index.is_empty() {
|
||||||
|
self.active_by_peer.remove(&key.hook_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(active)
|
Some(active)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a pending hook by its host-scoped key.
|
||||||
|
#[must_use]
|
||||||
|
pub fn pending(&self, key: &HookKey) -> Option<&PendingHook> {
|
||||||
|
self.pending
|
||||||
|
.get(&key.hook_id)?
|
||||||
|
.get(key.return_path.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an active hook by its host-scoped key.
|
/// Returns an active hook by its host-scoped key.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn active(&self, key: &HookKey) -> Option<&ActiveHook> {
|
pub fn active(&self, key: &HookKey) -> Option<&ActiveHook> {
|
||||||
self.active.get(key)
|
self.active.get(&key.hook_id)?.get(key.return_path.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mutable access to an active hook by its host-scoped key.
|
||||||
|
pub fn active_mut(&mut self, key: &HookKey) -> Option<&mut ActiveHook> {
|
||||||
|
self.active
|
||||||
|
.get_mut(&key.hook_id)?
|
||||||
|
.get_mut(key.return_path.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves one active hook key from either the host side or the peer side.
|
/// Resolves one active hook key from either the host side or the peer side.
|
||||||
@@ -113,21 +186,44 @@ impl HookTable {
|
|||||||
peer_path: &[String],
|
peer_path: &[String],
|
||||||
) -> Option<HookKey> {
|
) -> Option<HookKey> {
|
||||||
let host_key = HookKey::new(return_path.to_vec(), hook_id);
|
let host_key = HookKey::new(return_path.to_vec(), hook_id);
|
||||||
if self.active.contains_key(&host_key) {
|
if self.active(&host_key).is_some() {
|
||||||
return Some(host_key);
|
return Some(host_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.active_by_peer
|
self.active_by_peer
|
||||||
.get(&PeerHookKey {
|
.get(&hook_id)?
|
||||||
hook_id,
|
.get(peer_path)
|
||||||
peer_path: peer_path.to_vec(),
|
|
||||||
})
|
|
||||||
.cloned()
|
.cloned()
|
||||||
|
.map(|return_path| HookKey::new(return_path, hook_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks one locally-originated final data packet.
|
||||||
|
pub fn mark_local_end(&mut self, key: &HookKey) -> bool {
|
||||||
|
let Some(active) = self.active_mut(key) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
active.local_ended = true;
|
||||||
|
active.peer_ended
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks one peer-originated final data packet.
|
||||||
|
pub fn mark_peer_end(&mut self, key: &HookKey) -> bool {
|
||||||
|
let Some(active) = self.active_mut(key) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
active.peer_ended = true;
|
||||||
|
active.local_ended
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether one key still has pending or active state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn contains(&self, key: &HookKey) -> bool {
|
||||||
|
self.pending(key).is_some() || self.active(key).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of active hooks.
|
/// Returns the number of active hooks.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn active_len(&self) -> usize {
|
pub fn active_len(&self) -> usize {
|
||||||
self.active.len()
|
self.active.values().map(BTreeMap::len).sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ pub use endpoint::{
|
|||||||
ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec,
|
ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafSpec,
|
||||||
LocalEvent, ProtocolEndpoint,
|
LocalEvent, ProtocolEndpoint,
|
||||||
};
|
};
|
||||||
pub use hook::{ActiveHook, HookConflict, HookKey, HookTable};
|
pub use hook::{ActiveHook, HookConflict, HookKey, HookTable, PendingHook};
|
||||||
pub use routing::{
|
pub use routing::{
|
||||||
DefaultRouteProvider, LeafNode, RouteDecision, RouteProvider, TreeNode, is_prefix,
|
CompiledRoutes, DefaultRouteProvider, LeafNode, RouteDecision, RouteProvider, TreeNode,
|
||||||
route_destination,
|
is_prefix, route_destination,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Path routing helpers and explicit enum tree declarations.
|
//! Path routing helpers and explicit enum tree declarations.
|
||||||
|
|
||||||
use alloc::{string::String, vec::Vec};
|
use alloc::{collections::BTreeMap, string::String, vec, vec::Vec};
|
||||||
|
|
||||||
/// Explicit test tree declaration used for configuration.
|
/// Explicit test tree declaration used for configuration.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -67,6 +67,93 @@ pub enum RouteDecision {
|
|||||||
Drop,
|
Drop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One compiled routing table for one endpoint boundary.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CompiledRoutes {
|
||||||
|
local_path: Vec<String>,
|
||||||
|
has_parent: bool,
|
||||||
|
nodes: Vec<RouteTrieNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct RouteTrieNode {
|
||||||
|
best_child: Option<usize>,
|
||||||
|
edges: BTreeMap<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompiledRoutes {
|
||||||
|
/// Compiles the registered-child prefixes into a trie once.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(local_path: &[String], child_paths: &[Vec<String>], has_parent: bool) -> Self {
|
||||||
|
let mut table = Self {
|
||||||
|
local_path: local_path.to_vec(),
|
||||||
|
has_parent,
|
||||||
|
nodes: vec![RouteTrieNode::default()],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (index, child_path) in child_paths.iter().enumerate() {
|
||||||
|
table.insert_child(index, child_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_child(&mut self, index: usize, child_path: &[String]) {
|
||||||
|
if !is_prefix(&self.local_path, child_path) || child_path.len() <= self.local_path.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut node_index = 0usize;
|
||||||
|
for segment in &child_path[self.local_path.len()..] {
|
||||||
|
let next_index = if let Some(next_index) = self.nodes[node_index].edges.get(segment) {
|
||||||
|
*next_index
|
||||||
|
} else {
|
||||||
|
let next_index = self.nodes.len();
|
||||||
|
self.nodes.push(RouteTrieNode::default());
|
||||||
|
self.nodes[node_index]
|
||||||
|
.edges
|
||||||
|
.insert(segment.clone(), next_index);
|
||||||
|
next_index
|
||||||
|
};
|
||||||
|
node_index = next_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nodes[node_index].best_child = Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves one destination path using one segment walk.
|
||||||
|
#[must_use]
|
||||||
|
pub fn route(&self, dst_path: &[String]) -> RouteDecision {
|
||||||
|
if !is_prefix(&self.local_path, dst_path) {
|
||||||
|
return if self.has_parent {
|
||||||
|
RouteDecision::Parent
|
||||||
|
} else {
|
||||||
|
RouteDecision::Drop
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best_child = None;
|
||||||
|
let mut node_index = 0usize;
|
||||||
|
for segment in &dst_path[self.local_path.len()..] {
|
||||||
|
let Some(next_index) = self.nodes[node_index].edges.get(segment) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
node_index = *next_index;
|
||||||
|
if let Some(index) = self.nodes[node_index].best_child {
|
||||||
|
best_child = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = best_child {
|
||||||
|
return RouteDecision::Child(index);
|
||||||
|
}
|
||||||
|
if self.local_path == dst_path {
|
||||||
|
return RouteDecision::Local;
|
||||||
|
}
|
||||||
|
RouteDecision::Drop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if `prefix` is a path prefix of `path`.
|
/// Returns `true` if `prefix` is a path prefix of `path`.
|
||||||
pub fn is_prefix(prefix: &[String], path: &[String]) -> bool {
|
pub fn is_prefix(prefix: &[String], path: &[String]) -> bool {
|
||||||
prefix.len() <= path.len()
|
prefix.len() <= path.len()
|
||||||
@@ -165,6 +252,24 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compiled_routes_choose_longest_prefix_without_child_scan() {
|
||||||
|
let table = CompiledRoutes::new(
|
||||||
|
&[String::from("a")],
|
||||||
|
&[
|
||||||
|
vec![String::from("a"), String::from("b")],
|
||||||
|
vec![String::from("a"), String::from("x")],
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
table.route(&[String::from("a"), String::from("b"), String::from("c")]),
|
||||||
|
RouteDecision::Child(0)
|
||||||
|
);
|
||||||
|
assert_eq!(table.route(&[String::from("z")]), RouteDecision::Parent);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tree_enum_flattens_paths() {
|
fn tree_enum_flattens_paths() {
|
||||||
let tree = TreeNode::Root {
|
let tree = TreeNode::Root {
|
||||||
|
|||||||
Reference in New Issue
Block a user