mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Reduce leaf fail hook actions
This commit is contained in:
@@ -329,10 +329,9 @@ connection closes or unregisters
|
|||||||
|
|
||||||
## Known Gaps In The Current Branch
|
## Known Gaps In The Current Branch
|
||||||
|
|
||||||
- `LeafAction::SendCall` and `LeafAction::SendHookData` are reduced by
|
- `LeafAction::SendCall`, `LeafAction::SendHookData`, and `LeafAction::FailHook`
|
||||||
`NodeRuntime`; hook fault and connection action variants are still unsupported
|
are reduced by `NodeRuntime`; connection action variants are still unsupported
|
||||||
and must remain queued when encountered.
|
and must remain queued when encountered.
|
||||||
- Hook fault actions through the runtime are not implemented.
|
|
||||||
- Connection actions through the runtime are not implemented.
|
- Connection actions through the runtime are not implemented.
|
||||||
- Disconnect does not yet clean hooks, sessions, route state, and queued effects.
|
- Disconnect does not yet clean hooks, sessions, route state, and queued effects.
|
||||||
- Child ingress still allocates because the existing `Ingress::Child` owns a
|
- Child ingress still allocates because the existing `Ingress::Child` owns a
|
||||||
@@ -340,13 +339,13 @@ connection closes or unregisters
|
|||||||
|
|
||||||
## Next Implementation Slice
|
## Next Implementation Slice
|
||||||
|
|
||||||
Implement the next narrow leaf-action path:
|
Implement the next narrow connection-action path:
|
||||||
|
|
||||||
1. Apply queued `LeafAction::FailHook` through endpoint packet state.
|
1. Keep connection registration actions queued until runtime-owned disconnect
|
||||||
2. Preserve pending/active hook cleanup semantics without dropping unprocessed
|
|
||||||
actions.
|
|
||||||
3. Keep connection registration actions queued until runtime-owned disconnect
|
|
||||||
cleanup can update connections, routes, hooks, and queued effects atomically.
|
cleanup can update connections, routes, hooks, and queued effects atomically.
|
||||||
|
2. Add connection registration reduction only when route, connection, hook, and
|
||||||
|
queued-effect cleanup can be updated as one runtime transaction.
|
||||||
|
3. Preserve FIFO retry semantics for unsupported or failed connection actions.
|
||||||
|
|
||||||
That slice should continue the one-variant-at-a-time reducer approach without
|
That slice should continue the one-variant-at-a-time reducer approach without
|
||||||
implementing connection actions early.
|
implementing disconnect cleanup early.
|
||||||
|
|||||||
@@ -10,6 +10,31 @@ 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 {
|
||||||
|
/// Returns the route that would carry a locally generated hook fault for `hook_id`.
|
||||||
|
///
|
||||||
|
/// The method does not mutate hook state. Runtime owners use it to preflight transport
|
||||||
|
/// availability before calling [`fail_hook`](Self::fail_hook), which removes hook state when
|
||||||
|
/// the fault is emitted.
|
||||||
|
#[must_use]
|
||||||
|
pub fn hook_fault_route(&self, hook_id: u64) -> Option<RouteDecision> {
|
||||||
|
self.hooks
|
||||||
|
.key_for_hook_id(hook_id)
|
||||||
|
.map(|key| self.decide_route(&key.return_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminates a locally known hook with a protocol fault.
|
||||||
|
///
|
||||||
|
/// Unknown hooks are treated as an intentional drop. Known hooks are removed before the fault
|
||||||
|
/// is routed so no further local data can be emitted after the terminal fault.
|
||||||
|
pub fn fail_hook(
|
||||||
|
&mut self,
|
||||||
|
hook_id: u64,
|
||||||
|
fault: ProtocolFault,
|
||||||
|
) -> Result<EndpointOutcome, EndpointError> {
|
||||||
|
let key = self.hooks.key_for_hook_id(hook_id);
|
||||||
|
self.emit_fault_if_possible(key, fault)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn emit_fault_if_possible(
|
pub(crate) fn emit_fault_if_possible(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: Option<HookKey>,
|
key: Option<HookKey>,
|
||||||
|
|||||||
@@ -324,6 +324,25 @@ impl HookTable {
|
|||||||
Some(active)
|
Some(active)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a hook key matching `hook_id`, preferring active hooks over pending hooks.
|
||||||
|
///
|
||||||
|
/// This is intentionally a narrow bridge for current leaf APIs that identify a hook only by
|
||||||
|
/// id. Hook ids are protocol-scoped by host path, so future APIs should pass the full
|
||||||
|
/// [`HookKey`] when leaf dispatch exposes it.
|
||||||
|
#[must_use]
|
||||||
|
pub fn key_for_hook_id(&self, hook_id: u64) -> Option<HookKey> {
|
||||||
|
self.active
|
||||||
|
.keys()
|
||||||
|
.find(|key| key.hook_id == hook_id)
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| {
|
||||||
|
self.pending
|
||||||
|
.keys()
|
||||||
|
.find(|key| key.hook_id == hook_id)
|
||||||
|
.cloned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the pending hook for `key`, if present.
|
/// Returns the pending hook for `key`, if present.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
//! handles, does not dispatch leaves, and does not make admission decisions.
|
//! handles, does not dispatch leaves, and does not make admission decisions.
|
||||||
|
|
||||||
use unshell_protocol::{
|
use unshell_protocol::{
|
||||||
CallMessage, FrameBytes, PacketHeader, PacketType, tree::Endpoint as ProtocolEndpointTrait,
|
CallMessage, FrameBytes, PacketHeader, PacketType, ProtocolFault,
|
||||||
validate_call, validate_header, validate_procedure_id,
|
tree::Endpoint as ProtocolEndpointTrait, validate_call, validate_header, validate_procedure_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use unshell_protocol::tree::{
|
pub use unshell_protocol::tree::{
|
||||||
@@ -90,6 +90,21 @@ impl EndpointState {
|
|||||||
.send_data(dst_path, hook_id, procedure_id, data, end_hook)
|
.send_data(dst_path, hook_id, procedure_id, data, end_hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the route that would carry a terminal hook fault, if the hook is known.
|
||||||
|
#[must_use]
|
||||||
|
pub fn hook_fault_route(&self, hook_id: u64) -> Option<RouteDecision> {
|
||||||
|
self.endpoint.hook_fault_route(hook_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Terminates a known hook with a protocol fault, or drops unknown hook ids.
|
||||||
|
pub fn fail_hook(
|
||||||
|
&mut self,
|
||||||
|
hook_id: u64,
|
||||||
|
fault: ProtocolFault,
|
||||||
|
) -> Result<EndpointOutcome, EndpointError> {
|
||||||
|
self.endpoint.fail_hook(hook_id, fault)
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds and routes one call packet through the wrapped endpoint state.
|
/// Builds and routes one call packet through the wrapped endpoint state.
|
||||||
pub fn send_call(
|
pub fn send_call(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
//! This first slice owns transport and connection metadata, derives ingress from
|
//! This first slice owns transport and connection metadata, derives ingress from
|
||||||
//! registered connections, delegates packet invariants to [`EndpointState`], and
|
//! registered connections, delegates packet invariants to [`EndpointState`], and
|
||||||
//! queues concrete runtime effects. Leaf action reduction is intentionally
|
//! queues concrete runtime effects. Leaf action reduction is intentionally
|
||||||
//! narrow: this slice only turns outbound calls and hook-data replies into
|
//! narrow and grows one action family at a time.
|
||||||
//! endpoint outcomes.
|
|
||||||
|
|
||||||
use crate::alloc::{string::String, vec::Vec};
|
use crate::alloc::{string::String, vec::Vec};
|
||||||
use crate::connections::{
|
use crate::connections::{
|
||||||
@@ -544,10 +543,10 @@ where
|
|||||||
|
|
||||||
/// Reduces queued leaf actions through endpoint packet state.
|
/// Reduces queued leaf actions through endpoint packet state.
|
||||||
///
|
///
|
||||||
/// [`LeafAction::SendCall`] and [`LeafAction::SendHookData`] are implemented
|
/// [`LeafAction::SendCall`], [`LeafAction::SendHookData`], and
|
||||||
/// in this slice. Unsupported actions stop reduction and remain queued with
|
/// [`LeafAction::FailHook`] are implemented in this slice. Unsupported
|
||||||
/// all later actions so callers can retry after a future runtime gains
|
/// actions stop reduction and remain queued with all later actions so callers
|
||||||
/// support.
|
/// can retry after a future runtime gains support.
|
||||||
pub fn reduce_leaf_actions(&mut self) -> Result<usize, NodeRuntimeError<T::Error>> {
|
pub fn reduce_leaf_actions(&mut self) -> Result<usize, NodeRuntimeError<T::Error>> {
|
||||||
let mut reduced = 0usize;
|
let mut reduced = 0usize;
|
||||||
let mut retained = Vec::new();
|
let mut retained = Vec::new();
|
||||||
@@ -649,6 +648,40 @@ where
|
|||||||
}
|
}
|
||||||
reduced += 1;
|
reduced += 1;
|
||||||
}
|
}
|
||||||
|
LeafAction::FailHook { hook_id, fault } => {
|
||||||
|
let original_action = LeafAction::FailHook { hook_id, fault };
|
||||||
|
if let Some(route) = self.endpoint.hook_fault_route(hook_id)
|
||||||
|
&& (matches!(route, RouteDecision::Drop)
|
||||||
|
|| (route_requires_connection(route)
|
||||||
|
&& self.connection_for_route(route).is_none()))
|
||||||
|
{
|
||||||
|
retained.push((leaf_id, original_action));
|
||||||
|
retained.extend(pending);
|
||||||
|
self.leaf_actions = retained;
|
||||||
|
return Err(NodeRuntimeError::MissingRouteConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint_checkpoint = self.endpoint.clone();
|
||||||
|
let outcome = match self.endpoint.fail_hook(hook_id, fault) {
|
||||||
|
Ok(outcome) => outcome,
|
||||||
|
Err(error) => {
|
||||||
|
self.endpoint = endpoint_checkpoint;
|
||||||
|
retained.push((leaf_id, original_action));
|
||||||
|
retained.extend(pending);
|
||||||
|
self.leaf_actions = retained;
|
||||||
|
return Err(NodeRuntimeError::Endpoint(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = self.apply_outcome(outcome) {
|
||||||
|
self.endpoint = endpoint_checkpoint;
|
||||||
|
retained.push((leaf_id, original_action));
|
||||||
|
retained.extend(pending);
|
||||||
|
self.leaf_actions = retained;
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
reduced += 1;
|
||||||
|
}
|
||||||
unsupported => {
|
unsupported => {
|
||||||
let action_name = leaf_action_name(&unsupported);
|
let action_name = leaf_action_name(&unsupported);
|
||||||
retained.push((leaf_id.clone(), unsupported));
|
retained.push((leaf_id.clone(), unsupported));
|
||||||
@@ -825,6 +858,7 @@ mod tests {
|
|||||||
use crate::transport::Transport;
|
use crate::transport::Transport;
|
||||||
use unshell_protocol::tree::{
|
use unshell_protocol::tree::{
|
||||||
ChildRoute, EndpointError, IncomingCall, LeafSpec, LocalEvent, ProtocolEndpoint,
|
ChildRoute, EndpointError, IncomingCall, LeafSpec, LocalEvent, ProtocolEndpoint,
|
||||||
|
RouteDecision,
|
||||||
};
|
};
|
||||||
use unshell_protocol::{
|
use unshell_protocol::{
|
||||||
CallMessage, FrameBytes, HookTarget, PacketHeader, PacketType, ProtocolFault, decode_frame,
|
CallMessage, FrameBytes, HookTarget, PacketHeader, PacketType, ProtocolFault, decode_frame,
|
||||||
@@ -1536,6 +1570,82 @@ mod tests {
|
|||||||
assert!(data.end_hook);
|
assert!(data.end_hook);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaf_fail_hook_reduces_to_parent_fault_frame() {
|
||||||
|
let parent = ConnectionId::new(1);
|
||||||
|
let mut connections = Connections::new();
|
||||||
|
connections.push(Connection::registered(
|
||||||
|
parent,
|
||||||
|
ConnectionDirection::Parent,
|
||||||
|
vec![],
|
||||||
|
ConnectionGeneration::INITIAL,
|
||||||
|
));
|
||||||
|
|
||||||
|
let leaf_name = "org.example.v1.echo";
|
||||||
|
let endpoint = ProtocolEndpoint::new(
|
||||||
|
vec![String::from("agent")],
|
||||||
|
Some(vec![]),
|
||||||
|
vec![],
|
||||||
|
vec![LeafSpec {
|
||||||
|
name: String::from(leaf_name),
|
||||||
|
procedures: vec![String::from("org.example.v1.echo.invoke")],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let frame = encode_packet(
|
||||||
|
&PacketHeader {
|
||||||
|
packet_type: PacketType::Call,
|
||||||
|
src_path: vec![],
|
||||||
|
dst_path: vec![String::from("agent")],
|
||||||
|
dst_leaf: Some(String::from(leaf_name)),
|
||||||
|
hook_id: None,
|
||||||
|
},
|
||||||
|
&CallMessage {
|
||||||
|
procedure_id: String::from("org.example.v1.echo.invoke"),
|
||||||
|
data: vec![9],
|
||||||
|
response_hook: Some(HookTarget {
|
||||||
|
hook_id: 7,
|
||||||
|
return_path: vec![],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("frame encodes");
|
||||||
|
let calls = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut runtime = NodeRuntime::new(
|
||||||
|
EndpointState::new(endpoint),
|
||||||
|
connections,
|
||||||
|
RecordingTransport::default(),
|
||||||
|
);
|
||||||
|
runtime.register_leaf(RecordingLeaf::new(leaf_name, Rc::clone(&calls)));
|
||||||
|
runtime
|
||||||
|
.receive_frame(parent, frame)
|
||||||
|
.expect("call activates hook");
|
||||||
|
runtime.dispatch_local_effects().expect("dispatch succeeds");
|
||||||
|
runtime.leaf_actions.clear();
|
||||||
|
runtime.leaf_actions.push((
|
||||||
|
crate::leaf::LeafId::new(String::from(leaf_name)),
|
||||||
|
LeafAction::FailHook {
|
||||||
|
hook_id: 7,
|
||||||
|
fault: ProtocolFault::INTERNAL_ERROR,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
let reduced = runtime.reduce_leaf_actions().expect("fault reduces");
|
||||||
|
let outcome = runtime.tick(TickBudget::default()).expect("tick flushes");
|
||||||
|
|
||||||
|
assert_eq!(reduced, 1);
|
||||||
|
assert!(runtime.leaf_actions().is_empty());
|
||||||
|
assert_eq!(outcome.outbound_frames, 1);
|
||||||
|
assert_eq!(runtime.transport().sent.len(), 1);
|
||||||
|
assert_eq!(runtime.transport().sent[0].0, parent);
|
||||||
|
let parsed = decode_frame(&runtime.transport().sent[0].1).expect("fault decodes");
|
||||||
|
assert_eq!(parsed.header().packet_type, PacketType::Fault);
|
||||||
|
assert_eq!(parsed.header().src_path, [String::from("agent")]);
|
||||||
|
assert_eq!(parsed.header().dst_path, Vec::<String>::new());
|
||||||
|
assert_eq!(parsed.header().hook_id, Some(7));
|
||||||
|
let fault = parsed.deserialize_fault().expect("payload is fault");
|
||||||
|
assert_eq!(fault.fault, ProtocolFault::INTERNAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn leaf_send_call_reduces_to_child_transport_frame() {
|
fn leaf_send_call_reduces_to_child_transport_frame() {
|
||||||
let child = ConnectionId::new(1);
|
let child = ConnectionId::new(1);
|
||||||
@@ -1810,7 +1920,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsupported_leaf_action_is_reported_and_retained() {
|
fn unsupported_connection_action_is_reported_and_retained() {
|
||||||
let leaf_id = crate::leaf::LeafId::new(String::from("org.example.v1.echo"));
|
let leaf_id = crate::leaf::LeafId::new(String::from("org.example.v1.echo"));
|
||||||
let mut runtime = NodeRuntime::new(
|
let mut runtime = NodeRuntime::new(
|
||||||
EndpointState::new(ProtocolEndpoint::new(
|
EndpointState::new(ProtocolEndpoint::new(
|
||||||
@@ -1822,13 +1932,6 @@ mod tests {
|
|||||||
Connections::new(),
|
Connections::new(),
|
||||||
RecordingTransport::default(),
|
RecordingTransport::default(),
|
||||||
);
|
);
|
||||||
runtime.leaf_actions.push((
|
|
||||||
leaf_id.clone(),
|
|
||||||
LeafAction::FailHook {
|
|
||||||
hook_id: 7,
|
|
||||||
fault: ProtocolFault::INTERNAL_ERROR,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
runtime.leaf_actions.push((
|
runtime.leaf_actions.push((
|
||||||
leaf_id.clone(),
|
leaf_id.clone(),
|
||||||
LeafAction::Connection(ConnectionAction::Unregister {
|
LeafAction::Connection(ConnectionAction::Unregister {
|
||||||
@@ -1843,15 +1946,11 @@ mod tests {
|
|||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
error,
|
error,
|
||||||
NodeRuntimeError::UnsupportedLeafAction { ref leaf_id, action }
|
NodeRuntimeError::UnsupportedLeafAction { ref leaf_id, action }
|
||||||
if leaf_id.as_str() == "org.example.v1.echo" && action == "FailHook"
|
if leaf_id.as_str() == "org.example.v1.echo" && action == "Connection"
|
||||||
));
|
));
|
||||||
assert_eq!(runtime.leaf_actions().len(), 2);
|
assert_eq!(runtime.leaf_actions().len(), 1);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
runtime.leaf_actions()[0].1,
|
runtime.leaf_actions()[0].1,
|
||||||
LeafAction::FailHook { .. }
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
runtime.leaf_actions()[1].1,
|
|
||||||
LeafAction::Connection(_)
|
LeafAction::Connection(_)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1939,26 +2038,225 @@ mod tests {
|
|||||||
runtime
|
runtime
|
||||||
.register_parent_connection(parent, vec![], ConnectionGeneration::INITIAL)
|
.register_parent_connection(parent, vec![], ConnectionGeneration::INITIAL)
|
||||||
.expect("parent route restored");
|
.expect("parent route restored");
|
||||||
let retry_error = runtime
|
let reduced = runtime
|
||||||
.reduce_leaf_actions()
|
.reduce_leaf_actions()
|
||||||
.expect_err("later unsupported action is still reported");
|
.expect("remaining supported actions reduce");
|
||||||
|
|
||||||
|
assert_eq!(reduced, 2);
|
||||||
|
assert!(runtime.leaf_actions().is_empty());
|
||||||
|
assert!(matches!(
|
||||||
|
runtime.effects()[0],
|
||||||
|
RuntimeEffect::SendFrame { connection, .. } if connection == parent
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
runtime.effects()[1],
|
||||||
|
RuntimeEffect::SendFrame { connection, .. } if connection == parent
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_fail_hook_route_preserves_action_and_hook_for_retry() {
|
||||||
|
let parent = ConnectionId::new(1);
|
||||||
|
let mut connections = Connections::new();
|
||||||
|
connections.push(Connection::registered(
|
||||||
|
parent,
|
||||||
|
ConnectionDirection::Parent,
|
||||||
|
vec![],
|
||||||
|
ConnectionGeneration::INITIAL,
|
||||||
|
));
|
||||||
|
|
||||||
|
let leaf_name = "org.example.v1.echo";
|
||||||
|
let endpoint = ProtocolEndpoint::new(
|
||||||
|
vec![String::from("agent")],
|
||||||
|
Some(vec![]),
|
||||||
|
vec![],
|
||||||
|
vec![LeafSpec {
|
||||||
|
name: String::from(leaf_name),
|
||||||
|
procedures: vec![String::from("org.example.v1.echo.invoke")],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let frame = encode_packet(
|
||||||
|
&PacketHeader {
|
||||||
|
packet_type: PacketType::Call,
|
||||||
|
src_path: vec![],
|
||||||
|
dst_path: vec![String::from("agent")],
|
||||||
|
dst_leaf: Some(String::from(leaf_name)),
|
||||||
|
hook_id: None,
|
||||||
|
},
|
||||||
|
&CallMessage {
|
||||||
|
procedure_id: String::from("org.example.v1.echo.invoke"),
|
||||||
|
data: vec![],
|
||||||
|
response_hook: Some(HookTarget {
|
||||||
|
hook_id: 7,
|
||||||
|
return_path: vec![],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("frame encodes");
|
||||||
|
let calls = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut runtime = NodeRuntime::new(
|
||||||
|
EndpointState::new(endpoint),
|
||||||
|
connections,
|
||||||
|
RecordingTransport::default(),
|
||||||
|
);
|
||||||
|
runtime.register_leaf(RecordingLeaf::new(leaf_name, Rc::clone(&calls)));
|
||||||
|
runtime
|
||||||
|
.receive_frame(parent, frame)
|
||||||
|
.expect("call activates hook");
|
||||||
|
runtime.dispatch_local_effects().expect("dispatch succeeds");
|
||||||
|
runtime.leaf_actions.clear();
|
||||||
|
runtime.leaf_actions.push((
|
||||||
|
crate::leaf::LeafId::new(String::from(leaf_name)),
|
||||||
|
LeafAction::FailHook {
|
||||||
|
hook_id: 7,
|
||||||
|
fault: ProtocolFault::INTERNAL_ERROR,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
runtime.leaf_actions.push((
|
||||||
|
crate::leaf::LeafId::new(String::from(leaf_name)),
|
||||||
|
LeafAction::Connection(ConnectionAction::Unregister { connection: parent }),
|
||||||
|
));
|
||||||
|
runtime
|
||||||
|
.connections
|
||||||
|
.get_mut(parent)
|
||||||
|
.expect("parent connection exists")
|
||||||
|
.set_state(ConnectionState::Connected {
|
||||||
|
generation: ConnectionGeneration::INITIAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
let error = runtime
|
||||||
|
.reduce_leaf_actions()
|
||||||
|
.expect_err("missing route connection is reported");
|
||||||
|
|
||||||
|
assert!(matches!(error, NodeRuntimeError::MissingRouteConnection));
|
||||||
|
assert_eq!(runtime.leaf_actions().len(), 2);
|
||||||
|
assert!(matches!(
|
||||||
|
runtime.leaf_actions()[0].1,
|
||||||
|
LeafAction::FailHook { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
runtime.leaf_actions()[1].1,
|
||||||
|
LeafAction::Connection(_)
|
||||||
|
));
|
||||||
|
assert!(runtime.effects().is_empty());
|
||||||
|
|
||||||
|
runtime
|
||||||
|
.register_parent_connection(parent, vec![], ConnectionGeneration::INITIAL)
|
||||||
|
.expect("parent route restored");
|
||||||
|
let error = runtime
|
||||||
|
.reduce_leaf_actions()
|
||||||
|
.expect_err("retry faults hook then stops at connection action");
|
||||||
|
let outcome = runtime.tick(TickBudget::default()).expect("tick flushes");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
retry_error,
|
error,
|
||||||
NodeRuntimeError::UnsupportedLeafAction {
|
NodeRuntimeError::UnsupportedLeafAction {
|
||||||
action: "FailHook",
|
action: "Connection",
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
assert_eq!(runtime.leaf_actions().len(), 1);
|
assert_eq!(runtime.leaf_actions().len(), 1);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
runtime.leaf_actions()[0].1,
|
runtime.leaf_actions()[0].1,
|
||||||
LeafAction::FailHook { .. }
|
LeafAction::Connection(_)
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert_eq!(outcome.outbound_frames, 1);
|
||||||
runtime.effects()[0],
|
let parsed = decode_frame(&runtime.transport().sent[0].1).expect("fault decodes");
|
||||||
RuntimeEffect::SendFrame { connection, .. } if connection == parent
|
assert_eq!(parsed.header().packet_type, PacketType::Fault);
|
||||||
|
assert_eq!(parsed.header().hook_id, Some(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropped_fail_hook_route_preserves_action_and_hook_for_retry() {
|
||||||
|
let parent = ConnectionId::new(1);
|
||||||
|
let mut connections = Connections::new();
|
||||||
|
connections.push(Connection::registered(
|
||||||
|
parent,
|
||||||
|
ConnectionDirection::Parent,
|
||||||
|
vec![],
|
||||||
|
ConnectionGeneration::INITIAL,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let leaf_name = "org.example.v1.echo";
|
||||||
|
let endpoint = ProtocolEndpoint::new(
|
||||||
|
vec![String::from("agent")],
|
||||||
|
Some(vec![]),
|
||||||
|
vec![],
|
||||||
|
vec![LeafSpec {
|
||||||
|
name: String::from(leaf_name),
|
||||||
|
procedures: vec![String::from("org.example.v1.echo.invoke")],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let frame = encode_packet(
|
||||||
|
&PacketHeader {
|
||||||
|
packet_type: PacketType::Call,
|
||||||
|
src_path: vec![],
|
||||||
|
dst_path: vec![String::from("agent")],
|
||||||
|
dst_leaf: Some(String::from(leaf_name)),
|
||||||
|
hook_id: None,
|
||||||
|
},
|
||||||
|
&CallMessage {
|
||||||
|
procedure_id: String::from("org.example.v1.echo.invoke"),
|
||||||
|
data: vec![],
|
||||||
|
response_hook: Some(HookTarget {
|
||||||
|
hook_id: 7,
|
||||||
|
return_path: vec![],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("frame encodes");
|
||||||
|
let calls = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let mut runtime = NodeRuntime::new(
|
||||||
|
EndpointState::new(endpoint),
|
||||||
|
connections,
|
||||||
|
RecordingTransport::default(),
|
||||||
|
);
|
||||||
|
runtime.register_leaf(RecordingLeaf::new(leaf_name, Rc::clone(&calls)));
|
||||||
|
runtime
|
||||||
|
.receive_frame(parent, frame)
|
||||||
|
.expect("call activates hook with dropped return path");
|
||||||
|
assert!(matches!(runtime.effects()[0], RuntimeEffect::Local(_)));
|
||||||
|
runtime.dispatch_local_effects().expect("dispatch succeeds");
|
||||||
|
runtime
|
||||||
|
.endpoint
|
||||||
|
.endpoint_mut()
|
||||||
|
.set_parent_path(None)
|
||||||
|
.expect("parent route removes");
|
||||||
|
assert_eq!(
|
||||||
|
runtime.endpoint.hook_fault_route(7),
|
||||||
|
Some(RouteDecision::Drop)
|
||||||
|
);
|
||||||
|
runtime.leaf_actions.clear();
|
||||||
|
runtime.leaf_actions.push((
|
||||||
|
crate::leaf::LeafId::new(String::from(leaf_name)),
|
||||||
|
LeafAction::FailHook {
|
||||||
|
hook_id: 7,
|
||||||
|
fault: ProtocolFault::INTERNAL_ERROR,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
let error = runtime
|
||||||
|
.reduce_leaf_actions()
|
||||||
|
.expect_err("dropped fault route is reported before mutation");
|
||||||
|
|
||||||
|
assert!(matches!(error, NodeRuntimeError::MissingRouteConnection));
|
||||||
|
assert_eq!(runtime.leaf_actions().len(), 1);
|
||||||
|
assert!(runtime.effects().is_empty());
|
||||||
|
|
||||||
|
runtime
|
||||||
|
.register_parent_connection(parent, vec![], ConnectionGeneration::INITIAL)
|
||||||
|
.expect("parent route restored");
|
||||||
|
let reduced = runtime
|
||||||
|
.reduce_leaf_actions()
|
||||||
|
.expect("retained fault retries after route is restored");
|
||||||
|
let outcome = runtime.tick(TickBudget::default()).expect("tick flushes");
|
||||||
|
|
||||||
|
assert_eq!(reduced, 1);
|
||||||
|
assert_eq!(outcome.outbound_frames, 1);
|
||||||
|
assert_eq!(runtime.transport().sent[0].0, parent);
|
||||||
|
let parsed = decode_frame(&runtime.transport().sent[0].1).expect("fault decodes");
|
||||||
|
assert_eq!(parsed.header().packet_type, PacketType::Fault);
|
||||||
|
assert_eq!(parsed.header().hook_id, Some(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user