Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions crates/nexum-runtime/src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,36 @@ fn enforce_extension_sections<T: RuntimeTypes>(
Ok(())
}

/// Refuse a string two wired extensions both claim, fail-fast at boot, in
/// any of three classes: service namespace, subscription kind, manifest
/// section. Each class dedupes silently downstream, so an unchecked
/// collision routes to whichever extension the map or `.any()` scan hits
/// first.
fn enforce_extension_uniqueness<T: RuntimeTypes>(
extensions: &[Arc<dyn Extension<T>>],
) -> Result<()> {
let mut namespaces = BTreeSet::new();
let mut kinds = BTreeSet::new();
let mut sections = BTreeSet::new();
for ext in extensions {
let namespace = ext.namespace();
if !namespaces.insert(namespace) {
return Err(anyhow!("extension namespace {namespace} is claimed twice"));
}
for kind in ext.subscriptions() {
if !kinds.insert(*kind) {
return Err(anyhow!("subscription kind {kind} is claimed twice"));
}
}
for section in ext.manifest_sections() {
if !sections.insert(*section) {
return Err(anyhow!("manifest section [{section}] is claimed twice"));
}
}
}
Ok(())
}

/// Insert one kind row, refusing a duplicate manifest spelling.
fn register_kind<T: RuntimeTypes>(
kinds: &mut ProviderKinds<T>,
Expand Down Expand Up @@ -395,6 +425,7 @@ impl<T: RuntimeTypes> Supervisor<T> {
extensions: &[Arc<dyn Extension<T>>],
clocks: Option<WasiClockOverride>,
) -> Result<Self> {
enforce_extension_uniqueness(extensions)?;
let registry = capability_registry(extensions);
let services = HostServices::from_extensions(extensions)?;
// Provider kinds the boot loop resolves manifest kinds against.
Expand Down Expand Up @@ -489,6 +520,7 @@ impl<T: RuntimeTypes> Supervisor<T> {
extensions: &[Arc<dyn Extension<T>>],
clocks: Option<WasiClockOverride>,
) -> Result<Self> {
enforce_extension_uniqueness(extensions)?;
let registry = capability_registry(extensions);
let services = HostServices::from_extensions(extensions)?;
let entry = ModuleEntry {
Expand Down
62 changes: 62 additions & 0 deletions crates/nexum-runtime/src/supervisor/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,68 @@ fn extension_sections_must_be_claimed() {
assert!(err.to_string().contains("keeper"), "{err}");
}

/// Two extensions colliding on a subscription kind or a manifest section
/// are refused at boot; a non-colliding set passes the uniqueness pass.
#[test]
fn extension_claims_must_be_unique() {
struct Claiming {
namespace: &'static str,
subscriptions: &'static [&'static str],
sections: &'static [&'static str],
}
impl Extension<TestTypes> for Claiming {
fn namespace(&self) -> &'static str {
self.namespace
}
fn capabilities(&self) -> crate::manifest::NamespaceCaps {
crate::manifest::NamespaceCaps {
prefix: "acme:ext/",
ifaces: &[],
}
}
fn link(&self, _linker: &mut Linker<HostState<TestTypes>>) -> anyhow::Result<()> {
Ok(())
}
fn subscriptions(&self) -> &'static [&'static str] {
self.subscriptions
}
fn manifest_sections(&self) -> &'static [&'static str] {
self.sections
}
}
fn ext(
namespace: &'static str,
subscriptions: &'static [&'static str],
sections: &'static [&'static str],
) -> Arc<dyn Extension<TestTypes>> {
Arc::new(Claiming {
namespace,
subscriptions,
sections,
})
}

enforce_extension_uniqueness(&[
ext("a", &["orders"], &["venue"]),
ext("b", &["fills"], &["pool"]),
])
.expect("non-colliding set boots");

let err = enforce_extension_uniqueness(&[
ext("a", &["orders"], &["venue"]),
ext("b", &["orders"], &["pool"]),
])
.expect_err("duplicate subscription kind");
assert!(err.to_string().contains("orders"), "{err}");

let err = enforce_extension_uniqueness(&[
ext("a", &["orders"], &["venue"]),
ext("b", &["fills"], &["venue"]),
])
.expect_err("duplicate manifest section");
assert!(err.to_string().contains("[venue]"), "{err}");
}

#[tokio::test]
async fn empty_supervisor_returns_no_subscriptions() {
let engine = make_wasmtime_engine();
Expand Down