Files
alknet/tasks/call/operation-env-invoke-peer.md

156 lines
6.7 KiB
Markdown

---
id: call/operation-env-invoke-peer
name: Add invoke_peer/peer_contains/PeerRef to OperationEnv trait for peer-keyed routing (ADR-029 §2)
status: completed
depends_on: [call/peer-composite-env]
scope: moderate
risk: medium
impact: component
level: 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
```rust
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
```rust
#[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
```rust
#[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
- [ ] `PeerRef` enum with `Specific(PeerId)` and `Any` variants
- [ ] `OperationEnv::invoke_peer` method with default-impl (delegates to `invoke_with_policy`)
- [ ] `OperationEnv::peer_contains` method with default-impl (delegates to `contains`)
- [ ] `PeerCompositeEnv` overrides `invoke_peer` 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` (insertion-order fan-out)
- [ ] `PeerCompositeEnv` overrides `peer_contains` (checks specific peer's sub-overlay)
- [ ] Reachability check (`scoped_env.allows`) gates before peer routing
- [ ] `LocalOperationEnv` and overlay envs use default-impls (no changes)
- [ ] Unit test: `PeerRef::Specific` routes to the named peer
- [ ] Unit test: `PeerRef::Specific` → NOT_FOUND when peer doesn't serve the op (no fallthrough)
- [ ] Unit test: `PeerRef::Any` routes to first peer (insertion order) that serves it
- [ ] Unit test: `peer_contains` checks specific peer's overlay
- [ ] Unit test: default-impl `invoke_peer` delegates to `invoke_with_policy` (back-compat)
- [ ] `cargo test -p alknet-call` succeeds
- [ ] `cargo clippy -p alknet-call` succeeds 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. `PeerCompositeEnv`
> overrides with real peer-keyed routing. `PeerRef::Specific` has no
> fallthrough (explicit routing must be honored or fail loudly). `PeerRef::Any`
> reuses the `invoke_with_policy` fan-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.