v0.3.11-pre.004
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Error code used when the RAW transaction ingest Worker reaches an invalid runtime or lifecycle condition.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("worker_raw_transaction_ingest", "runtime_invalid");
|
||||
/// Error code used when RAW transaction ingest Worker settings violate one bounded runtime invariant.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_INGEST_SETTINGS_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("worker_raw_transaction_ingest", "settings_invalid");
|
||||
|
||||
/// Creates one runtime-domain error carrying only one stable internal condition code.
|
||||
pub(crate) fn runtime_error(condition: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID, "invalid RAW transaction ingest Worker runtime state")
|
||||
.with_context("condition", condition);
|
||||
}
|
||||
|
||||
/// Creates one settings-domain error carrying only the stable invalid field name.
|
||||
pub(crate) fn settings_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_SETTINGS_INVALID, "invalid RAW transaction ingest Worker settings")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,18 +7,28 @@
|
||||
|
||||
//! Source-neutral runtime foundation for continuous KSP RAW transaction ingestion.
|
||||
//!
|
||||
//! This tranche owns only the concrete Worker family identity and validated technical
|
||||
//! settings. Lifecycle, tasks, admission, persistence and snapshots are introduced only
|
||||
//! by their dedicated prereleases; no live source or Transport dependency exists here.
|
||||
//! This tranche owns the concrete Worker family identity, validated technical settings
|
||||
//! and the minimal caller-runtime-owned start/stop lifecycle. Task supervision, bounded
|
||||
//! admission, persistence and latest-value snapshots remain in their dedicated prereleases;
|
||||
//! no live source or Transport dependency exists here.
|
||||
|
||||
mod error;
|
||||
mod identity;
|
||||
mod runtime;
|
||||
mod settings;
|
||||
|
||||
/// Error code used when the RAW transaction ingest Worker reaches an invalid runtime or lifecycle condition.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID;
|
||||
/// Error code used when RAW transaction ingest Worker settings violate one bounded runtime invariant.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_INGEST_SETTINGS_INVALID;
|
||||
/// Stable Worker kind code used by the continuous RAW transaction ingest vertical.
|
||||
pub use self::identity::RAW_TRANSACTION_INGEST_WORKER_KIND_CODE;
|
||||
/// Cloneable external control handle for one continuous RAW transaction ingest Worker.
|
||||
pub use self::runtime::RawTransactionIngestHandle;
|
||||
/// Runtime-neutral boxed future resolving after one RAW transaction ingest Worker has fully reached a terminal lifecycle state.
|
||||
pub use self::runtime::RawTransactionIngestTerminalFuture;
|
||||
/// Entry point owning synchronous validation and task launch for one RAW transaction ingest Worker run.
|
||||
pub use self::runtime::RawTransactionIngestWorker;
|
||||
/// Default bounded admission queue capacity for one RAW transaction ingest Worker.
|
||||
pub use self::settings::DEFAULT_RAW_TRANSACTION_INGEST_ADMISSION_QUEUE_CAPACITY;
|
||||
/// Default number of concurrent Store persistence operations for one RAW transaction ingest Worker.
|
||||
@@ -40,5 +50,7 @@ pub use self::settings::MIN_RAW_TRANSACTION_INGEST_SHUTDOWN_DRAIN_TIMEOUT;
|
||||
/// Validated source-neutral runtime settings for one continuous RAW transaction ingest Worker.
|
||||
pub use self::settings::RawTransactionIngestSettings;
|
||||
|
||||
/// Creates one runtime-domain error without copying runtime/provider/Store values into diagnostics.
|
||||
pub(crate) use self::error::runtime_error;
|
||||
/// Creates one settings-domain error without copying caller-supplied values into diagnostics.
|
||||
pub(crate) use self::error::settings_error;
|
||||
|
||||
157
crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs
Normal file
157
crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs
|
||||
// version: 1
|
||||
|
||||
/// Runtime-neutral boxed future resolving after one RAW transaction ingest Worker has fully reached a terminal lifecycle state.
|
||||
pub type RawTransactionIngestTerminalFuture<'a> =
|
||||
std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = ksp_core_lib::Result<ksp_worker_api::WorkerState>> + std::marker::Send + 'a>>;
|
||||
|
||||
/// Cloneable external control handle for one continuous RAW transaction ingest Worker.
|
||||
#[derive(Clone)]
|
||||
pub struct RawTransactionIngestHandle {
|
||||
stop_sender: tokio::sync::watch::Sender<bool>,
|
||||
stop_token: ksp_worker_api::WorkerStopToken,
|
||||
terminal_receiver: tokio::sync::watch::Receiver<ksp_worker_api::WorkerState>,
|
||||
}
|
||||
|
||||
impl crate::RawTransactionIngestHandle {
|
||||
/// Requests cooperative stop and returns `true` only for the first request accepted by the live runtime.
|
||||
#[must_use]
|
||||
pub fn request_stop(&self) -> bool {
|
||||
if !self.stop_token.request_stop() {
|
||||
return false;
|
||||
}
|
||||
return self.stop_sender.send(true).is_ok();
|
||||
}
|
||||
|
||||
/// Waits until the private runtime task has published and closed one terminal lifecycle state.
|
||||
#[must_use]
|
||||
pub fn wait_terminal(&self) -> crate::RawTransactionIngestTerminalFuture<'_> {
|
||||
let mut receiver = self.terminal_receiver.clone();
|
||||
return std::boxed::Box::pin(async move {
|
||||
loop {
|
||||
let current = *receiver.borrow();
|
||||
if current.is_terminal() {
|
||||
let changed = receiver.changed().await;
|
||||
if changed.is_err() {
|
||||
return std::result::Result::Ok(current);
|
||||
}
|
||||
return std::result::Result::Err(crate::runtime_error("terminal.changed_after_terminal"));
|
||||
}
|
||||
let changed = receiver.changed().await;
|
||||
if changed.is_err() {
|
||||
return std::result::Result::Err(crate::runtime_error("terminal.closed_before_terminal"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawTransactionIngestHandle {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let state = *self.terminal_receiver.borrow();
|
||||
return formatter
|
||||
.debug_struct("RawTransactionIngestHandle")
|
||||
.field("stop_requested", &self.stop_token.is_stop_requested())
|
||||
.field("state", &state)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry point owning synchronous validation and task launch for one RAW transaction ingest Worker run.
|
||||
pub struct RawTransactionIngestWorker;
|
||||
|
||||
impl crate::RawTransactionIngestWorker {
|
||||
/// Starts one Worker on the caller-owned current Tokio runtime while retaining the caller-owned Store facade through an `Arc`.
|
||||
pub fn start(
|
||||
settings: crate::RawTransactionIngestSettings,
|
||||
store: std::sync::Arc<ksp_store_lib::Store>,
|
||||
) -> ksp_core_lib::Result<crate::RawTransactionIngestHandle> {
|
||||
let runtime = match current_runtime_handle() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let store_snapshot = store.runtime_snapshot();
|
||||
if let std::result::Result::Err(error) = validate_store_network(&settings, store_snapshot.network()) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return start_foundation(settings, runtime, std::option::Option::Some(store));
|
||||
}
|
||||
}
|
||||
|
||||
fn current_runtime_handle() -> ksp_core_lib::Result<tokio::runtime::Handle> {
|
||||
return match tokio::runtime::Handle::try_current() {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crate::runtime_error("start.runtime_unavailable")),
|
||||
};
|
||||
}
|
||||
|
||||
fn finish_stopped(lifecycle: &mut ksp_worker_api::WorkerLifecycle, sender: &tokio::sync::watch::Sender<ksp_worker_api::WorkerState>) {
|
||||
if lifecycle.mark_stopping().is_err() {
|
||||
sender.send_replace(ksp_worker_api::WorkerState::Faulted(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID));
|
||||
return;
|
||||
}
|
||||
sender.send_replace(lifecycle.state());
|
||||
if lifecycle.mark_stopped().is_err() {
|
||||
sender.send_replace(ksp_worker_api::WorkerState::Faulted(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID));
|
||||
return;
|
||||
}
|
||||
sender.send_replace(lifecycle.state());
|
||||
return;
|
||||
}
|
||||
|
||||
async fn run_foundation(
|
||||
mut lifecycle: ksp_worker_api::WorkerLifecycle,
|
||||
_store_guard: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
|
||||
mut stop_receiver: tokio::sync::watch::Receiver<bool>,
|
||||
terminal_sender: tokio::sync::watch::Sender<ksp_worker_api::WorkerState>,
|
||||
) {
|
||||
if *stop_receiver.borrow() {
|
||||
finish_stopped(&mut lifecycle, &terminal_sender);
|
||||
return;
|
||||
}
|
||||
if lifecycle.mark_running().is_err() {
|
||||
terminal_sender.send_replace(ksp_worker_api::WorkerState::Faulted(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID));
|
||||
return;
|
||||
}
|
||||
terminal_sender.send_replace(lifecycle.state());
|
||||
loop {
|
||||
let changed = stop_receiver.changed().await;
|
||||
if changed.is_err() || *stop_receiver.borrow() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
finish_stopped(&mut lifecycle, &terminal_sender);
|
||||
return;
|
||||
}
|
||||
|
||||
fn start_foundation(
|
||||
settings: crate::RawTransactionIngestSettings,
|
||||
runtime: tokio::runtime::Handle,
|
||||
store_guard: std::option::Option<std::sync::Arc<ksp_store_lib::Store>>,
|
||||
) -> ksp_core_lib::Result<crate::RawTransactionIngestHandle> {
|
||||
let kind = match ksp_worker_api::WorkerKindCode::new(crate::RAW_TRANSACTION_INGEST_WORKER_KIND_CODE) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("start.worker_kind_invalid")),
|
||||
};
|
||||
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(settings.worker_id().clone(), kind);
|
||||
if lifecycle.start().is_err() {
|
||||
return std::result::Result::Err(crate::runtime_error("start.lifecycle_invalid"));
|
||||
}
|
||||
let stop_token = ksp_worker_api::WorkerStopToken::new();
|
||||
let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false);
|
||||
let (terminal_sender, terminal_receiver) = tokio::sync::watch::channel(lifecycle.state());
|
||||
let handle = crate::RawTransactionIngestHandle { stop_sender, stop_token, terminal_receiver };
|
||||
std::mem::drop(runtime.spawn(run_foundation(lifecycle, store_guard, stop_receiver, terminal_sender)));
|
||||
return std::result::Result::Ok(handle);
|
||||
}
|
||||
|
||||
fn validate_store_network(settings: &crate::RawTransactionIngestSettings, store_network: &ksp_store_lib::RawNetworkId) -> ksp_core_lib::Result<()> {
|
||||
if settings.network() != store_network {
|
||||
return std::result::Result::Err(crate::runtime_error("start.store_network_mismatch"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/runtime.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Dependency firewall canaries for the RAW transaction ingest Worker foundation.
|
||||
|
||||
@@ -52,20 +52,30 @@ fn pre_002_manifest_keeps_forbidden_layers_and_live_sources_out() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_source_surface_opens_identity_and_settings_without_runtime_behavior() {
|
||||
fn pre_004_source_surface_opens_lifecycle_without_supervisor_admission_or_live_sources() {
|
||||
let root = include_str!("../src/lib.rs");
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
for required in [
|
||||
"mod error;",
|
||||
"mod identity;",
|
||||
"mod settings;",
|
||||
"pub use self::error::ERROR_CODE_RAW_TRANSACTION_INGEST_SETTINGS_INVALID;",
|
||||
"pub use self::identity::RAW_TRANSACTION_INGEST_WORKER_KIND_CODE;",
|
||||
"pub use self::settings::RawTransactionIngestSettings;",
|
||||
"mod runtime;",
|
||||
"pub use self::runtime::RawTransactionIngestHandle;",
|
||||
"pub use self::runtime::RawTransactionIngestTerminalFuture;",
|
||||
"pub use self::runtime::RawTransactionIngestWorker;",
|
||||
"pub fn start(",
|
||||
"pub fn request_stop(&self) -> bool",
|
||||
"pub fn wait_terminal(&self)",
|
||||
] {
|
||||
assert!(root.contains(required), "required pre.003 crate-root contract missing: {required}");
|
||||
assert!(root.contains(required) || runtime.contains(required), "required pre.004 runtime contract missing: {required}");
|
||||
}
|
||||
for forbidden in ["pub mod ", "tokio::", "start(", "spawn(", "JoinHandle", "JoinSet", "mpsc::", "watch::", "ksp_onchain_transport_lib::"] {
|
||||
assert!(!root.contains(forbidden), "pre.003 opened runtime behavior too early: {forbidden}");
|
||||
for forbidden in [
|
||||
"JoinSet",
|
||||
"mpsc::",
|
||||
"canonicalize_raw_transaction",
|
||||
"persist_raw_transaction",
|
||||
"ksp_onchain_transport_lib::",
|
||||
"RawTransactionIngestSnapshot",
|
||||
"WorkerSnapshotSource",
|
||||
] {
|
||||
assert!(!root.contains(forbidden) && !runtime.contains(forbidden), "pre.004 opened later runtime scope too early: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/tests/public_api.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! External public-surface proofs for the RAW transaction ingest Worker identity and settings foundation.
|
||||
|
||||
@@ -72,3 +72,22 @@ fn pre_003_runtime_bound_errors_are_stable_and_do_not_echo_values() {
|
||||
assert!(!std::format!("{error:?}").contains("raw-ingest-mainnet-001"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_start_handle_and_terminal_future_are_consumable_without_public_join_handle() {
|
||||
let _start: fn(
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestSettings,
|
||||
std::sync::Arc<ksp_store_lib::Store>,
|
||||
) -> ksp_core_lib::Result<ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle> =
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestWorker::start;
|
||||
let _request_stop: fn(&ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle) -> bool =
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle::request_stop;
|
||||
let _wait_terminal: for<'a> fn(
|
||||
&'a ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle,
|
||||
) -> ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestTerminalFuture<'a> =
|
||||
ksp_worker_raw_transaction_ingest_lib::RawTransactionIngestHandle::wait_terminal;
|
||||
let root = include_str!("../src/lib.rs");
|
||||
assert!(!root.contains("JoinHandle"));
|
||||
assert!(!root.contains("JoinSet"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs
|
||||
// version: 1
|
||||
|
||||
fn settings(network: &str) -> std::option::Option<crate::RawTransactionIngestSettings> {
|
||||
let network = match ksp_store_lib::RawNetworkId::new(network) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let worker_id = match ksp_worker_api::WorkerId::new("raw-ingest-runtime-001") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return std::option::Option::Some(crate::RawTransactionIngestSettings::with_defaults(network, worker_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_current_runtime_is_required_before_spawn() {
|
||||
let result = super::current_runtime_handle();
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
assert_eq!(error.context().len(), 1);
|
||||
assert_eq!(error.context()[0].value(), "start.runtime_unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_store_network_mismatch_is_rejected_without_echoing_network_values() {
|
||||
let settings = match settings("mainnet") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let other = match ksp_store_lib::RawNetworkId::new("devnet") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let result = super::validate_store_network(&settings, &other);
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(value) => value,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID);
|
||||
let rendered = std::format!("{error:?}");
|
||||
assert!(!rendered.contains("mainnet"));
|
||||
assert!(!rendered.contains("devnet"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_immediate_stop_is_idempotent_and_reaches_stopped_terminal() {
|
||||
let settings = match settings("mainnet") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let handle = match super::start_foundation(settings, tokio::runtime::Handle::current(), std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(*handle.terminal_receiver.borrow(), ksp_worker_api::WorkerState::Starting);
|
||||
assert!(handle.request_stop());
|
||||
assert!(!handle.request_stop());
|
||||
let terminal = handle.wait_terminal().await;
|
||||
assert!(terminal.is_ok());
|
||||
let terminal = match terminal {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(terminal, ksp_worker_api::WorkerState::Stopped);
|
||||
assert!(terminal.is_terminal());
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_running_lifecycle_stops_through_cloned_handle() {
|
||||
let settings = match settings("mainnet") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let handle = match super::start_foundation(settings, tokio::runtime::Handle::current(), std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
tokio::task::yield_now().await;
|
||||
assert_eq!(*handle.terminal_receiver.borrow(), ksp_worker_api::WorkerState::Running);
|
||||
let clone = handle.clone();
|
||||
assert!(clone.request_stop());
|
||||
assert!(!handle.request_stop());
|
||||
let terminal = handle.wait_terminal().await;
|
||||
let terminal = match terminal {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(terminal, ksp_worker_api::WorkerState::Stopped);
|
||||
return;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn pre_004_dropping_last_control_handle_causes_private_runtime_exit() {
|
||||
let settings = match settings("mainnet") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let handle = match super::start_foundation(settings, tokio::runtime::Handle::current(), std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut observer = handle.terminal_receiver.clone();
|
||||
std::mem::drop(handle);
|
||||
loop {
|
||||
let current = *observer.borrow();
|
||||
if current.is_terminal() {
|
||||
let closed = observer.changed().await;
|
||||
assert!(closed.is_err());
|
||||
assert_eq!(current, ksp_worker_api::WorkerState::Stopped);
|
||||
return;
|
||||
}
|
||||
let changed = observer.changed().await;
|
||||
assert!(changed.is_ok(), "runtime closed before publishing terminal state");
|
||||
if changed.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user