Status: ✅ Stable — production-ready framework for high-performance daemon services.
This is the canonical API reference. Versioned release notes live in
docs/release-notes/. Performance numbers live in
PERFORMANCE.md. Contribution norms live in
PRINCIPLES.md.
- Installation
- Core Concepts
- Constructing a Daemon
DaemonBuilder- The
SubsystemTrait RestartPolicySubsystemState&SubsystemEventShutdownHandle&ShutdownCoordinatorConfig&ConfigBuilder- Error Handling
- Optional modules
- Feature Flags
- Platform Support
- Best Practices
[dependencies]
proc-daemon = "1.1.2"With specific optional features:
[dependencies]
proc-daemon = { version = "1.1.2", features = ["tokio", "metrics", "high-res-timing"] }Convenience meta-feature (tokio, metrics, console, json-logs,
config-watch, ipc):
[dependencies]
proc-daemon = { version = "1.1.2", features = ["full"] }MSRV: Rust 1.82.0.
The Daemon is the running process. It owns:
- Signal handling (cross-platform: SIGTERM/SIGINT/SIGQUIT/SIGHUP on Unix; Ctrl-C and console events on Windows).
- Subsystem lifecycle (start, monitor, restart, stop).
- Shutdown coordination (graceful → force → kill ladder with independent timeouts).
- Optional metrics, hot-reload, IPC, and resource tracking.
Construct via Daemon::new() (infallible — uses Config::default()) or
Daemon::builder(config) for explicit configuration.
Any long-running unit of work the daemon manages. Can be:
- A function:
with_task("name", fn)— simplest case, takes aShutdownHandle, returnsResult<()>. - A trait impl:
with_subsystem(impl Subsystem)— for stateful subsystems that need health checks or custom restart policies. - A custom registration:
with_subsystem_fn("name", |mgr| mgr.register_fn(...))— for advanced lifetime patterns.
Every subsystem receives a ShutdownHandle. The handle exposes:
cancelled().await— resolves when shutdown is initiated (use intokio::select!).is_shutdown()— synchronous check.ready()— mark this subsystem as cleanly shut down (the coordinator uses this to know when graceful shutdown completed).
When a signal arrives (or daemon.shutdown() is called), the
ShutdownCoordinator broadcasts to all handles, then waits up to:
graceful_timeout_msfor subsystems to mark themselvesready.force_timeout_msfor stragglers to be force-aborted.kill_timeout_msas the final ceiling.
Infallible. Uses Config::default(). Preferred entry point for the
common case.
use proc_daemon::Daemon;
# async fn example() -> proc_daemon::Result<()> {
Daemon::new()
.with_task("worker", |mut shutdown| async move {
shutdown.cancelled().await;
Ok(())
})
.run()
.await
# }Use when you need explicit configuration:
use proc_daemon::{Config, Daemon, LogLevel};
use std::time::Duration;
# fn example() -> proc_daemon::Result<()> {
let config = Config::builder()
.name("my-service")
.log_level(LogLevel::Info)
.shutdown_timeout(Duration::from_secs(30))?
.force_shutdown_timeout(Duration::from_secs(45))?
.kill_timeout(Duration::from_secs(60))?
.worker_threads(4)
.build()?;
let _builder = Daemon::builder(config);
# Ok(())
# }Retained for backward compatibility. Prefer Daemon::new(). The
default Config is valid by construction, so the Result return
is theatre.
Equivalent to Daemon::new(). Enables Default-bound generics.
use proc_daemon::DaemonBuilder;
let builder = DaemonBuilder::default();
# let _ = builder;Fluent builder. Every method takes self and returns Self so they
chain. Methods are documented in declaration order below.
Add a function-style subsystem. The function receives a
ShutdownHandle and returns an awaitable Result<()>.
name: human-readable identifier, used in logs and events.task_fn: anyFn(ShutdownHandle) -> impl Future<Output = Result<()>> + Send + 'static.
use proc_daemon::{Daemon, ShutdownHandle};
use std::time::Duration;
async fn metrics_pump(mut shutdown: ShutdownHandle) -> proc_daemon::Result<()> {
loop {
tokio::select! {
() = shutdown.cancelled() => break,
() = tokio::time::sleep(Duration::from_secs(10)) => {
// emit metrics
}
}
}
Ok(())
}
# async fn run() -> proc_daemon::Result<()> {
Daemon::new()
.with_task("metrics_pump", metrics_pump)
.run()
.await
# }Add a trait-implementing subsystem. Use when you need health checks, custom restart policy, or per-subsystem state.
use proc_daemon::{Daemon, RestartPolicy, ShutdownHandle, Subsystem};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
struct DatabaseSync {
target: String,
}
impl Subsystem for DatabaseSync {
fn run(
&self,
mut shutdown: ShutdownHandle,
) -> Pin<Box<dyn Future<Output = proc_daemon::Result<()>> + Send>> {
let target = self.target.clone();
Box::pin(async move {
loop {
tokio::select! {
() = shutdown.cancelled() => break,
() = tokio::time::sleep(Duration::from_secs(5)) => {
// sync against `target`
let _ = ⌖
}
}
}
Ok(())
})
}
fn name(&self) -> &str { "database_sync" }
fn restart_policy(&self) -> RestartPolicy {
RestartPolicy::ExponentialBackoff {
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
max_attempts: 5,
}
}
}
# async fn run() -> proc_daemon::Result<()> {
Daemon::new()
.with_subsystem(DatabaseSync { target: "primary".into() })
.run()
.await
# }Low-level escape hatch. Hands you the SubsystemManager so you can
register through any path (register, register_fn,
register_closure).
use proc_daemon::Daemon;
# async fn run() -> proc_daemon::Result<()> {
Daemon::new()
.with_subsystem_fn("worker", |mgr| {
mgr.register_fn("worker", |mut shutdown| async move {
shutdown.cancelled().await;
Ok(())
})
})
.run()
.await
# }Customize which signals are handled. See signal.
Shortcut for the two most common signals.
Disable signal handling entirely. The daemon will only shut down via
programmatic daemon.shutdown() or subsystem completion.
use proc_daemon::Daemon;
# async fn run() -> proc_daemon::Result<()> {
Daemon::new()
.without_signals()
.with_task("worker", |mut shutdown| async move {
shutdown.cancelled().await;
Ok(())
})
.run()
.await
# }Override the path the config-watch feature watches. Defaults to
Config::work_dir.join(DEFAULT_CONFIG_FILE) or ./daemon.toml.
Construct the Daemon without starting it. Validates the config and
sets up the shutdown coordinator, subsystem manager, and signal
handler.
build() + Daemon::run() in one call. The common shape.
pub trait Subsystem: Send + Sync + 'static {
fn run(
&self,
shutdown: ShutdownHandle,
) -> Pin<Box<dyn Future<Output = proc_daemon::Result<()>> + Send>>;
fn name(&self) -> &str;
fn health_check(&self) -> Option<Box<dyn Fn() -> bool + Send + Sync>> { None }
fn restart_policy(&self) -> RestartPolicy { RestartPolicy::Never }
}run: produces the long-running future. Must respectshutdown.cancelled().name: stable identifier; appears in logs, events, and metadata.health_check: optional; the daemon main loop polls these everymonitoring.health_check_interval_mswhenmonitoring.health_checks = true.restart_policy: applies if the future returnsErr(_)or panics.
The Pin<Box<dyn Future>> return shape is required for v1.x trait
objects. v2.0.0 will replace it with async fn in trait (tracked in
the v2 roadmap).
async fn worker(mut shutdown: proc_daemon::ShutdownHandle) -> proc_daemon::Result<()> {
shutdown.cancelled().await;
Ok(())
}
# fn _r() -> impl Fn(proc_daemon::ShutdownHandle) -> std::pin::Pin<Box<dyn std::future::Future<Output = proc_daemon::Result<()>> + Send>> {
# |sh| Box::pin(worker(sh))
# }let _closure = |mut shutdown: proc_daemon::ShutdownHandle| async move {
shutdown.cancelled().await;
proc_daemon::Result::Ok(())
};See DatabaseSync example above.
pub enum RestartPolicy {
Never, // default
Always,
OnFailure,
ExponentialBackoff {
initial_delay: std::time::Duration,
max_delay: std::time::Duration,
max_attempts: u32,
},
}| Variant | Behavior |
|---|---|
Never |
Subsystem stays stopped after returning. |
Always |
Restart immediately whether returned Ok or Err. |
OnFailure |
Restart only if returned Err(_) or panicked. |
ExponentialBackoff { … } |
Restart on failure with capped exponential delays, up to max_attempts. |
use proc_daemon::RestartPolicy;
use std::time::Duration;
// Critical: never give up.
let _ = RestartPolicy::Always;
// Best-effort: tolerate transient errors.
let _ = RestartPolicy::OnFailure;
// Production default for restartable services:
let _ = RestartPolicy::ExponentialBackoff {
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
max_attempts: 5,
};#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubsystemState {
Starting,
Running,
Stopping,
Stopped,
Failed,
Restarting,
}Read via SubsystemMetadata.state or queried through
SubsystemManager::get_subsystem_metadata(id).
pub struct SubsystemMetadata {
pub id: SubsystemId,
pub name: String,
pub state: SubsystemState,
pub registered_at: std::time::Instant,
pub started_at: Option<std::time::Instant>,
pub stopped_at: Option<std::time::Instant>,
pub restart_count: u32,
pub last_error: Option<String>,
pub restart_policy: RestartPolicy,
}Push-based notifications when lockfree-coordination is enabled.
pub enum SubsystemEvent {
StateChanged {
id: SubsystemId,
name: String,
state: SubsystemState,
at: std::time::Instant,
},
}Activate via manager.enable_events(), then either poll
try_next_event() or take a cloned Receiver via subscribe_events().
impl ShutdownHandle {
pub fn is_shutdown(&self) -> bool;
pub async fn cancelled(&mut self);
pub fn shutdown_reason(&self) -> Option<ShutdownReason>;
pub fn shutdown_time(&self) -> Option<std::time::Instant>;
pub fn is_forced(&self) -> bool;
pub fn ready(&self);
pub fn time_remaining(&self) -> Option<std::time::Duration>;
}use proc_daemon::ShutdownHandle;
use std::time::Duration;
async fn worker(mut shutdown: ShutdownHandle) -> proc_daemon::Result<()> {
loop {
tokio::select! {
() = shutdown.cancelled() => {
tracing::info!("shutdown observed; cleaning up");
break;
}
() = tokio::time::sleep(Duration::from_millis(100)) => {
// do work
}
}
}
shutdown.ready(); // tell the coordinator we're clean
Ok(())
}use proc_daemon::ShutdownHandle;
async fn worker(mut shutdown: ShutdownHandle) -> proc_daemon::Result<()> {
shutdown.cancelled().await;
if shutdown.is_forced() {
// Skip optional cleanup that might take too long.
} else if let Some(remaining) = shutdown.time_remaining() {
// Use the budget the coordinator is giving us.
tracing::info!(?remaining, "graceful budget remaining");
}
shutdown.ready();
Ok(())
}pub enum ShutdownReason {
Signal(i32), // OS signal (SIGTERM = 15, SIGINT = 2, etc.)
Requested, // programmatic daemon.shutdown()
Error, // a subsystem returned Err
ResourceExhausted, // emitted by future versions; reserved
Forced, // promotion: graceful timeout exceeded
}impl ShutdownCoordinator {
pub fn new(graceful_ms: u64, force_ms: u64, kill_ms: u64) -> Self;
pub fn create_handle<S: Into<String>>(&self, subsystem_name: S) -> ShutdownHandle;
pub fn initiate_shutdown(&self, reason: ShutdownReason) -> bool;
pub fn is_shutdown(&self) -> bool;
pub fn get_reason(&self) -> Option<ShutdownReason>;
#[cfg(feature = "tokio")]
pub async fn wait_initiated(&self); // v1.1.0+: lock-free wait
pub async fn wait_for_shutdown(&self) -> Result<()>;
pub async fn wait_for_force_shutdown(&self) -> Result<()>;
pub async fn wait_for_kill_shutdown(&self) -> Result<()>;
pub fn get_stats(&self) -> ShutdownStats;
pub fn update_timeouts(&self, graceful_ms: u64, force_ms: u64, kill_ms: u64);
}Resolves immediately when shutdown is initiated, without waiting
for subsystems to mark themselves ready. Use inside tokio::select!
to break out of polling loops. Used internally by the daemon main
loop to eliminate health-check-interval-bounded shutdown latency.
use proc_daemon::shutdown::ShutdownCoordinator;
use std::time::Duration;
# async fn example(coord: &ShutdownCoordinator) {
loop {
tokio::select! {
() = coord.wait_initiated() => break,
() = tokio::time::sleep(Duration::from_secs(5)) => {
// do periodic work
}
}
}
# }use proc_daemon::{Config, LogLevel};
use std::time::Duration;
# fn example() -> proc_daemon::Result<Config> {
let config = Config::builder()
.name("my-service")
.log_level(LogLevel::Info)
.json_logging(false)
// Timeouts are validated against each other (graceful < force < kill).
// The setters return `Result<Self>`; chain with `?`.
.shutdown_timeout(Duration::from_secs(30))?
.force_shutdown_timeout(Duration::from_secs(45))?
.kill_timeout(Duration::from_secs(60))?
.worker_threads(0) // 0 = auto-detect via std::thread::available_parallelism()
.enable_metrics(true)
.hot_reload(true)
.work_dir("/var/lib/my-service")
.pid_file("/var/run/my-service.pid")
.build()
# }# daemon.toml
name = "my-production-daemon"
[logging]
level = "info"
json = false
color = true
file = "/var/log/my-daemon.log"
max_file_size = 104857600 # 100 MiB
max_files = 5
[shutdown]
graceful = 30000 # 30 s
force = 45000 # 45 s
kill = 60000 # 60 s
[performance]
worker_threads = 0 # auto-detect
thread_pinning = false
memory_pool_size = 1048576
numa_aware = false
lock_free = true
[monitoring]
enable_metrics = true
metrics_interval_ms = 1000
track_resources = true
health_checks = true
health_check_interval_ms = 5000use proc_daemon::Config;
# fn example() -> proc_daemon::Result<Config> {
Config::load_from_file("daemon.toml")
# }Environment variables override TOML, using the DAEMON_ prefix and
double-underscore for nesting:
export DAEMON_NAME="my-daemon"
export DAEMON_LOGGING__LEVEL="debug"
export DAEMON_SHUTDOWN__GRACEFUL="60000"
export DAEMON_PERFORMANCE__WORKER_THREADS="16"use proc_daemon::Config;
# fn example() -> proc_daemon::Result<Config> {
Config::load() // merges defaults + DEFAULT_CONFIG_FILE + env
# }pub struct LogConfig {
pub level: LogLevel,
pub json: bool,
pub color: bool,
pub file: Option<std::path::PathBuf>,
pub max_file_size: Option<u64>,
pub max_files: Option<u32>,
}
pub struct ShutdownConfig {
pub graceful: u64,
pub force: u64,
pub kill: u64,
}
pub struct PerformanceConfig {
pub worker_threads: usize,
pub thread_pinning: bool,
pub memory_pool_size: usize,
pub numa_aware: bool,
pub lock_free: bool,
}
pub struct MonitoringConfig {
pub enable_metrics: bool,
pub metrics_interval_ms: u64,
pub track_resources: bool,
pub health_checks: bool,
pub health_check_interval_ms: u64,
}
pub enum LogLevel { Trace, Debug, Info, Warn, Error }When config-watch is enabled and Config.hot_reload = true, the
daemon installs a notify watcher and updates an ArcSwap<Config>
on every successful reload:
let snapshot = daemon.config_snapshot(); // Arc<Config>
let level = snapshot.logging.level;config_snapshot() is feature-gated to config-watch.
pub type Result<T> = std::result::Result<T, Error>;Error is a thiserror::Error enum with these variants:
Config— invalid or unparseable configuration.Signal— signal-handling setup or invocation failure.Shutdown— coordinator failure or timeout exceeded.Subsystem— registration, start, or stop failure.Io— file/path errors.ResourceExhausted— soft-limit or hard-limit hit.Timeout— explicit timeout exceeded.InvalidState— illegal state transition or value.Platform— feature unavailable on this OS.
Every variant carries an ErrorCode (stable numeric identifier in the
1000–10999 range) for metrics and structured logging:
use proc_daemon::Error;
# fn example(err: Error) {
match err {
Error::Config { code, message, .. } => {
eprintln!("config error {code}: {message}");
}
Error::Subsystem { code, name, message, .. } => {
eprintln!("subsystem '{name}' failed [{code}]: {message}");
}
_ => {}
}
# }Each variant has a short constructor:
use proc_daemon::Error;
let _ = Error::config("invalid config");
let _ = Error::io_with_source("read failed", std::io::Error::other("eof"));
let _ = Error::subsystem("worker", "panicked");
let _ = Error::timeout("flush", 5_000);Categorized integer codes:
| Range | Category |
|---|---|
| 1000–1999 | Config |
| 2000–2999 | Signal |
| 3000–3999 | Shutdown |
| 4000–4999 | Subsystem |
| 5000–5999 | I/O |
| 6000–6999 | Runtime |
| 7000–7999 | Resource exhaustion |
| 8000–8999 | Timeout |
| 9000–9999 | State |
| 10000–10999 | Platform |
| 99999 | Unknown |
Enable features = ["serde"] to serialize Error and ErrorCode
into structured logs or external transport. source: dyn Error
fields are skipped during serialization.
Enable features = ["backtrace"] to attach backtraces to errors
created via BacktraceError::new(...) and
BacktraceError::with_source(...). Backtrace capture follows
RUST_BACKTRACE.
Built-in counters / gauges / histograms with snapshot export.
[dependencies]
proc-daemon = { version = "1.1.2", features = ["metrics"] }use proc_daemon::metrics::MetricsCollector;
use std::time::Duration;
let collector = MetricsCollector::new();
// Counters
collector.increment_counter("requests_total", 1);
collector.increment_counter("requests_total", 5);
// Gauges
collector.set_gauge("active_connections", 42);
// Histograms (raw + Duration helper)
collector.record_histogram("request_duration", Duration::from_millis(150));
// Snapshot at any time
let snapshot = collector.get_metrics();
println!("uptime: {:?}", snapshot.uptime);Auto-scoped timing with Timer:
use proc_daemon::metrics::{MetricsCollector, Timer};
use std::sync::Arc;
let collector = Arc::new(MetricsCollector::new());
{
let _timer = Timer::start("operation", Arc::clone(&collector));
// ... do work ...
} // duration recorded on dropCross-platform per-process memory / CPU / thread sampling. Always available (no feature gate).
use proc_daemon::resources::ResourceTracker;
use std::time::Duration;
let tracker = ResourceTracker::new(Duration::from_secs(1));
let usage = tracker.current_usage();
println!("memory: {:.2} MB, cpu: {:.1}%", usage.memory_mb(), usage.cpu_percent());
# let _ = tracker;With alerting:
use proc_daemon::resources::ResourceTracker;
use std::time::Duration;
let tracker = ResourceTracker::new(Duration::from_secs(5));
let _ = tracker;
// Configure soft-limit + alert handler via the tracker's builder
// methods (see source for the full API). Alerts fire when the soft
// memory limit is exceeded; the handler is `Arc<dyn Fn(Alert)>`.The default SignalHandler listens for SIGTERM, SIGINT, SIGQUIT, and
SIGHUP on Unix; Ctrl-C and console events on Windows.
For customization, build a SignalConfig:
use proc_daemon::signal::SignalConfig;
let signal_config = SignalConfig::new()
.with_sighup() // enable SIGHUP
.with_sigusr1() // enable SIGUSR1
.without_sigint() // disable SIGINT
.with_custom_handler(12, "Custom signal");
# let _ = signal_config;Pass to the builder:
use proc_daemon::{signal::SignalConfig, Daemon};
# async fn example(signal_config: SignalConfig) -> proc_daemon::Result<()> {
Daemon::new()
.with_signal_config(signal_config)
.run()
.await
# }Pretty-print a signal number:
let _ = proc_daemon::signal::signal_description(15); // "SIGTERM"Prevent multiple daemons from running with the same PID file:
use proc_daemon::lock::InstanceLock;
use std::path::Path;
let _lock = InstanceLock::acquire(Path::new("/var/run/my-daemon.pid"))
.expect("another instance is running");Drop releases the lock and removes the file.
Reusable buffers to reduce allocation pressure in hot paths:
use proc_daemon::{StringPool, VecPool};
let strings = StringPool::new(/*initial=*/ 16, /*max=*/ 128, /*cap=*/ 64);
{
let mut s = strings.acquire();
s.push_str("hello");
} // returned to pool on drop
let buffers: VecPool<u8> = VecPool::new(8, 32, 1024);
{
let mut buf = buffers.acquire();
buf.extend_from_slice(b"payload");
}Unix-domain sockets on Unix, named pipes on Windows.
// Unix:
use proc_daemon::ipc::unix;
let listener = unix::bind("/tmp/my-daemon.sock").await?;
loop {
let (_stream, _addr) = listener.accept().await?;
}// Windows:
use proc_daemon::ipc::windows;
let server = windows::create_server(r"\\.\pipe\my-daemon")?;
windows::server_connect(&server).await?;
windows::echo_once(server).await?;The Unix bind helper rejects existing non-socket files and
symlinks to avoid TOCTOU footguns; see src/ipc.rs for the full
security note.
profiling feature (Unix-only): CPU profile via pprof.
use proc_daemon::profiling::CpuProfiler;
let prof = CpuProfiler::start()?;
// ... workload ...
prof.stop_to_file("cpu.pb")?;heap-profiling feature (cross-platform): heap profile via dhat.
use proc_daemon::profiling::heap::HeapProfiler;
let prof = HeapProfiler::start(Some("heap.json"))?;
// ... allocation-heavy workload ...
prof.stop();On Windows the CPU profiler is unavailable (the feature is target-gated to Unix in v1.0.1+). Heap profiling works on all platforms.
Lock-free channel facade. Backed by crossbeam-channel when
lockfree-coordination is enabled, falling back to std::sync::mpsc
otherwise — same API in both cases.
use proc_daemon::coord::chan;
let (tx, rx) = chan::unbounded::<u32>();
tx.send(7).unwrap();
match chan::try_recv(&rx) {
Ok(v) => println!("got {v}"),
Err(_) => println!("empty"),
}Subsystem events stream through the same channel when
SubsystemManager::enable_events() is called.
Enable high-res-timing to access an ultra-fast monotonic clock
backed by quanta (RDTSC where available).
let t0 = proc_daemon::timing::now();
// ... measured work ...
let t1 = proc_daemon::timing::now();
let dt = t1.duration_since(t0);
println!("elapsed: {dt:?}");Enable scheduler-hints to expose two best-effort tuning hooks:
proc_daemon::scheduler::apply_process_hints(config);
proc_daemon::scheduler::apply_runtime_hints();With scheduler-hints-unix on Linux, apply_process_hints attempts
a renice -5 (no-op if it lacks privileges) and apply_runtime_hints
sets a permissive CPU affinity mask. On Windows / macOS both calls
are no-ops.
| Feature | Description | Default | Platform |
|---|---|---|---|
tokio |
Tokio runtime (recommended). | ✅ | all |
toml |
TOML config loader (used by Config::load_from_file). |
✅ | all |
async-std |
async-std runtime (legacy; removal in v2.0.0). | ❌ | all |
metrics |
Counters / gauges / histograms via MetricsCollector. |
❌ | all |
console |
Colored terminal output for selected helpers. | ❌ | all |
json-logs |
JSON-formatted tracing output. |
❌ | all |
config-watch |
Live-reload Config on file change (notify). |
❌ | all |
ipc |
Unix-socket / named-pipe IPC scaffold. | ❌ | all |
mmap-config |
Memory-mapped fast-path for TOML loading. | ❌ | all |
mimalloc |
Switch global allocator to mimalloc. | ❌ | all |
high-res-timing |
quanta-backed proc_daemon::timing. |
❌ | all |
scheduler-hints |
Best-effort scheduler hooks. | ❌ | all |
scheduler-hints-unix |
Activates nix/resource for renice/affinity. |
❌ | Unix |
lockfree-coordination |
crossbeam-channel backend + subsystem events. |
❌ | all |
profiling |
CPU profiler (pprof, Unix-only). |
❌ | Unix |
heap-profiling |
Heap profiler (dhat). |
❌ | all |
backtrace |
Backtrace capture on error creation. | ❌ | all |
serde |
Serialize Error/ErrorCode (used by JSON logs and IPC). |
❌ | all |
windows-monitoring |
Win32 ToolHelp-based process monitoring. | ❌ | Windows |
full |
tokio+metrics+console+json-logs+config-watch+ipc. |
❌ | all |
| Platform | Status |
|---|---|
| Linux x86_64 | first-class |
| Linux aarch64 | first-class |
| macOS x86_64 / aarch64 | first-class |
| Windows x86_64 (MSVC) | first-class (with windows-monitoring for Win32 stats) |
| FreeBSD / OpenBSD | should work via the Unix path; not regularly tested |
Notes:
profilingis Unix-only (target-gated). On Windows the feature is inert — no compile error, butCpuProfileris not exported. Useheap-profiling(dhat) cross-platform or a Windows-native profiler.scheduler-hints-unixis a no-op on non-Unix.async-stdis marked legacy and slated for removal in v2.0.0.
-
Always
select!againstcancelled()in long-running futures. The daemon main loop already does this since v1.1.0, but every subsystem with its own sleep/poll loop must too — otherwise shutdown latency is bounded by your interval. -
Mark
ready()after cleanup. The coordinator can't tell the difference between "still working" and "done cleaning up" without it; you'll be force-aborted instead of getting credit for being tidy. -
Use
RestartPolicy::ExponentialBackofffor network-facing subsystems.Alwayswill hot-loop on persistent failures;Neverwill silently die. -
Set
force_shutdown_timeout > shutdown_timeout > 0andkill_timeout > force_shutdown_timeout. The builder validates this; mis-ordered timeouts fail atbuild(). -
Prefer
Daemon::new()overDaemon::builder(Config::default())orDaemon::with_defaults()?. They're equivalent;new()is shorter and infallible (v1.1.0+). -
Enable
mimallocfor allocation-heavy workloads. It's a one-line opt-in (features = ["mimalloc"]) and typically wins 5–15% on throughput in services that allocate a lot per request. -
Treat
Daemon::run()as the last call inmain(). It owns the runtime for the process lifetime. If you need to share state with subsystems, capture it before callingrun. -
Subscribe to
SubsystemEventrather than pollingget_stats()when you need reactivity. Polling is fine for periodic introspection (every few seconds); events are required for sub-second latency.
Compatibility: This document is current as of v1.1.2.
Earlier APIs that have been replaced or augmented are still listed
(e.g., Daemon::with_defaults), with the preferred form noted.
v2.0.0 will introduce breaking changes; see the local
.dev/V2-ROADMAP.md for the plan.