Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dcbe1db
Change process_handle return type to Arc instead of Box
mrcrgl Jul 7, 2025
a0e9012
Handle and ignore unknown control messages in workers and signal
mrcrgl Jul 7, 2025
e93eb82
Handle custom control messages and catch panics in child processes
mrcrgl Jul 7, 2025
efcf91b
Refactor RuntimeGuard to improve shutdown handling and ticker support
mrcrgl Jul 7, 2025
ec0b267
Use OnceCell for lazy initialization of completion_rx in ProcessManager
mrcrgl Jul 7, 2025
e14f01a
Refactor runtime guard and ticker for async safety
mrcrgl Jul 7, 2025
8b1cebf
Cache process control handles to optimize broadcasts and update on
mrcrgl Jul 7, 2025
6c5e0f0
Add tracing span for child process spawn in process_manager
mrcrgl Jul 7, 2025
44a0bf4
Add GitHub Actions CI workflow and initial changelog
mrcrgl Jul 7, 2025
e5c82fa
Add ProcessManagerBuilder for safer manager construction
mrcrgl Jul 7, 2025
1291863
Introduce fluent ProcessManagerBuilder with custom names
mrcrgl Jul 7, 2025
f595b2d
Implement Runnable for Arc wrapping Runnable types
mrcrgl Jul 7, 2025
df9b2ab
Instrument process spawn with tracing span to improve visibility
mrcrgl Jul 7, 2025
f837014
Deny broken intra-doc links in rustdoc and clean up ProcessManager API
mrcrgl Jul 7, 2025
e9bb5cf
Add IdleProcess no-op runnable and reorganize builtins module
mrcrgl Jul 7, 2025
0bfc7e6
Refactor ProcessManager to track child tasks and add shutdown timeouts
mrcrgl Jul 7, 2025
f4d6b97
Avoid unused variable warning by renaming elapsed to _elapsed
mrcrgl Jul 8, 2025
2ea1b1d
Revise README to expand and clarify ProcessManager usage and features
mrcrgl Jul 8, 2025
7818212
Fix spacing in branch names and remove deprecated test job
mrcrgl Jul 8, 2025
3498c43
Fix doc test annotation in idle process example code block
mrcrgl Jul 8, 2025
08a9bf5
Enable signal_receiver module only with "signal" feature flag
mrcrgl Jul 8, 2025
51ff78e
bump v0.5.0
mrcrgl Jul 8, 2025
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
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Rust CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
build-test:
name: Build & test (${{ matrix.toolchain }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
toolchain: [stable, beta, nightly]

steps:
- name: Checkout sources
uses: actions/checkout@v4

- name: Install Rust (${{ matrix.toolchain }})
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
override: true
profile: minimal
components: clippy, rustfmt

# Re-use cargo build cache for faster CI runs
- name: Cache cargo registry + build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }}

- name: Check formatting
run: cargo fmt --all -- --check

- name: Clippy (deny warnings)
run: cargo clippy --all-features -- -D warnings

- name: Run tests (default features)
run: cargo test --all

- name: Run tests (no default features)
run: cargo test --no-default-features
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Changelog

All notable changes to this project will be documented in this file.
This project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and its version numbers follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- GitHub Actions workflow for CI (`build`, `clippy`, `fmt`, full test-matrix,
feature power-set).
- `CHANGELOG.md` with Keep-a-Changelog layout.
- Optional `tracing` span around every child process (requires `tracing` feature).
- Cached vector of child `ProcessControlHandler`s for allocation-free broadcast.
- Architecture diagram (Mermaid) in `README.md`.
- `Custom(Box<dyn Any>)` variant to `RuntimeControlMessage` for future
extensibility.
- **Fluent `ProcessManagerBuilder`** allowing compile-time safe setup and
configuration.
- `.name("…")` builder method and internal plumbing for custom supervisor
names.

### Changed
- `process_handle()` now returns `Arc<dyn ProcessControlHandler>` (cheap cloning,
no double boxing).
- Default `process_name()` no longer allocates; returns `Cow<'static, str>`.
- `ProcessManager` constructors **deprecated** in favour of the new builder;
`new()`, `manual_cleanup()` and `auto_cleanup()` now issue warnings.
- Busy-wait loops in `RuntimeGuard` replaced with `Notify`-based signalling.
- Child panic handling now caught with `catch_unwind`, ensuring supervisor
never hangs.
- All examples, doctests and integration tests migrated to the builder API
(no more deprecation warnings in user-facing code).
- Internal channels refactored to remove extra locks (use of `OnceCell`).

### Fixed
- Active-child counter accuracy under edge conditions (spawn panics).
- Numerous doc examples updated for new APIs.

### Removed
- Unused aliases and imports producing compiler warnings.

---

## [0.4.1] – 2024-04-19
Removed dependency to `async_trait`.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "processmanager"
description = "manage process lifecycles, graceful shutdown and process faults"
version = "0.4.1"
version = "0.5.0"
edition = "2024"
authors = ["Marc Riegel <mail@mrcrgl.de>"]
license = "MIT"
Expand Down Expand Up @@ -34,3 +34,4 @@ tokio = { version = "1", features = [
] }
log = { version = "0.4", optional = true }
tracing = { version = "0.1", optional = true }
once_cell = "1"
207 changes: 169 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,193 @@
# ProcessManager

Manage multiple running services. A ProcessManager collects impl of `Runnable`
and takes over the runtime management like starting, stopping (graceful or in
failure) of services.
`ProcessManager` is a light-weight **Tokio based supervisor** for coordinating
multiple long-running asynchronous tasks (called *processes*).
It takes care of

If one service fails, the manager initiates a graceful shutdown for all other.
* spawning all registered processes,
* forwarding *reload* / *shutdown* commands,
* propagating errors,
* initiating a **graceful shutdown** of the whole tree once any process fails,
* and optionally cleaning up completed children to avoid memory leaks.

# Examples
The crate is completely runtime-agnostic except for its dependency on Tokio
tasks behind the scenes; you are free to use any async code in your own
processes.

---

## Table of Contents

1. [Features](#features)
2. [Installation](#installation)
3. [Quick-Start](#quick-start)
4. [Built-in Helpers](#built-in-helpers)
5. [Examples](#examples)
6. [High-Level Architecture](#high-level-architecture)
7. [Crate Features](#crate-features)
8. [License](#license)

---

## Features

| Capability | Description |
| -------------------------------- | ------------------------------------------------------------------------- |
| Graceful shutdown | Propagates a single `shutdown` request to **all** children. |
| Dynamic child management | Add new `Runnable`s even while the manager is already running. |
| Error propagation | A failing child triggers a global shutdown and returns the *first* error. |
| Auto cleanup | Optionally remove finished children to keep memory usage bounded. |
| Hierarchical composition | Managers implement `Runnable` themselves → build arbitrary process trees. |
| Built-in helpers | See [`IdleProcess`](#built-in-helpers) and [`SignalReceiver`](#built-in-helpers). |

---

## Installation

Add the dependency to your `Cargo.toml`:

```toml
[dependencies]
processmanager = "0"
```

Optional features are listed [below](#crate-features).

---

## Quick-Start

A minimal program that runs two workers for three seconds and then shuts down
gracefully:

```rust
use processmanager::*;
use std::{sync::Arc, time::Duration};
use tokio::time::{interval, sleep};

#[tokio::main]
async fn main() {
struct Worker {
id: usize,
guard: Arc<RuntimeGuard>,
}

#[derive(Default)]
struct ExampleController {
runtime_guard: RuntimeGuard,
impl Worker {
fn new(id: usize) -> Self {
Self { id, guard: Arc::new(RuntimeGuard::default()) }
}
}

impl Runnable for ExampleController {
fn process_start(&self) -> ProcFuture<'_> {
Box::pin(async {
// This can be any type of future like an async streams
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1));

loop {
match self.runtime_guard.tick(interval.tick()).await {
ProcessOperation::Next(_) => println!("work"),
ProcessOperation::Control(RuntimeControlMessage::Shutdown) => {
println!("shutdown");
break
},
ProcessOperation::Control(RuntimeControlMessage::Reload) => println!("trigger relead"),
}
}
impl Runnable for Worker {
fn process_start(&self) -> ProcFuture<'_> {
let id = self.id;
let guard = self.guard.clone();

Box::pin(async move {
let ticker = guard.runtime_ticker().await;
let mut beat = interval(Duration::from_secs(1));

Ok(())
})
}
loop {
match ticker.tick(beat.tick()).await {
ProcessOperation::Next(_) => println!("worker-{id}: heartbeat"),
ProcessOperation::Control(RuntimeControlMessage::Shutdown) => break,
_ => continue,
}
}
Ok(())
})
}

fn process_handle(&self) -> Box<dyn ProcessControlHandler> {
Box::new(self.runtime_guard.handle())
}
fn process_handle(&self) -> Arc<dyn ProcessControlHandler> {
self.guard.handle()
}
}

let mut manager = ProcessManager::new();
manager.insert(ExampleController::default());
#[tokio::main]
async fn main() {
let manager = ProcessManagerBuilder::default()
.pre_insert(Worker::new(0))
.pre_insert(Worker::new(1))
.build();

let handle = manager.process_handle();

// start all processes
let _ = tokio::spawn(async move {
manager.process_start().await.expect("service start failed");
tokio::spawn(async move {
manager.process_start().await.expect("manager error");
});

// Shutdown waits for all services to shutdown gracefully.
sleep(Duration::from_secs(3)).await;
handle.shutdown().await;
}
```

---

## Built-in Helpers

| Helper | Purpose | Feature flag |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------ |
| `IdleProcess` | Keeps an otherwise empty manager alive until an external shutdown is requested. | — |
| `SignalReceiver` | Listens for `SIGHUP`, `SIGINT`, `SIGTERM`, `SIGQUIT` and converts them into *shutdown / reload* control messages. | `signal` |

Enable `SignalReceiver` like this:

```toml
processmanager = { version = "0", features = ["signal"] }
```

---

## Examples

Ready-to-run examples live in [`examples/`](examples/) and can be launched with
Cargo:

| Command | Highlights |
| -------------------------------------------- | ------------------------------------------------------ |
| `cargo run --example simple` | Minimal setup, two workers, graceful shutdown |
| `cargo run --example dynamic_add` | Dynamically add workers while the manager is running |

Feel free to copy or adapt the code for your own services.

---

## High-Level Architecture

```mermaid
flowchart TD
subgraph Supervisor
Mgr(ProcessManager)
end
Mgr -->|spawn| Child1[Runnable #1]
Mgr --> Child2[Runnable #2]
Child1 -- control --> Mgr
Child2 -- control --> Mgr
ExtHandle(External&nbsp;Handle) -- shutdown / reload --> Mgr
```

* Every `Runnable` gets its own Tokio task.
* A `ProcessControlHandler` allows external code to **shut down** or **reload**
a single process or the whole subtree.
* The first child that ends in `Err(_)` terminates the entire supervisor.

---

## Crate Features

| Feature | Default? | Description |
| --------- | -------- | ------------------------------------------------------------------- |
| `signal` | no | Activates `builtin::SignalReceiver` for Unix signal handling. |
| `tracing` | no | Emit structured log events via the `tracing` crate. |
| `log` | no | Use the `log` crate for textual logging if `tracing` is disabled. |

Pick one of `tracing` **or** `log` to avoid duplicate output.

---

## License

Licensed under either of

* Apache License, Version 2.0
* MIT license

at your option.
11 changes: 7 additions & 4 deletions examples/dynamic_add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ impl Runnable for Worker {
println!("worker-{id}: shutting down");
break;
}
// absorb any future control messages we don't explicitly handle
ProcessOperation::Control(_) => continue,
}
}
Ok(())
})
}

fn process_handle(&self) -> Box<dyn ProcessControlHandler> {
Box::new(self.guard.handle())
fn process_handle(&self) -> Arc<dyn ProcessControlHandler> {
self.guard.handle()
}
}

Expand All @@ -67,8 +69,9 @@ async fn main() {
// ------------------------------------------------------------------
// 1. Manager with a single initial worker
// ------------------------------------------------------------------
let mut mgr = ProcessManager::new();
mgr.insert(Worker::new(0));
let mgr = ProcessManagerBuilder::default()
.pre_insert(Worker::new(0))
.build();

// We need to keep access to the manager after starting it, therefore
// wrap it in an `Arc` and clone it for the spawning task.
Expand Down
Loading
Loading