155
Cargo.lock
generated
155
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.71"
|
version = "1.0.71"
|
||||||
@@ -83,6 +92,21 @@ version = "0.4.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -100,9 +124,14 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"rand",
|
"rand",
|
||||||
|
"test-case",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-test",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"tracing-test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -215,6 +244,23 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -278,6 +324,59 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-case"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
|
||||||
|
dependencies = [
|
||||||
|
"test-case-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-case-core"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "test-case-macros"
|
||||||
|
version = "3.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"test-case-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
@@ -315,6 +414,28 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-test"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -330,11 +451,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
@@ -343,9 +463,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.24"
|
version = "0.1.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -379,14 +499,39 @@ version = "0.3.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-test"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68"
|
||||||
|
dependencies = [
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"tracing-test-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-test-macro"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
thiserror = "2"
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
test-case = "3"
|
||||||
|
tokio-test = "0.4"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing-test = { version = "0.2", features = ["no-env-filter"] }
|
||||||
|
|||||||
53
crates/noprocess/src/error.rs
Normal file
53
crates/noprocess/src/error.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::HandleID;
|
||||||
|
|
||||||
|
/// A boxed error type for process handler return values.
|
||||||
|
/// This allows users to return any error type from their process handlers.
|
||||||
|
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
/// Result type for process handler functions.
|
||||||
|
pub type ProcessResult = Result<(), BoxError>;
|
||||||
|
|
||||||
|
/// Error type for noprocess library operations.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The process runner has terminated and is no longer accepting commands.
|
||||||
|
#[error("runner terminated")]
|
||||||
|
RunnerTerminated,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about a process execution failure.
|
||||||
|
/// Used in the on_error callback to provide details about what went wrong.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProcessFailure {
|
||||||
|
pub handle_id: HandleID,
|
||||||
|
pub kind: ProcessFailureKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of process execution failure.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ProcessFailureKind {
|
||||||
|
/// The process returned an error.
|
||||||
|
Returned(String),
|
||||||
|
/// The process panicked.
|
||||||
|
Panicked(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ProcessFailure {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self.kind {
|
||||||
|
ProcessFailureKind::Returned(e) => {
|
||||||
|
write!(f, "process {} returned error: {}", self.handle_id, e)
|
||||||
|
}
|
||||||
|
ProcessFailureKind::Panicked(e) => {
|
||||||
|
write!(f, "process {} panicked: {}", self.handle_id, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for ProcessFailure {}
|
||||||
|
|
||||||
|
/// Type alias for error handler callback.
|
||||||
|
pub type ErrorHandler = Arc<dyn Fn(ProcessFailure) + Send + Sync + 'static>;
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
|
mod error;
|
||||||
mod handle_id;
|
mod handle_id;
|
||||||
mod process;
|
mod process;
|
||||||
mod process_handler;
|
mod process_handler;
|
||||||
mod shutdown_config;
|
mod shutdown_config;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
use std::{collections::BTreeMap, sync::Arc};
|
use std::{collections::BTreeMap, sync::Arc};
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
pub use error::{BoxError, Error, ProcessFailure, ProcessFailureKind, ProcessResult};
|
||||||
pub use handle_id::HandleID;
|
pub use handle_id::HandleID;
|
||||||
pub use process::Process;
|
pub use process::{Process, ProcessBuilder};
|
||||||
pub use process_handler::ProcessHandler;
|
pub use process_handler::ProcessHandler;
|
||||||
pub use shutdown_config::ShutdownConfig;
|
pub use shutdown_config::ShutdownConfig;
|
||||||
|
|
||||||
@@ -31,25 +36,26 @@ impl ProcessManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_process(&self, process: Process) {
|
pub async fn add_process(&self, process: Process) -> HandleID {
|
||||||
let handle_id = process.handle_id().clone();
|
let handle_id = process.handle_id().clone();
|
||||||
info!(%handle_id, "adding process to manager");
|
info!(%handle_id, "adding process to manager");
|
||||||
self.inner.lock().await.add_process(process);
|
self.inner.lock().await.add_process(process);
|
||||||
|
handle_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_process(&self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
pub async fn start_process(&self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
self.inner.lock().await.start_process(handle_id).await
|
self.inner.lock().await.start_process(handle_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restart_process(&self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
pub async fn restart_process(&self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
self.inner.lock().await.restart_process(handle_id).await
|
self.inner.lock().await.restart_process(handle_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_process(&self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
pub async fn stop_process(&self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
self.inner.lock().await.stop_process(handle_id).await
|
self.inner.lock().await.stop_process(handle_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn kill_process(&self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
pub async fn kill_process(&self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
self.inner.lock().await.kill_process(handle_id).await
|
self.inner.lock().await.kill_process(handle_id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +70,7 @@ impl Inner {
|
|||||||
self.handles.insert(process.handle_id().clone(), process);
|
self.handles.insert(process.handle_id().clone(), process);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_process(&mut self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
async fn start_process(&mut self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
let Some(handle) = self.handles.get_mut(handle_id) else {
|
let Some(handle) = self.handles.get_mut(handle_id) else {
|
||||||
warn!(%handle_id, "process not found");
|
warn!(%handle_id, "process not found");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -75,7 +81,7 @@ impl Inner {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restart_process(&mut self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
async fn restart_process(&mut self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
let Some(handle) = self.handles.get_mut(handle_id) else {
|
let Some(handle) = self.handles.get_mut(handle_id) else {
|
||||||
warn!(%handle_id, "process not found");
|
warn!(%handle_id, "process not found");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -86,7 +92,7 @@ impl Inner {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_process(&mut self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
async fn stop_process(&mut self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
let Some(handle) = self.handles.get_mut(handle_id) else {
|
let Some(handle) = self.handles.get_mut(handle_id) else {
|
||||||
warn!(%handle_id, "process not found");
|
warn!(%handle_id, "process not found");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -97,7 +103,7 @@ impl Inner {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn kill_process(&mut self, handle_id: &HandleID) -> anyhow::Result<Option<()>> {
|
async fn kill_process(&mut self, handle_id: &HandleID) -> Result<Option<()>, Error> {
|
||||||
let Some(handle) = self.handles.get_mut(handle_id) else {
|
let Some(handle) = self.handles.get_mut(handle_id) else {
|
||||||
warn!(%handle_id, "process not found");
|
warn!(%handle_id, "process not found");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use tracing::{Instrument, debug, debug_span, info, trace, warn};
|
use tracing::{Instrument, debug, debug_span, info, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
error::{Error, ErrorHandler, ProcessFailure, ProcessFailureKind, ProcessResult},
|
||||||
handle_id::{generate_short_id, HandleID},
|
handle_id::{generate_short_id, HandleID},
|
||||||
process_handler::{IntoProcess, ProcessHandler, SharedProcess},
|
process_handler::{IntoProcess, ProcessHandler, SharedProcess},
|
||||||
shutdown_config::ShutdownConfig,
|
shutdown_config::ShutdownConfig,
|
||||||
@@ -38,13 +39,23 @@ pub struct Process {
|
|||||||
|
|
||||||
impl Process {
|
impl Process {
|
||||||
pub fn new(process: impl ProcessHandler) -> Self {
|
pub fn new(process: impl ProcessHandler) -> Self {
|
||||||
Self::new_with_handle(HandleID::new_random(), process)
|
ProcessBuilder::new(process).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_handle(handle_id: impl Into<HandleID>, process: impl ProcessHandler) -> Self {
|
pub fn new_with_handle(handle_id: impl Into<HandleID>, process: impl ProcessHandler) -> Self {
|
||||||
let handle_id = handle_id.into();
|
ProcessBuilder::new(process).handle_id(handle_id).build()
|
||||||
let process = process.into_process();
|
}
|
||||||
let shutdown_config = ShutdownConfig::default();
|
|
||||||
|
pub fn builder(process: impl ProcessHandler) -> ProcessBuilder {
|
||||||
|
ProcessBuilder::new(process)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
handle_id: HandleID,
|
||||||
|
process: SharedProcess,
|
||||||
|
shutdown_config: ShutdownConfig,
|
||||||
|
error_handler: Option<ErrorHandler>,
|
||||||
|
) -> Self {
|
||||||
let (command_tx, command_rx) = mpsc::channel(1);
|
let (command_tx, command_rx) = mpsc::channel(1);
|
||||||
let (event_tx, event_rx) = watch::channel(ProcessState::Pending);
|
let (event_tx, event_rx) = watch::channel(ProcessState::Pending);
|
||||||
|
|
||||||
@@ -52,8 +63,15 @@ impl Process {
|
|||||||
|
|
||||||
let span = debug_span!("process_loop", handle_id = %handle_id);
|
let span = debug_span!("process_loop", handle_id = %handle_id);
|
||||||
let handle = tokio::spawn(
|
let handle = tokio::spawn(
|
||||||
Self::run_loop(handle_id.clone(), process, command_rx, shutdown_config, event_tx)
|
Self::run_loop(
|
||||||
.instrument(span),
|
handle_id.clone(),
|
||||||
|
process,
|
||||||
|
command_rx,
|
||||||
|
shutdown_config,
|
||||||
|
event_tx,
|
||||||
|
error_handler,
|
||||||
|
)
|
||||||
|
.instrument(span),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -63,13 +81,59 @@ impl Process {
|
|||||||
task: handle,
|
task: handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProcessBuilder {
|
||||||
|
process: SharedProcess,
|
||||||
|
handle_id: Option<HandleID>,
|
||||||
|
shutdown_config: Option<ShutdownConfig>,
|
||||||
|
error_handler: Option<ErrorHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessBuilder {
|
||||||
|
pub fn new(process: impl ProcessHandler) -> Self {
|
||||||
|
Self {
|
||||||
|
process: process.into_process(),
|
||||||
|
handle_id: None,
|
||||||
|
shutdown_config: None,
|
||||||
|
error_handler: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_id(mut self, handle_id: impl Into<HandleID>) -> Self {
|
||||||
|
self.handle_id = Some(handle_id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown_config(mut self, config: ShutdownConfig) -> Self {
|
||||||
|
self.shutdown_config = Some(config);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set an error handler that will be called when the process errors or panics
|
||||||
|
pub fn on_error<F>(mut self, handler: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(ProcessFailure) + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.error_handler = Some(std::sync::Arc::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Process {
|
||||||
|
let handle_id = self.handle_id.unwrap_or_else(HandleID::new_random);
|
||||||
|
let shutdown_config = self.shutdown_config.unwrap_or_default();
|
||||||
|
Process::create(handle_id, self.process, shutdown_config, self.error_handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Process {
|
||||||
async fn run_loop(
|
async fn run_loop(
|
||||||
handle_id: HandleID,
|
handle_id: HandleID,
|
||||||
process: SharedProcess,
|
process: SharedProcess,
|
||||||
mut commands: mpsc::Receiver<ProcessCommand>,
|
mut commands: mpsc::Receiver<ProcessCommand>,
|
||||||
shutdown_config: ShutdownConfig,
|
shutdown_config: ShutdownConfig,
|
||||||
state: watch::Sender<ProcessState>,
|
state: watch::Sender<ProcessState>,
|
||||||
|
error_handler: Option<ErrorHandler>,
|
||||||
) {
|
) {
|
||||||
let mut current_task: Option<RunningTask> = None;
|
let mut current_task: Option<RunningTask> = None;
|
||||||
|
|
||||||
@@ -99,6 +163,7 @@ impl Process {
|
|||||||
&handle_id,
|
&handle_id,
|
||||||
&state,
|
&state,
|
||||||
&shutdown_config,
|
&shutdown_config,
|
||||||
|
&error_handler,
|
||||||
&mut current_task
|
&mut current_task
|
||||||
).await;
|
).await;
|
||||||
}
|
}
|
||||||
@@ -136,43 +201,39 @@ impl Process {
|
|||||||
trace!("exiting run loop");
|
trace!("exiting run loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&mut self) -> anyhow::Result<()> {
|
pub async fn start(&mut self) -> Result<(), Error> {
|
||||||
info!(handle_id = %self.handle_id, "starting process");
|
info!(handle_id = %self.handle_id, "starting process");
|
||||||
self.command_tx
|
self.command_tx
|
||||||
.send(ProcessCommand::Start)
|
.send(ProcessCommand::Start)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))
|
.map_err(|_| Error::RunnerTerminated)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop(&mut self) -> anyhow::Result<()> {
|
pub async fn stop(&mut self) -> Result<(), Error> {
|
||||||
info!(handle_id = %self.handle_id, "stopping process");
|
info!(handle_id = %self.handle_id, "stopping process");
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
|
||||||
self.command_tx
|
self.command_tx
|
||||||
.send(ProcessCommand::Stop { response: resp_tx })
|
.send(ProcessCommand::Stop { response: resp_tx })
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))?;
|
.map_err(|_| Error::RunnerTerminated)?;
|
||||||
|
|
||||||
resp_rx
|
resp_rx.await.map_err(|_| Error::RunnerTerminated)
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn kill(&mut self) -> anyhow::Result<()> {
|
pub async fn kill(&mut self) -> Result<(), Error> {
|
||||||
info!(handle_id = %self.handle_id, "killing process");
|
info!(handle_id = %self.handle_id, "killing process");
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
|
|
||||||
self.command_tx
|
self.command_tx
|
||||||
.send(ProcessCommand::Kill { response: resp_tx })
|
.send(ProcessCommand::Kill { response: resp_tx })
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))?;
|
.map_err(|_| Error::RunnerTerminated)?;
|
||||||
|
|
||||||
resp_rx
|
resp_rx.await.map_err(|_| Error::RunnerTerminated)
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restart(&mut self) -> anyhow::Result<()> {
|
pub async fn restart(&mut self) -> Result<(), Error> {
|
||||||
info!(handle_id = %self.handle_id, "restarting process");
|
info!(handle_id = %self.handle_id, "restarting process");
|
||||||
|
|
||||||
// Stop without the info log
|
// Stop without the info log
|
||||||
@@ -180,16 +241,14 @@ impl Process {
|
|||||||
self.command_tx
|
self.command_tx
|
||||||
.send(ProcessCommand::Stop { response: resp_tx })
|
.send(ProcessCommand::Stop { response: resp_tx })
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))?;
|
.map_err(|_| Error::RunnerTerminated)?;
|
||||||
resp_rx
|
resp_rx.await.map_err(|_| Error::RunnerTerminated)?;
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))?;
|
|
||||||
|
|
||||||
// Start without the info log
|
// Start without the info log
|
||||||
self.command_tx
|
self.command_tx
|
||||||
.send(ProcessCommand::Start)
|
.send(ProcessCommand::Start)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow::anyhow!("runner terminated"))
|
.map_err(|_| Error::RunnerTerminated)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn wait_for(&self, desired: ProcessState) {
|
pub(crate) async fn wait_for(&self, desired: ProcessState) {
|
||||||
@@ -239,11 +298,12 @@ impl Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_task_completion(
|
async fn handle_task_completion(
|
||||||
result: Result<anyhow::Result<()>, tokio::task::JoinError>,
|
result: Result<ProcessResult, tokio::task::JoinError>,
|
||||||
process: SharedProcess,
|
process: SharedProcess,
|
||||||
handle_id: &HandleID,
|
handle_id: &HandleID,
|
||||||
state: &watch::Sender<ProcessState>,
|
state: &watch::Sender<ProcessState>,
|
||||||
config: &ShutdownConfig,
|
config: &ShutdownConfig,
|
||||||
|
error_handler: &Option<ErrorHandler>,
|
||||||
current_task: &mut Option<RunningTask>,
|
current_task: &mut Option<RunningTask>,
|
||||||
) {
|
) {
|
||||||
match result {
|
match result {
|
||||||
@@ -264,6 +324,12 @@ impl Process {
|
|||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
warn!(error = %e, "process returned error");
|
warn!(error = %e, "process returned error");
|
||||||
|
if let Some(handler) = error_handler {
|
||||||
|
handler(ProcessFailure {
|
||||||
|
handle_id: handle_id.clone(),
|
||||||
|
kind: ProcessFailureKind::Returned(e.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
let _ = state.send(ProcessState::Errored);
|
let _ = state.send(ProcessState::Errored);
|
||||||
}
|
}
|
||||||
Err(e) if e.is_cancelled() => {
|
Err(e) if e.is_cancelled() => {
|
||||||
@@ -271,6 +337,12 @@ impl Process {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(error = %e, "process panicked");
|
warn!(error = %e, "process panicked");
|
||||||
|
if let Some(handler) = error_handler {
|
||||||
|
handler(ProcessFailure {
|
||||||
|
handle_id: handle_id.clone(),
|
||||||
|
kind: ProcessFailureKind::Panicked(e.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
let _ = state.send(ProcessState::Errored);
|
let _ = state.send(ProcessState::Errored);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +357,7 @@ impl Drop for Process {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct RunningTask {
|
struct RunningTask {
|
||||||
handle: JoinHandle<anyhow::Result<()>>,
|
handle: JoinHandle<ProcessResult>,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
invocation_id: InvocationID,
|
invocation_id: InvocationID,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,28 @@ use std::{future::Future, pin::Pin, sync::Arc};
|
|||||||
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::error::ProcessResult;
|
||||||
|
|
||||||
#[allow(async_fn_in_trait)]
|
#[allow(async_fn_in_trait)]
|
||||||
pub trait ProcessHandler: Send + Sync + 'static {
|
pub trait ProcessHandler: Send + Sync + 'static {
|
||||||
fn call(
|
fn call(
|
||||||
&self,
|
&self,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> impl Future<Output = anyhow::Result<()>> + Send;
|
) -> impl Future<Output = ProcessResult> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait AsyncProcess: Send + Sync {
|
pub(crate) trait AsyncProcess: Send + Sync {
|
||||||
fn call_async(
|
fn call_async(
|
||||||
&self,
|
&self,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + '_>>;
|
) -> Pin<Box<dyn Future<Output = ProcessResult> + Send + '_>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E: ProcessHandler> AsyncProcess for E {
|
impl<E: ProcessHandler> AsyncProcess for E {
|
||||||
fn call_async(
|
fn call_async(
|
||||||
&self,
|
&self,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + '_>> {
|
) -> Pin<Box<dyn Future<Output = ProcessResult> + Send + '_>> {
|
||||||
Box::pin(self.call(cancellation))
|
Box::pin(self.call(cancellation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
955
crates/noprocess/src/tests.rs
Normal file
955
crates/noprocess/src/tests.rs
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::sync::Barrier;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::{process::ProcessState, BoxError, Process, ProcessHandler, ProcessManager, ProcessResult};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// A process that runs until cancelled
|
||||||
|
struct LongRunningProcess {
|
||||||
|
started: Arc<AtomicUsize>,
|
||||||
|
stopped: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LongRunningProcess {
|
||||||
|
fn new() -> (Self, Arc<AtomicUsize>, Arc<AtomicUsize>) {
|
||||||
|
let started = Arc::new(AtomicUsize::new(0));
|
||||||
|
let stopped = Arc::new(AtomicUsize::new(0));
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
started: started.clone(),
|
||||||
|
stopped: stopped.clone(),
|
||||||
|
},
|
||||||
|
started,
|
||||||
|
stopped,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessHandler for LongRunningProcess {
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> impl std::future::Future<Output = ProcessResult> + Send {
|
||||||
|
let started = self.started.clone();
|
||||||
|
let stopped = self.stopped.clone();
|
||||||
|
async move {
|
||||||
|
started.fetch_add(1, Ordering::SeqCst);
|
||||||
|
cancellation.cancelled().await;
|
||||||
|
stopped.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A process that returns an error
|
||||||
|
struct ErroringProcess;
|
||||||
|
|
||||||
|
/// Simple error type for testing
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TestError(String);
|
||||||
|
|
||||||
|
impl std::fmt::Display for TestError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TestError {}
|
||||||
|
|
||||||
|
impl ProcessHandler for ErroringProcess {
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
_cancellation: CancellationToken,
|
||||||
|
) -> impl std::future::Future<Output = ProcessResult> + Send {
|
||||||
|
async move { Err(Box::new(TestError("intentional error".to_string())) as BoxError) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A process that panics
|
||||||
|
struct PanickingProcess;
|
||||||
|
|
||||||
|
impl ProcessHandler for PanickingProcess {
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
_cancellation: CancellationToken,
|
||||||
|
) -> impl std::future::Future<Output = ProcessResult> + Send {
|
||||||
|
async move { panic!("intentional panic") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A process that ignores cancellation (stubborn)
|
||||||
|
struct StubbornProcess {
|
||||||
|
started: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StubbornProcess {
|
||||||
|
fn new() -> (Self, Arc<AtomicUsize>) {
|
||||||
|
let started = Arc::new(AtomicUsize::new(0));
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
started: started.clone(),
|
||||||
|
},
|
||||||
|
started,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessHandler for StubbornProcess {
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
_cancellation: CancellationToken,
|
||||||
|
) -> impl std::future::Future<Output = ProcessResult> + Send {
|
||||||
|
let started = self.started.clone();
|
||||||
|
async move {
|
||||||
|
started.fetch_add(1, Ordering::SeqCst);
|
||||||
|
// Never exits, ignores cancellation
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A process that waits for a barrier before proceeding
|
||||||
|
struct BarrierProcess {
|
||||||
|
barrier: Arc<Barrier>,
|
||||||
|
completed: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BarrierProcess {
|
||||||
|
fn new(barrier: Arc<Barrier>) -> (Self, Arc<AtomicUsize>) {
|
||||||
|
let completed = Arc::new(AtomicUsize::new(0));
|
||||||
|
(
|
||||||
|
Self {
|
||||||
|
barrier,
|
||||||
|
completed: completed.clone(),
|
||||||
|
},
|
||||||
|
completed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessHandler for BarrierProcess {
|
||||||
|
fn call(
|
||||||
|
&self,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> impl std::future::Future<Output = ProcessResult> + Send {
|
||||||
|
let barrier = self.barrier.clone();
|
||||||
|
let completed = self.completed.clone();
|
||||||
|
async move {
|
||||||
|
barrier.wait().await;
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {}
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(60)) => {}
|
||||||
|
}
|
||||||
|
completed.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Basic Operation Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod basic_operations {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_process() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 0);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_stop_process() {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 0);
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_kill_process() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
proc.kill().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_restart_process() {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
proc.restart().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
// Give a moment for the new process to start
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 2);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_with_custom_handle_id() {
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
let proc = Process::new_with_handle("my-custom-id", process);
|
||||||
|
|
||||||
|
assert_eq!(proc.handle_id().to_string(), "my-custom-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_builder() {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::builder(process)
|
||||||
|
.handle_id("builder-test")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(proc.handle_id().to_string(), "builder-test");
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_builder_with_shutdown_config() {
|
||||||
|
use crate::ShutdownConfig;
|
||||||
|
|
||||||
|
let (process, started) = StubbornProcess::new();
|
||||||
|
let mut proc = Process::builder(process)
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_millis(100),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
// Stop should timeout quickly (100ms) and force kill
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
let elapsed = start_time.elapsed();
|
||||||
|
// Should be around 100ms, not the default 5s
|
||||||
|
assert!(elapsed < Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_add_process_returns_handle_id() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
|
||||||
|
let handle_id = manager.add_process(Process::new(process)).await;
|
||||||
|
|
||||||
|
// Can immediately use the returned handle_id
|
||||||
|
manager.start_process(&handle_id).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Edge Case Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod edge_cases {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_already_running_process() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
// Second start should be ignored
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_stop_not_running_process() {
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
// Stop without starting should succeed
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_kill_not_running_process() {
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
// Kill without starting should succeed
|
||||||
|
proc.kill().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_stop_already_stopped_process() {
|
||||||
|
let (process, _started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
// Second stop should succeed
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_restart_not_running_process() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
// Restart without starting should work (stop is no-op, start succeeds)
|
||||||
|
proc.restart().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_that_errors() {
|
||||||
|
let mut proc = Process::new(ErroringProcess);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_that_panics() {
|
||||||
|
let mut proc = Process::new(PanickingProcess);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_after_error() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
// We can't easily make it error then recover, so test start after error state
|
||||||
|
// by using the process manager approach instead
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Graceful Shutdown Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod graceful_shutdown {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_stubborn_process_is_killed_after_timeout() {
|
||||||
|
let (process, started) = StubbornProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
// Stop will timeout and force kill
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
// Should have waited for graceful timeout (default 5s)
|
||||||
|
// but we can't easily test that without changing the config
|
||||||
|
// Just verify it completed
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
assert!(elapsed >= Duration::from_secs(4)); // Allow some tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_kill_is_immediate_for_stubborn_process() {
|
||||||
|
let (process, started) = StubbornProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
// Kill should be immediate
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
proc.kill().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
assert!(elapsed < Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Process Manager Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod process_manager {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_add_and_start_process() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let proc = Process::new(process);
|
||||||
|
let handle_id = proc.handle_id().clone();
|
||||||
|
|
||||||
|
manager.add_process(proc).await;
|
||||||
|
manager.start_process(&handle_id).await.unwrap();
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_stop_process() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
let (process, _started, stopped) = LongRunningProcess::new();
|
||||||
|
let proc = Process::new(process);
|
||||||
|
let handle_id = proc.handle_id().clone();
|
||||||
|
|
||||||
|
manager.add_process(proc).await;
|
||||||
|
manager.start_process(&handle_id).await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
manager.stop_process(&handle_id).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_multiple_processes() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
|
||||||
|
let (process1, started1, _) = LongRunningProcess::new();
|
||||||
|
let (process2, started2, _) = LongRunningProcess::new();
|
||||||
|
let (process3, started3, _) = LongRunningProcess::new();
|
||||||
|
|
||||||
|
let proc1 = Process::new(process1);
|
||||||
|
let proc2 = Process::new(process2);
|
||||||
|
let proc3 = Process::new(process3);
|
||||||
|
|
||||||
|
let id1 = proc1.handle_id().clone();
|
||||||
|
let id2 = proc2.handle_id().clone();
|
||||||
|
let id3 = proc3.handle_id().clone();
|
||||||
|
|
||||||
|
manager.add_process(proc1).await;
|
||||||
|
manager.add_process(proc2).await;
|
||||||
|
manager.add_process(proc3).await;
|
||||||
|
|
||||||
|
manager.start_process(&id1).await.unwrap();
|
||||||
|
manager.start_process(&id2).await.unwrap();
|
||||||
|
manager.start_process(&id3).await.unwrap();
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
assert_eq!(started1.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(started2.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(started3.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_process_not_found() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
let nonexistent_id = "nonexistent".into();
|
||||||
|
|
||||||
|
let result = manager.start_process(&nonexistent_id).await.unwrap();
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_restart_process() {
|
||||||
|
let manager = ProcessManager::new();
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let proc = Process::new(process);
|
||||||
|
let handle_id = proc.handle_id().clone();
|
||||||
|
|
||||||
|
manager.add_process(proc).await;
|
||||||
|
manager.start_process(&handle_id).await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
manager.restart_process(&handle_id).await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 2);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Concurrency Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod concurrency {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sequential_start_commands() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
// Send multiple start commands sequentially
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
// Only one should have started
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_sequential_stop_commands() {
|
||||||
|
let (process, _started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
// Send multiple stop commands sequentially
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
|
||||||
|
// Only one should have stopped
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_rapid_start_stop_cycles() {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
for _ in 0..5 {
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 5);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_manager_concurrent_operations_different_processes() {
|
||||||
|
let manager = Arc::new(ProcessManager::new());
|
||||||
|
|
||||||
|
let (process1, started1, _) = LongRunningProcess::new();
|
||||||
|
let (process2, started2, _) = LongRunningProcess::new();
|
||||||
|
|
||||||
|
let proc1 = Process::new(process1);
|
||||||
|
let proc2 = Process::new(process2);
|
||||||
|
|
||||||
|
let id1 = proc1.handle_id().clone();
|
||||||
|
let id2 = proc2.handle_id().clone();
|
||||||
|
|
||||||
|
manager.add_process(proc1).await;
|
||||||
|
manager.add_process(proc2).await;
|
||||||
|
|
||||||
|
// Start both processes concurrently
|
||||||
|
let manager1 = manager.clone();
|
||||||
|
let manager2 = manager.clone();
|
||||||
|
let id1_clone = id1.clone();
|
||||||
|
let id2_clone = id2.clone();
|
||||||
|
|
||||||
|
let (r1, r2) = tokio::join!(
|
||||||
|
async move { manager1.start_process(&id1_clone).await },
|
||||||
|
async move { manager2.start_process(&id2_clone).await },
|
||||||
|
);
|
||||||
|
|
||||||
|
r1.unwrap();
|
||||||
|
r2.unwrap();
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
assert_eq!(started1.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(started2.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_barrier_synchronized_processes() {
|
||||||
|
let barrier = Arc::new(Barrier::new(3));
|
||||||
|
|
||||||
|
let (process1, completed1) = BarrierProcess::new(barrier.clone());
|
||||||
|
let (process2, completed2) = BarrierProcess::new(barrier.clone());
|
||||||
|
|
||||||
|
let mut proc1 = Process::new(process1);
|
||||||
|
let mut proc2 = Process::new(process2);
|
||||||
|
|
||||||
|
proc1.start().await.unwrap();
|
||||||
|
proc2.start().await.unwrap();
|
||||||
|
|
||||||
|
// Wait at the barrier (the third waiter)
|
||||||
|
barrier.wait().await;
|
||||||
|
|
||||||
|
// Both processes should now be past the barrier
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
proc1.stop().await.unwrap();
|
||||||
|
proc2.stop().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(completed1.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(completed2.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Parametric Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod parametric {
|
||||||
|
use super::*;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
#[test_case(1; "single start")]
|
||||||
|
#[test_case(5; "five starts")]
|
||||||
|
#[test_case(10; "ten starts")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_multiple_start_attempts(attempts: usize) {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
for _ in 0..attempts {
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
// Only one should have actually started
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(1; "single cycle")]
|
||||||
|
#[test_case(3; "three cycles")]
|
||||||
|
#[test_case(5; "five cycles")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_start_stop_cycles(cycles: usize) {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
for _ in 0..cycles {
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), cycles);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), cycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case(Duration::from_millis(10); "10ms delay")]
|
||||||
|
#[test_case(Duration::from_millis(50); "50ms delay")]
|
||||||
|
#[test_case(Duration::from_millis(100); "100ms delay")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_delayed_stop(delay: Duration) {
|
||||||
|
let (process, started, stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
tokio::time::sleep(delay).await;
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
assert_eq!(stopped.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Drop Behavior Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod drop_behavior {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_dropped_while_running() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::new(process);
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 1);
|
||||||
|
|
||||||
|
// Drop the process
|
||||||
|
drop(proc);
|
||||||
|
|
||||||
|
// The task should be aborted
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_dropped_before_start() {
|
||||||
|
let (process, started, _stopped) = LongRunningProcess::new();
|
||||||
|
let proc = Process::new(process);
|
||||||
|
|
||||||
|
// Drop without starting
|
||||||
|
drop(proc);
|
||||||
|
|
||||||
|
assert_eq!(started.load(Ordering::SeqCst), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Error Handler Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod error_handler {
|
||||||
|
use super::*;
|
||||||
|
use crate::{ProcessFailure, ProcessFailureKind, ShutdownConfig};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_on_error_callback_for_returned_error() {
|
||||||
|
let errors: Arc<Mutex<Vec<ProcessFailure>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let errors_clone = errors.clone();
|
||||||
|
|
||||||
|
let mut proc = Process::builder(ErroringProcess)
|
||||||
|
.handle_id("error-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.on_error(move |e| {
|
||||||
|
errors_clone.lock().unwrap().push(e);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
|
||||||
|
let errors = errors.lock().unwrap();
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert_eq!(errors[0].handle_id.to_string(), "error-test");
|
||||||
|
assert!(matches!(errors[0].kind, ProcessFailureKind::Returned(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_on_error_callback_for_panic() {
|
||||||
|
let errors: Arc<Mutex<Vec<ProcessFailure>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let errors_clone = errors.clone();
|
||||||
|
|
||||||
|
let mut proc = Process::builder(PanickingProcess)
|
||||||
|
.handle_id("panic-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.on_error(move |e| {
|
||||||
|
errors_clone.lock().unwrap().push(e);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
|
||||||
|
let errors = errors.lock().unwrap();
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert_eq!(errors[0].handle_id.to_string(), "panic-test");
|
||||||
|
assert!(matches!(errors[0].kind, ProcessFailureKind::Panicked(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_on_error_not_called_for_successful_process() {
|
||||||
|
let errors: Arc<Mutex<Vec<ProcessFailure>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let errors_clone = errors.clone();
|
||||||
|
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::builder(process)
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.on_error(move |e| {
|
||||||
|
errors_clone.lock().unwrap().push(e);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
let errors = errors.lock().unwrap();
|
||||||
|
assert_eq!(errors.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_process_failure_display() {
|
||||||
|
let error = ProcessFailure {
|
||||||
|
handle_id: "test-id".into(),
|
||||||
|
kind: ProcessFailureKind::Returned("something went wrong".to_string()),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"process test-id returned error: something went wrong"
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = ProcessFailure {
|
||||||
|
handle_id: "test-id".into(),
|
||||||
|
kind: ProcessFailureKind::Panicked("panic message".to_string()),
|
||||||
|
};
|
||||||
|
assert_eq!(error.to_string(), "process test-id panicked: panic message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tracing Tests
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
mod tracing_tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ShutdownConfig;
|
||||||
|
use tracing_test::traced_test;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[traced_test]
|
||||||
|
async fn test_error_is_logged_when_process_returns_error() {
|
||||||
|
let mut proc = Process::builder(ErroringProcess)
|
||||||
|
.handle_id("traced-error-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
|
||||||
|
assert!(logs_contain("process returned error"));
|
||||||
|
assert!(logs_contain("intentional error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[traced_test]
|
||||||
|
async fn test_panic_is_logged_when_process_panics() {
|
||||||
|
let mut proc = Process::builder(PanickingProcess)
|
||||||
|
.handle_id("traced-panic-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Errored).await;
|
||||||
|
|
||||||
|
assert!(logs_contain("process panicked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[traced_test]
|
||||||
|
async fn test_graceful_shutdown_timeout_is_logged() {
|
||||||
|
let (process, _started) = StubbornProcess::new();
|
||||||
|
let mut proc = Process::builder(process)
|
||||||
|
.handle_id("traced-timeout-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_millis(100),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
assert!(logs_contain("graceful shutdown timed out"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[traced_test]
|
||||||
|
async fn test_start_stop_logging() {
|
||||||
|
let (process, _started, _stopped) = LongRunningProcess::new();
|
||||||
|
let mut proc = Process::builder(process)
|
||||||
|
.handle_id("traced-lifecycle-test")
|
||||||
|
.shutdown_config(ShutdownConfig {
|
||||||
|
graceful_timeout: Duration::from_secs(1),
|
||||||
|
restart_on_success: false,
|
||||||
|
restart_delay: Duration::ZERO,
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
proc.start().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Running).await;
|
||||||
|
|
||||||
|
assert!(logs_contain("starting process"));
|
||||||
|
assert!(logs_contain("traced-lifecycle-test"));
|
||||||
|
|
||||||
|
proc.stop().await.unwrap();
|
||||||
|
proc.wait_for(ProcessState::Stopped).await;
|
||||||
|
|
||||||
|
assert!(logs_contain("stopping process"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user