diff --git a/crates/nexum-runtime/Cargo.toml b/crates/nexum-runtime/Cargo.toml index 19be69b..5f26565 100644 --- a/crates/nexum-runtime/Cargo.toml +++ b/crates/nexum-runtime/Cargo.toml @@ -25,14 +25,9 @@ thiserror.workspace = true async-trait.workspace = true # Newtype boilerplate (`Display`, `AsRef`, `From`) for identity wrappers. derive_more.workspace = true -# `strum::IntoStaticStr` on error enums gives metric labels (`error_kind`) -# free via a snake_case `&'static str` for every variant. Used at -# `tracing::warn!(error_kind = .into(), ...)` sites and -# any `metrics::counter!(... "error_kind" => kind)` recordings, so the -# Prometheus labels stay in lock-step with the Rust enum source of -# truth instead of needing a `match err { ... => "connect" ... }` -# ladder per call site. Pinned via the workspace so every consumer -# moves in lockstep. +# `strum::IntoStaticStr` on the pub error enums: a snake_case +# `&'static str` per variant, published for consumers' metric labels; +# nothing in-crate consumes the conversion. strum.workspace = true tokio.workspace = true # Task lifecycle and graceful shutdown; the sole crate that raw-spawns diff --git a/crates/nexum-runtime/src/host/component/mod.rs b/crates/nexum-runtime/src/host/component/mod.rs index a2cba91..5c31259 100644 --- a/crates/nexum-runtime/src/host/component/mod.rs +++ b/crates/nexum-runtime/src/host/component/mod.rs @@ -44,17 +44,7 @@ impl Clone for Components { mod tests { use super::*; use crate::host::local_store_redb::{LocalStore, ModuleStore}; - - /// Core-only lattice (no extension payload). - #[derive(Clone, Copy, Default)] - struct CoreTypes; - - impl crate::sealed::SealedRuntimeTypes for CoreTypes {} - - impl RuntimeTypes for CoreTypes { - type Store = LocalStore; - type Ext = (); - } + use crate::preset::CoreRuntime; fn store() {} fn handle() {} @@ -64,6 +54,6 @@ mod tests { fn concrete_backends_satisfy_the_traits() { store::(); handle::(); - lattice::(); + lattice::(); } } diff --git a/crates/nexum-runtime/src/host/component/runtime_types.rs b/crates/nexum-runtime/src/host/component/runtime_types.rs index ef7b97a..015af42 100644 --- a/crates/nexum-runtime/src/host/component/runtime_types.rs +++ b/crates/nexum-runtime/src/host/component/runtime_types.rs @@ -5,7 +5,8 @@ use crate::host::component::StateStore; /// Core backend seams a runtime assembly provides, plus the extension slot -/// ([`Ext`](RuntimeTypes::Ext)). Sealed. The chain backend is not a seam. +/// ([`Ext`](RuntimeTypes::Ext)). The marker bound is reserved for semver +/// evolution. The chain backend is not a seam. pub trait RuntimeTypes: crate::sealed::SealedRuntimeTypes + 'static { /// Process-wide store vending per-module handles. type Store: StateStore + Clone + Send + Sync + 'static; diff --git a/crates/nexum-runtime/src/host/extension.rs b/crates/nexum-runtime/src/host/extension.rs index 78f2ee1..fce70db 100644 --- a/crates/nexum-runtime/src/host/extension.rs +++ b/crates/nexum-runtime/src/host/extension.rs @@ -282,7 +282,7 @@ impl HostServices { #[cfg(test)] mod tests { use super::*; - use crate::supervisor::TestTypes; + use crate::preset::CoreRuntime; struct Registry(u64); impl HostService for Registry {} @@ -295,7 +295,7 @@ mod tests { service: Option>, } - impl Extension for ServiceExt { + impl Extension for ServiceExt { fn namespace(&self) -> &'static str { self.namespace } @@ -305,7 +305,7 @@ mod tests { ifaces: &[], } } - fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } fn service(&self) -> Option> { @@ -316,7 +316,7 @@ mod tests { fn ext( namespace: &'static str, service: Arc, - ) -> Arc> { + ) -> Arc> { Arc::new(ServiceExt { namespace, service: Some(service), @@ -340,7 +340,7 @@ mod tests { /// A serviceless extension contributes nothing to the map. #[test] fn serviceless_extension_is_absent() { - let serviceless: Arc> = Arc::new(ServiceExt { + let serviceless: Arc> = Arc::new(ServiceExt { namespace: "quiet", service: None, }); diff --git a/crates/nexum-runtime/src/lib.rs b/crates/nexum-runtime/src/lib.rs index 3f57075..11bbab2 100644 --- a/crates/nexum-runtime/src/lib.rs +++ b/crates/nexum-runtime/src/lib.rs @@ -15,7 +15,7 @@ use alloy_rpc_client as _; use alloy_transport as _; use alloy_transport_ws as _; -/// Sealing markers for [`preset::Runtime`] and +/// Markers reserved for semver evolution of [`preset::Runtime`] and /// [`host::component::RuntimeTypes`]: implement alongside the trait. #[doc(hidden)] pub mod sealed { diff --git a/crates/nexum-runtime/src/preset.rs b/crates/nexum-runtime/src/preset.rs index 289e14a..bbea422 100644 --- a/crates/nexum-runtime/src/preset.rs +++ b/crates/nexum-runtime/src/preset.rs @@ -22,7 +22,8 @@ use crate::host::provider_pool::ProviderPool; /// A bundled runtime assembly: the [`RuntimeTypes`] lattice plus the component /// builders, extensions, and add-ons the launcher needs. /// -/// Sealed: a preset opts in by also implementing the sealing marker. +/// The marker bound is reserved for semver evolution: a preset opts in by +/// also implementing it. pub trait Runtime: crate::sealed::SealedRuntime { /// The lattice the preset assembles. type Types: RuntimeTypes; diff --git a/crates/nexum-runtime/src/runtime/event_loop.rs b/crates/nexum-runtime/src/runtime/event_loop.rs index 3f231f5..0649122 100644 --- a/crates/nexum-runtime/src/runtime/event_loop.rs +++ b/crates/nexum-runtime/src/runtime/event_loop.rs @@ -104,6 +104,15 @@ fn receiver_stream( }) } +/// Bumps `attempt`, hands `(attempt, backoff_ms)` to the site's log line, and +/// sleeps the backoff. +async fn backoff_pause(attempt: &mut u32, log: impl FnOnce(u32, u64)) { + *attempt = attempt.saturating_add(1); + let backoff = backoff_for(*attempt); + log(*attempt, backoff.as_millis() as u64); + tokio::time::sleep(backoff).await; +} + /// Reconnect-aware loop for one chain's block subscription: re-opens the /// stream with exponential backoff after every drop or error. async fn reconnecting_block_task( @@ -167,21 +176,18 @@ async fn reconnecting_block_task( } } warn!(chain_id, "block stream ended (WebSocket dropped?)"); - attempt = attempt.saturating_add(1); } Err(err) => { warn!(chain_id, error = %err, "block subscription failed"); - attempt = attempt.saturating_add(1); } } - let backoff = backoff_for(attempt); - warn!( - chain_id, - attempt, - backoff_ms = backoff.as_millis() as u64, - "reconnecting block subscription after backoff", - ); - tokio::time::sleep(backoff).await; + backoff_pause(&mut attempt, |attempt, backoff_ms| { + warn!( + chain_id, + attempt, backoff_ms, "reconnecting block subscription after backoff", + ); + }) + .await; } } @@ -230,34 +236,34 @@ async fn reconnecting_chain_log_task( let provider = match pool.provider(chain) { Ok(provider) => provider, Err(err) => { - attempt = attempt.saturating_add(1); - let backoff = backoff_for(attempt); - warn!( - module = %module, - chain_id, - error = %err, - attempt, - backoff_ms = backoff.as_millis() as u64, - "chain-log provider lookup failed - retrying after backoff", - ); - tokio::time::sleep(backoff).await; + backoff_pause(&mut attempt, |attempt, backoff_ms| { + warn!( + module = %module, + chain_id, + error = %err, + attempt, + backoff_ms, + "chain-log provider lookup failed - retrying after backoff", + ); + }) + .await; continue; } }; let head = match provider.get_block_number().await { Ok(head) => head, Err(err) => { - attempt = attempt.saturating_add(1); - let backoff = backoff_for(attempt); - warn!( - module = %module, - chain_id, - error = %err, - attempt, - backoff_ms = backoff.as_millis() as u64, - "chain-log head fetch failed - retrying after backoff", - ); - tokio::time::sleep(backoff).await; + backoff_pause(&mut attempt, |attempt, backoff_ms| { + warn!( + module = %module, + chain_id, + error = %err, + attempt, + backoff_ms, + "chain-log head fetch failed - retrying after backoff", + ); + }) + .await; continue; } }; @@ -268,17 +274,17 @@ async fn reconnecting_chain_log_task( Ok(Some(block)) if block.header.hash == t.hash => {} Ok(Some(_)) => invalidated_tail = Some(t.number), Ok(None) | Err(_) => { - attempt = attempt.saturating_add(1); - let backoff = backoff_for(attempt); - warn!( - module = %module, - chain_id, - tail_block = t.number, - attempt, - backoff_ms = backoff.as_millis() as u64, - "chain-log tail hash unconfirmed - retrying after backoff", - ); - tokio::time::sleep(backoff).await; + backoff_pause(&mut attempt, |attempt, backoff_ms| { + warn!( + module = %module, + chain_id, + tail_block = t.number, + attempt, + backoff_ms, + "chain-log tail hash unconfirmed - retrying after backoff", + ); + }) + .await; continue; } } @@ -413,7 +419,6 @@ async fn reconnecting_chain_log_task( } } warn!(module = %module, chain_id, "chain-log poller stream ended - reopening"); - attempt = attempt.saturating_add(1); } Err(err) => { warn!( @@ -422,18 +427,18 @@ async fn reconnecting_chain_log_task( error = %err, "chain-log poller open failed" ); - attempt = attempt.saturating_add(1); } } - let backoff = backoff_for(attempt); - warn!( - module = %module, - chain_id, - attempt, - backoff_ms = backoff.as_millis() as u64, - "reconnecting chain-log poller after backoff", - ); - tokio::time::sleep(backoff).await; + backoff_pause(&mut attempt, |attempt, backoff_ms| { + warn!( + module = %module, + chain_id, + attempt, + backoff_ms, + "reconnecting chain-log poller after backoff", + ); + }) + .await; } } diff --git a/crates/nexum-runtime/src/supervisor/dispatch.rs b/crates/nexum-runtime/src/supervisor/dispatch.rs index 3419e06..c5ba4e3 100644 --- a/crates/nexum-runtime/src/supervisor/dispatch.rs +++ b/crates/nexum-runtime/src/supervisor/dispatch.rs @@ -323,7 +323,7 @@ pub(super) async fn with_dispatch_deadline( .map_err(|_elapsed| DeadlineExceeded(deadline)) } -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug)] pub(super) enum DispatchOutcome { Ok, /// Guest returned a typed `fault` via WIT, not a trap; the module stays alive. diff --git a/crates/nexum-runtime/src/supervisor/lifecycle.rs b/crates/nexum-runtime/src/supervisor/lifecycle.rs index 8fbcfd9..f562856 100644 --- a/crates/nexum-runtime/src/supervisor/lifecycle.rs +++ b/crates/nexum-runtime/src/supervisor/lifecycle.rs @@ -17,8 +17,8 @@ use crate::module_id::ModuleId; use crate::runtime::poison_policy::{PoisonPolicy, should_poison}; use crate::runtime::restart_policy::backoff_for; -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)] -pub(crate) enum LifecycleState { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum LifecycleState { /// Callable; the failure count beside it may still be nonzero. Alive, /// Dead pending a restart once `until` passes. @@ -32,7 +32,7 @@ pub(crate) enum LifecycleState { /// The failure count survives a restart unless the caller resets it; only a /// successful dispatch clears it. Methods take instants; nothing samples the clock. -pub(crate) struct Health { +pub(super) struct Health { state: LifecycleState, failure_count: u32, window: VecDeque, diff --git a/crates/nexum-runtime/src/supervisor/mod.rs b/crates/nexum-runtime/src/supervisor/mod.rs index 7e7fdf3..708d845 100644 --- a/crates/nexum-runtime/src/supervisor/mod.rs +++ b/crates/nexum-runtime/src/supervisor/mod.rs @@ -203,7 +203,6 @@ impl Supervisor { .any(|m| !m.health.dispatchable() && !m.subscriptions.is_empty()) } - #[cfg_attr(not(test), allow(dead_code))] pub fn poisoned_count(&self) -> usize { self.modules .iter() @@ -298,56 +297,5 @@ fn assemble( } } -/// Core-only lattice for the runtime's own tests (`Ext = ()`). -#[cfg(test)] -#[derive(Clone, Copy, Default)] -pub(crate) struct TestTypes; - -#[cfg(test)] -impl crate::sealed::SealedRuntimeTypes for TestTypes {} - -#[cfg(test)] -impl RuntimeTypes for TestTypes { - type Store = crate::host::local_store_redb::LocalStore; - type Ext = (); -} - -#[cfg(test)] -pub(crate) type DefaultSupervisor = Supervisor; - -#[cfg(test)] -use admission::enforce_extension_sections; -#[cfg(test)] -use artifact::read_verified_component; -#[cfg(test)] -use cursors::{chainlog_cursor_key, commit_chain_log_cursor, progress_key, read_chain_log_cursor}; -#[cfg(test)] -use dispatch::with_dispatch_deadline; -#[cfg(test)] -use prepass::{NamespaceLedger, claim_namespace, unconfigured_chain}; -#[cfg(test)] -use store::resolve_module_limits; -#[cfg(test)] -use subscriptions::build_alloy_filter; - -#[cfg(test)] -use crate::bindings::nexum; -#[cfg(test)] -use crate::digest::{ContentDigest, DigestMismatch}; -#[cfg(test)] -use crate::host::extension::{HostService, Installed, ProviderInstance, ProviderKind}; -#[cfg(test)] -use crate::host::logs::LogSource; -#[cfg(test)] -use crate::host::provider_pool::ProviderPool; -#[cfg(test)] -use crate::manifest::{self, CapabilityRegistry}; -#[cfg(test)] -use alloy_chains::Chain; -#[cfg(test)] -use std::time::Duration; -#[cfg(test)] -use tracing_core::Level; - #[cfg(test)] pub(crate) mod tests; diff --git a/crates/nexum-runtime/src/supervisor/role.rs b/crates/nexum-runtime/src/supervisor/role.rs index 60e7f22..07b376f 100644 --- a/crates/nexum-runtime/src/supervisor/role.rs +++ b/crates/nexum-runtime/src/supervisor/role.rs @@ -12,7 +12,7 @@ use crate::module_id::ModuleId; use crate::runtime::poison_policy::PoisonPolicy; /// Keys the per-role metric names and the compile-time tracing field keys. -#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::IntoStaticStr)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(super) enum Role { Module, Adapter, diff --git a/crates/nexum-runtime/src/supervisor/subscriptions.rs b/crates/nexum-runtime/src/supervisor/subscriptions.rs index c50bcb7..2fa8cbf 100644 --- a/crates/nexum-runtime/src/supervisor/subscriptions.rs +++ b/crates/nexum-runtime/src/supervisor/subscriptions.rs @@ -120,8 +120,7 @@ impl From<&alloy_rpc_types_eth::Log> for nexum::host::types::ChainLog { } } -#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] -#[strum(serialize_all = "snake_case")] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub(super) enum FilterError { /// `[[subscriptions]].address` did not parse as an EVM address. diff --git a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs index 4e8a5cb..a868a71 100644 --- a/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs +++ b/crates/nexum-runtime/src/supervisor/tests/chain_gate.rs @@ -209,8 +209,7 @@ fn configured_chains_normalise_named_and_numeric_spellings() { #[test] fn unconfigured_chain_message_says_none_when_engine_toml_declares_no_chains() { let chains = ConfiguredChains::from_config(&EngineConfig::default()); - let msg = crate::supervisor::unconfigured_chain(Role::Module, "example", 424_242, &chains) - .to_string(); + let msg = unconfigured_chain(Role::Module, "example", 424_242, &chains).to_string(); assert!(msg.contains("configured chains: none"), "{msg}"); assert!(!msg.contains("no engine.toml was found"), "{msg}"); } diff --git a/crates/nexum-runtime/src/supervisor/tests/ledger.rs b/crates/nexum-runtime/src/supervisor/tests/ledger.rs index 158e9c1..f9ade48 100644 --- a/crates/nexum-runtime/src/supervisor/tests/ledger.rs +++ b/crates/nexum-runtime/src/supervisor/tests/ledger.rs @@ -7,7 +7,7 @@ use super::*; #[test] fn extension_sections_must_be_claimed() { struct Claiming; - impl Extension for Claiming { + impl Extension for Claiming { fn namespace(&self) -> &'static str { "acme" } @@ -17,14 +17,14 @@ fn extension_sections_must_be_claimed() { ifaces: &[], } } - fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } fn manifest_sections(&self) -> &'static [&'static str] { &["venue"] } } - let extensions: Vec>> = vec![Arc::new(Claiming)]; + let extensions: Vec>> = vec![Arc::new(Claiming)]; let mut sections = manifest::ExtensionSections::new(); sections.insert("venue".into(), toml::Value::Boolean(true)); @@ -46,7 +46,7 @@ fn extension_claims_must_be_unique() { subscriptions: &'static [&'static str], sections: &'static [&'static str], } - impl Extension for Claiming { + impl Extension for Claiming { fn namespace(&self) -> &'static str { self.namespace } @@ -56,7 +56,7 @@ fn extension_claims_must_be_unique() { ifaces: &[], } } - fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } fn subscriptions(&self) -> &'static [&'static str] { @@ -70,7 +70,7 @@ fn extension_claims_must_be_unique() { namespace: &'static str, subscriptions: &'static [&'static str], sections: &'static [&'static str], - ) -> Arc> { + ) -> Arc> { Arc::new(Claiming { namespace, subscriptions, diff --git a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs index 2c9e28b..c3ede10 100644 --- a/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs +++ b/crates/nexum-runtime/src/supervisor/tests/lifecycle.rs @@ -383,18 +383,18 @@ async fn poison_pill_quarantines_module_after_threshold() { struct ScriptedKind(Arc); #[async_trait::async_trait] -impl ProviderKind for ScriptedKind { +impl ProviderKind for ScriptedKind { fn kind(&self) -> &'static str { "scripted-adapter" } - fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } async fn install( &self, - _instance: ProviderInstance<'_, TestTypes>, + _instance: ProviderInstance<'_, CoreRuntime>, _service: &Arc, ) -> anyhow::Result { Ok(if self.0.load(Ordering::SeqCst) { @@ -410,7 +410,7 @@ impl HostService for ScriptedService {} struct ScriptedExtension(Arc); -impl Extension for ScriptedExtension { +impl Extension for ScriptedExtension { fn namespace(&self) -> &'static str { "scripted" } @@ -422,7 +422,7 @@ impl Extension for ScriptedExtension { } } - fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { + fn link(&self, _linker: &mut Linker>) -> anyhow::Result<()> { Ok(()) } @@ -430,16 +430,16 @@ impl Extension for ScriptedExtension { Some(Arc::new(ScriptedService)) } - fn provider(&self) -> Option>> { + fn provider(&self) -> Option>> { Some(Box::new(ScriptedKind(self.0.clone()))) } } /// The shared wiring the sweep reinstalls through, with the scripted kind /// registered. The `TempDir` keeps the local store alive. -fn scripted_shared(live: &Arc) -> (tempfile::TempDir, Shared) { +fn scripted_shared(live: &Arc) -> (tempfile::TempDir, Shared) { let (dir, store) = temp_local_store(); - let extensions: Vec>> = + let extensions: Vec>> = vec![Arc::new(ScriptedExtension(live.clone()))]; let services = HostServices::from_extensions(&extensions).expect("services"); let kinds = diff --git a/crates/nexum-runtime/src/supervisor/tests/mod.rs b/crates/nexum-runtime/src/supervisor/tests/mod.rs index fe36f76..b3147fd 100644 --- a/crates/nexum-runtime/src/supervisor/tests/mod.rs +++ b/crates/nexum-runtime/src/supervisor/tests/mod.rs @@ -10,15 +10,36 @@ mod ledger; mod lifecycle; use std::path::{Path, PathBuf}; +use std::time::Duration; +use alloy_chains::Chain; +use tracing_core::Level; + +use super::admission::enforce_extension_sections; +use super::artifact::read_verified_component; +use super::cursors::{ + chainlog_cursor_key, commit_chain_log_cursor, progress_key, read_chain_log_cursor, +}; +use super::dispatch::with_dispatch_deadline; +use super::prepass::{NamespaceLedger, claim_namespace, unconfigured_chain}; +use super::store::resolve_module_limits; +use super::subscriptions::build_alloy_filter; use super::*; +use crate::bindings::nexum; +use crate::digest::{ContentDigest, DigestMismatch}; use crate::engine_config::ModuleLimits; -use crate::manifest::ResourceSection; +use crate::host::extension::{HostService, Installed, ProviderInstance, ProviderKind}; +use crate::host::logs::LogSource; +use crate::host::provider_pool::ProviderPool; +use crate::manifest::{self, CapabilityRegistry, ResourceSection}; +use crate::preset::CoreRuntime; use crate::test_utils::{ BootScenario, Entry, ManifestSource, Refusal, TestManifest, example_wasm_or_skip, mock_components, module_wasm_or_skip, test_wasmtime_engine, }; +type DefaultSupervisor = Supervisor; + const SEPOLIA: u64 = 11_155_111; /// Path to a manifest checked into the workspace tree. @@ -26,16 +47,17 @@ fn workspace_manifest(relative: &str) -> PathBuf { crate::test_utils::wasm::workspace_root().join(relative) } -fn core_extensions() -> Vec>> { +fn core_extensions() -> Vec>> { Vec::new() } -fn make_linker(engine: &wasmtime::Engine) -> Linker> { - crate::supervisor::build_linker::(engine, &core_extensions()).expect("build_linker") +fn make_linker(engine: &wasmtime::Engine) -> Linker> { + crate::supervisor::build_linker::(engine, &core_extensions()) + .expect("build_linker") } /// An empty chain pool, an empty extension slot, and the given store. -fn test_components(store: crate::host::local_store_redb::LocalStore) -> Components { +fn test_components(store: crate::host::local_store_redb::LocalStore) -> Components { Components { chain: ProviderPool::empty(), store,