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
11 changes: 3 additions & 8 deletions crates/nexum-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <variant_name>.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
Expand Down
14 changes: 2 additions & 12 deletions crates/nexum-runtime/src/host/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,7 @@ impl<T: RuntimeTypes> Clone for Components<T> {
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<T: StateStore>() {}
fn handle<T: StateHandle>() {}
Expand All @@ -64,6 +54,6 @@ mod tests {
fn concrete_backends_satisfy_the_traits() {
store::<LocalStore>();
handle::<ModuleStore>();
lattice::<CoreTypes>();
lattice::<CoreRuntime>();
}
}
3 changes: 2 additions & 1 deletion crates/nexum-runtime/src/host/component/runtime_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Handle: Send + Sync + 'static> + Clone + Send + Sync + 'static;
Expand Down
10 changes: 5 additions & 5 deletions crates/nexum-runtime/src/host/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand All @@ -295,7 +295,7 @@ mod tests {
service: Option<Arc<dyn HostService>>,
}

impl Extension<TestTypes> for ServiceExt {
impl Extension<CoreRuntime> for ServiceExt {
fn namespace(&self) -> &'static str {
self.namespace
}
Expand All @@ -305,7 +305,7 @@ mod tests {
ifaces: &[],
}
}
fn link(&self, _linker: &mut Linker<HostState<TestTypes>>) -> anyhow::Result<()> {
fn link(&self, _linker: &mut Linker<HostState<CoreRuntime>>) -> anyhow::Result<()> {
Ok(())
}
fn service(&self) -> Option<Arc<dyn HostService>> {
Expand All @@ -316,7 +316,7 @@ mod tests {
fn ext(
namespace: &'static str,
service: Arc<dyn HostService>,
) -> Arc<dyn Extension<TestTypes>> {
) -> Arc<dyn Extension<CoreRuntime>> {
Arc::new(ServiceExt {
namespace,
service: Some(service),
Expand All @@ -340,7 +340,7 @@ mod tests {
/// A serviceless extension contributes nothing to the map.
#[test]
fn serviceless_extension_is_absent() {
let serviceless: Arc<dyn Extension<TestTypes>> = Arc::new(ServiceExt {
let serviceless: Arc<dyn Extension<CoreRuntime>> = Arc::new(ServiceExt {
namespace: "quiet",
service: None,
});
Expand Down
2 changes: 1 addition & 1 deletion crates/nexum-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion crates/nexum-runtime/src/preset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
113 changes: 59 additions & 54 deletions crates/nexum-runtime/src/runtime/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ fn receiver_stream<T: Send + 'static>(
})
}

/// 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(
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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;
}
};
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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!(
Expand All @@ -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;
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/nexum-runtime/src/supervisor/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ pub(super) async fn with_dispatch_deadline<F: std::future::Future>(
.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.
Expand Down
6 changes: 3 additions & 3 deletions crates/nexum-runtime/src/supervisor/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Instant>,
Expand Down
52 changes: 0 additions & 52 deletions crates/nexum-runtime/src/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ impl<T: RuntimeTypes> Supervisor<T> {
.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()
Expand Down Expand Up @@ -298,56 +297,5 @@ fn assemble<T: RuntimeTypes>(
}
}

/// 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<TestTypes>;

#[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;
2 changes: 1 addition & 1 deletion crates/nexum-runtime/src/supervisor/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions crates/nexum-runtime/src/supervisor/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading