feat(runtime): supervise resident component resources
Detect the resident actor export, route Host events through the bounded Actor mailbox, enforce fuel, deadlines, payload limits, and translate WIT effects into typed runtime values. Add a revision-scoped ResidentSession with deny-by-default TCP, UDP, and Unix endpoint policies, non-reused resource IDs, stream lifecycle and backpressure state, atomic effect validation, timers, subscriptions, extension sources, and shutdown handling. Cover passive rejection, real Component ABI round trips, resource ownership, endpoint policy, and driver operation validation with integration tests.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
@@ -9,8 +10,11 @@ use std::{
|
||||
};
|
||||
|
||||
use wasmeld_runtime::{
|
||||
KV_MAX_VALUE_BYTES, KvBackend, KvBackendError, ResourceLimits, Runtime, RuntimeConfig,
|
||||
RuntimeError, ServiceManifest,
|
||||
ComponentExecution, KV_MAX_VALUE_BYTES, KvBackend, KvBackendError, NetworkScope,
|
||||
ResidentEffect, ResidentEndpoint, ResidentEvent, ResidentHostError, ResidentLimits,
|
||||
ResidentOperation, ResidentPolicy, ResidentResourceKind, ResidentResourceMetadata,
|
||||
ResidentSession, ResourceId, ResourceLimits, Runtime, RuntimeConfig, RuntimeError,
|
||||
ServiceManifest, StreamCloseReason,
|
||||
};
|
||||
|
||||
static COMPONENTS_BUILT: OnceLock<()> = OnceLock::new();
|
||||
@@ -192,6 +196,253 @@ fn explicit_clock_capability_is_linked() {
|
||||
runtime.stop(&key).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_component_handles_host_events_in_its_actor_mailbox() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) = start_component(&runtime, "resident-probe", "resident_probe_component.wasm");
|
||||
let stream_id = resource_id(1);
|
||||
let endpoint_id = resource_id(2);
|
||||
let timer_id = resource_id(3);
|
||||
let subscription_id = resource_id(4);
|
||||
let source_id = resource_id(5);
|
||||
|
||||
assert_eq!(
|
||||
runtime.execution(&key).unwrap(),
|
||||
ComponentExecution::Resident
|
||||
);
|
||||
assert_eq!(
|
||||
actor
|
||||
.dispatch_event(ResidentEvent::StreamData {
|
||||
stream_id,
|
||||
bytes: b"stream".to_vec(),
|
||||
})
|
||||
.unwrap(),
|
||||
vec![ResidentEffect::WriteStream {
|
||||
stream_id,
|
||||
bytes: b"stream".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.dispatch_event(
|
||||
&key,
|
||||
ResidentEvent::Datagram {
|
||||
endpoint_id,
|
||||
peer: "127.0.0.1:9000".to_owned(),
|
||||
bytes: b"datagram".to_vec(),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
vec![ResidentEffect::SendDatagram {
|
||||
endpoint_id,
|
||||
peer: "127.0.0.1:9000".to_owned(),
|
||||
bytes: b"datagram".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
actor
|
||||
.dispatch_event(ResidentEvent::Timer {
|
||||
timer_id,
|
||||
scheduled_at_ns: 42,
|
||||
})
|
||||
.unwrap(),
|
||||
vec![ResidentEffect::ArmTimer {
|
||||
timer_id,
|
||||
delay_ms: 10,
|
||||
interval_ms: None,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
actor
|
||||
.dispatch_event(ResidentEvent::Message {
|
||||
subscription_id,
|
||||
message_id: 99,
|
||||
bytes: b"message".to_vec(),
|
||||
})
|
||||
.unwrap(),
|
||||
vec![ResidentEffect::AcknowledgeMessage {
|
||||
subscription_id,
|
||||
message_id: 99,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
actor
|
||||
.dispatch_event(ResidentEvent::Source {
|
||||
source_id,
|
||||
event: "flush".to_owned(),
|
||||
payload: b"source".to_vec(),
|
||||
})
|
||||
.unwrap(),
|
||||
vec![ResidentEffect::SourceCommand {
|
||||
source_id,
|
||||
command: "flush".to_owned(),
|
||||
payload: b"source".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(decode_counter(actor.invoke(Vec::new()).unwrap()), 5);
|
||||
|
||||
runtime.stop(&key).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passive_component_rejects_resident_events_without_stopping() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) = start_component(&runtime, "passive-echo", "echo_component.wasm");
|
||||
|
||||
assert_eq!(
|
||||
runtime.execution(&key).unwrap(),
|
||||
ComponentExecution::Service
|
||||
);
|
||||
assert!(matches!(
|
||||
actor.dispatch_event(ResidentEvent::Shutdown),
|
||||
Err(RuntimeError::NotResident(service)) if service == key
|
||||
));
|
||||
assert_eq!(
|
||||
actor.invoke(b"still-running".to_vec()).unwrap(),
|
||||
b"still-running"
|
||||
);
|
||||
|
||||
runtime.stop(&key).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_session_owns_resources_and_validates_driver_operations() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) = start_component(
|
||||
&runtime,
|
||||
"resident-session",
|
||||
"resident_probe_component.wasm",
|
||||
);
|
||||
let policy = ResidentPolicy {
|
||||
tcp_listen: NetworkScope::Loopback,
|
||||
udp_bind: NetworkScope::Loopback,
|
||||
unix_listen_roots: vec![std::env::temp_dir()],
|
||||
};
|
||||
let mut session = ResidentSession::new(actor, policy).unwrap();
|
||||
let tcp_id = session
|
||||
.register_endpoint(ResidentEndpoint::Tcp {
|
||||
name: "ingress".to_owned(),
|
||||
bind: socket_addr("127.0.0.1:7000"),
|
||||
})
|
||||
.unwrap();
|
||||
let udp_id = session
|
||||
.register_endpoint(ResidentEndpoint::Udp {
|
||||
name: "discovery".to_owned(),
|
||||
bind: socket_addr("127.0.0.1:7001"),
|
||||
})
|
||||
.unwrap();
|
||||
let unix_id = session
|
||||
.register_endpoint(ResidentEndpoint::Unix {
|
||||
name: "local-ingress".to_owned(),
|
||||
path: std::env::temp_dir().join("wasmeld-resident-test.sock"),
|
||||
})
|
||||
.unwrap();
|
||||
let timer_id = session.register_timer("heartbeat").unwrap();
|
||||
let subscription_id = session.register_message_subscription("jobs").unwrap();
|
||||
let source_id = session
|
||||
.register_source("watcher", "fs-watch@0.1.0")
|
||||
.unwrap();
|
||||
|
||||
let (stream_id, opened) = session
|
||||
.accept_stream(tcp_id, Some("127.0.0.1:51000".to_owned()))
|
||||
.unwrap();
|
||||
assert!(opened.is_empty());
|
||||
assert_eq!(
|
||||
session.stream_data(stream_id, b"stream".to_vec()).unwrap(),
|
||||
vec![ResidentOperation::WriteStream {
|
||||
stream_id,
|
||||
bytes: b"stream".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
session
|
||||
.datagram(udp_id, "127.0.0.1:51001".to_owned(), b"datagram".to_vec(),)
|
||||
.unwrap(),
|
||||
vec![ResidentOperation::SendDatagram {
|
||||
endpoint_id: udp_id,
|
||||
peer: "127.0.0.1:51001".to_owned(),
|
||||
bytes: b"datagram".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
session.timer_fired(timer_id, 1).unwrap(),
|
||||
vec![ResidentOperation::ArmTimer {
|
||||
timer_id,
|
||||
delay_ms: 10,
|
||||
interval_ms: None,
|
||||
}]
|
||||
);
|
||||
assert!(matches!(
|
||||
session.resource_info(timer_id).unwrap().metadata,
|
||||
ResidentResourceMetadata::Timer { armed: true }
|
||||
));
|
||||
assert_eq!(
|
||||
session
|
||||
.message(subscription_id, 7, b"work".to_vec())
|
||||
.unwrap(),
|
||||
vec![ResidentOperation::AcknowledgeMessage {
|
||||
subscription_id,
|
||||
message_id: 7,
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
session
|
||||
.source_event(source_id, "flush".to_owned(), b"state".to_vec())
|
||||
.unwrap(),
|
||||
vec![ResidentOperation::SourceCommand {
|
||||
source_id,
|
||||
command: "flush".to_owned(),
|
||||
payload: b"state".to_vec(),
|
||||
}]
|
||||
);
|
||||
assert!(matches!(
|
||||
session.datagram(tcp_id, "127.0.0.1:1".to_owned(), Vec::new()),
|
||||
Err(ResidentHostError::WrongResourceKind { .. })
|
||||
));
|
||||
|
||||
session
|
||||
.stream_closed(stream_id, StreamCloseReason::PeerClosed)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
session.resource_info(stream_id),
|
||||
Err(ResidentHostError::UnknownResource { .. })
|
||||
));
|
||||
assert_eq!(session.owner(), &key);
|
||||
assert_eq!(
|
||||
session.resource_info(tcp_id).unwrap().kind,
|
||||
ResidentResourceKind::TcpEndpoint
|
||||
);
|
||||
assert_eq!(
|
||||
session.resource_info(unix_id).unwrap().kind,
|
||||
ResidentResourceKind::UnixEndpoint
|
||||
);
|
||||
|
||||
session.shutdown().unwrap();
|
||||
assert!(matches!(
|
||||
session.register_timer("late"),
|
||||
Err(ResidentHostError::ShuttingDown(_))
|
||||
));
|
||||
runtime.stop(&key).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resident_endpoint_policy_is_deny_by_default() {
|
||||
let runtime = Runtime::new(RuntimeConfig::default()).expect("runtime should start");
|
||||
let (key, actor) =
|
||||
start_component(&runtime, "resident-policy", "resident_probe_component.wasm");
|
||||
let mut session = ResidentSession::new(actor, ResidentPolicy::default()).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
session.register_endpoint(ResidentEndpoint::Tcp {
|
||||
name: "public".to_owned(),
|
||||
bind: socket_addr("0.0.0.0:8080"),
|
||||
}),
|
||||
Err(ResidentHostError::EndpointDenied(_))
|
||||
));
|
||||
|
||||
runtime.stop(&key).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kv_capability_is_service_scoped_and_shared_across_revisions() {
|
||||
let backend = Arc::new(MemoryKvBackend::default());
|
||||
@@ -379,6 +630,7 @@ fn build_components_once() {
|
||||
"components/spin/Cargo.toml",
|
||||
"components/clock-probe/Cargo.toml",
|
||||
"components/kv-probe/Cargo.toml",
|
||||
"components/resident-probe/Cargo.toml",
|
||||
"components/wasi-clock-probe/Cargo.toml",
|
||||
] {
|
||||
let status = Command::new("rustup")
|
||||
@@ -421,9 +673,18 @@ fn test_limits() -> ResourceLimits {
|
||||
mailbox_capacity: 16,
|
||||
max_input_bytes: 16 * 1024,
|
||||
max_output_bytes: 16 * 1024,
|
||||
resident: ResidentLimits::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_id(value: u64) -> ResourceId {
|
||||
ResourceId::new(value).expect("test resource id must be non-zero")
|
||||
}
|
||||
|
||||
fn socket_addr(value: &str) -> SocketAddr {
|
||||
value.parse().expect("test socket address must be valid")
|
||||
}
|
||||
|
||||
fn decode_counter(bytes: Vec<u8>) -> u64 {
|
||||
let array: [u8; 8] = bytes
|
||||
.try_into()
|
||||
@@ -447,6 +708,9 @@ fn component_world(artifact_name: &str) -> &'static str {
|
||||
"spin_component.wasm" => "component:spin/spin-component@0.1.0",
|
||||
"clock_probe_component.wasm" => "component:clock-probe/clock-probe-component@0.1.0",
|
||||
"kv_probe_component.wasm" => "component:kv-probe/kv-probe-component@0.1.0",
|
||||
"resident_probe_component.wasm" => {
|
||||
"component:resident-probe/resident-probe-component@0.1.0"
|
||||
}
|
||||
"wasi_clock_probe_component.wasm" => {
|
||||
"component:wasi-clock-probe/wasi-clock-probe-component@0.1.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user