6.7 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| call/operation-env-invoke-peer | Add invoke_peer/peer_contains/PeerRef to OperationEnv trait for peer-keyed routing (ADR-029 §2) | completed |
|
moderate | medium | component | implementation |
Description
Add the invoke_peer and peer_contains methods to the OperationEnv trait,
with the PeerRef selector enum. Per ADR-029 §2. PeerCompositeEnv (built in
call/peer-composite-env) overrides these with real peer-keyed routing; the
default-impl preserves back-compat for single-layer envs
(LocalOperationEnv, connection overlays) that don't route by peer.
PeerRef enum
pub enum PeerRef {
Specific(PeerId), // route to this peer; NOT_FOUND if it doesn't serve the op
Any, // first peer (insertion order) that serves it
}
PeerRef::Specific(PeerId) routes to the named peer's overlay only — no
fallthrough (explicit routing must be honored or fail loudly, ADR-029 §2).
PeerRef::Any reuses invoke_with_policy (the insertion-order fan-out built
in call/peer-composite-env).
OperationEnv trait additions
#[async_trait]
pub trait OperationEnv: Send + Sync {
// ... existing invoke, invoke_with_policy, contains ...
/// Peer-routing composition (ADR-029 §2). Routes to a specific peer
/// (`PeerRef::Specific`) or to the first peer that serves the op
/// (`PeerRef::Any`). The default impl ignores the peer selector and
/// delegates to `invoke_with_policy`, preserving back-compat for
/// single-layer envs that don't route by peer. `PeerCompositeEnv`
/// overrides with real peer-keyed routing.
async fn invoke_peer(
&self,
peer: &PeerRef,
namespace: &str,
operation: &str,
input: Value,
parent: &OperationContext,
policy: AbortPolicy,
) -> ResponseEnvelope {
let _ = peer; // unused — single-layer envs don't route by peer
self.invoke_with_policy(namespace, operation, input, parent, policy).await
}
/// Does this env contain the named op *on the named peer*? Used by
/// `PeerCompositeEnv` to probe a specific peer's sub-overlay before
/// dispatching via `invoke_peer` with `PeerRef::Specific`. Default impl
/// delegates to `contains` (single-layer envs ignore the peer dimension).
fn peer_contains(&self, _peer: &PeerId, name: &str) -> bool {
self.contains(name)
}
}
PeerCompositeEnv overrides
#[async_trait]
impl OperationEnv for PeerCompositeEnv {
// ... invoke_with_policy, contains from call/peer-composite-env ...
async fn invoke_peer(
&self,
peer: &PeerRef,
namespace: &str,
operation: &str,
input: Value,
parent: &OperationContext,
policy: AbortPolicy,
) -> ResponseEnvelope {
let name = format!("{namespace}/{operation}");
if !parent.scoped_env.allows(&name) {
return ResponseEnvelope::not_found(parent.request_id.clone(), &name);
}
match peer {
PeerRef::Specific(peer_id) => {
// Route to this peer's sub-overlay only. No fallthrough —
// explicit routing must be honored or fail loudly (ADR-029 §2).
match self.connections.get(peer_id) {
Some(conn_env) if conn_env.contains(&name) => {
conn_env.invoke_with_policy(namespace, operation, input, parent, policy).await
}
_ => ResponseEnvelope::not_found(parent.request_id.clone(), &name),
}
}
PeerRef::Any => {
// Same as invoke_with_policy: session → peers in order → base.
self.invoke_with_policy(namespace, operation, input, parent, policy).await
}
}
}
fn peer_contains(&self, peer: &PeerId, name: &str) -> bool {
self.connections.get(peer).map_or(false, |c| c.contains(name))
}
}
Back-compat
Existing impls (LocalOperationEnv, connection overlay envs) use the default
invoke_peer (delegates to invoke_with_policy, ignores peer selector) and
default peer_contains (delegates to contains). No changes needed to those
impls — the trait surface grows, the behavior is preserved.
Acceptance Criteria
PeerRefenum withSpecific(PeerId)andAnyvariantsOperationEnv::invoke_peermethod with default-impl (delegates toinvoke_with_policy)OperationEnv::peer_containsmethod with default-impl (delegates tocontains)PeerCompositeEnvoverridesinvoke_peerwith real peer-keyed routingPeerRef::Specificroutes to named peer only (no fallthrough → NOT_FOUND if peer doesn't serve op)PeerRef::Anyreusesinvoke_with_policy(insertion-order fan-out)PeerCompositeEnvoverridespeer_contains(checks specific peer's sub-overlay)- Reachability check (
scoped_env.allows) gates before peer routing LocalOperationEnvand overlay envs use default-impls (no changes)- Unit test:
PeerRef::Specificroutes to the named peer - Unit test:
PeerRef::Specific→ NOT_FOUND when peer doesn't serve the op (no fallthrough) - Unit test:
PeerRef::Anyroutes to first peer (insertion order) that serves it - Unit test:
peer_containschecks specific peer's overlay - Unit test: default-impl
invoke_peerdelegates toinvoke_with_policy(back-compat) cargo test -p alknet-callsucceedscargo clippy -p alknet-callsucceeds with no warnings
References
- docs/architecture/crates/call/operation-registry.md — OperationEnv, invoke_peer, PeerRef
- docs/architecture/decisions/029-peer-graph-routing-model.md — ADR-029 §2 (PeerRef routing)
Notes
The default-impl preserves back-compat — existing single-layer envs (
LocalOperationEnv, connection overlays) work unchanged.PeerCompositeEnvoverrides with real peer-keyed routing.PeerRef::Specifichas no fallthrough (explicit routing must be honored or fail loudly).PeerRef::Anyreuses theinvoke_with_policyfan-out. The reachability check (scoped_env.allows) gates before peer routing, same as before.
Summary
Added PeerRef enum (Specific/Any variants), invoke_peer trait method with default-impl delegating to invoke_with_policy, and PeerCompositeEnv override with real peer-keyed routing — PeerRef::Specific routes to named peer only (no fallthrough → NOT_FOUND if peer doesn't serve op), PeerRef::Any reuses invoke_with_policy. Reachability check (scoped_env.allows) gates before peer routing. peer_contains default delegates to contains; PeerCompositeEnv override checks specific peer's sub-overlay. 8 unit tests covering all acceptance criteria. 221 unit + 2 integration tests pass, clippy clean, fmt clean.