Skip to content

Latest commit

 

History

History
1167 lines (897 loc) · 31.1 KB

File metadata and controls

1167 lines (897 loc) · 31.1 KB
High-Performance Process EcoSystem for Rust

Process Daemon
API REFERENCE — v1.1.2

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.

Table of Contents

  1. Installation
  2. Core Concepts
  3. Constructing a Daemon
  4. DaemonBuilder
  5. The Subsystem Trait
  6. RestartPolicy
  7. SubsystemState & SubsystemEvent
  8. ShutdownHandle & ShutdownCoordinator
  9. Config & ConfigBuilder
  10. Error Handling
  11. Optional modules
  12. Feature Flags
  13. Platform Support
  14. Best Practices

Installation

[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.


Core Concepts

Daemon

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.

Subsystem

Any long-running unit of work the daemon manages. Can be:

  • A function: with_task("name", fn) — simplest case, takes a ShutdownHandle, returns Result<()>.
  • 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.

ShutdownHandle

Every subsystem receives a ShutdownHandle. The handle exposes:

  • cancelled().await — resolves when shutdown is initiated (use in tokio::select!).
  • is_shutdown() — synchronous check.
  • ready() — mark this subsystem as cleanly shut down (the coordinator uses this to know when graceful shutdown completed).

Coordinated shutdown

When a signal arrives (or daemon.shutdown() is called), the ShutdownCoordinator broadcasts to all handles, then waits up to:

  1. graceful_timeout_ms for subsystems to mark themselves ready.
  2. force_timeout_ms for stragglers to be force-aborted.
  3. kill_timeout_ms as the final ceiling.

Constructing a Daemon

Daemon::new() -> DaemonBuilder (v1.1.0+)

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
# }

Daemon::builder(config: Config) -> DaemonBuilder

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(())
# }

Daemon::with_defaults() -> Result<DaemonBuilder>

Retained for backward compatibility. Prefer Daemon::new(). The default Config is valid by construction, so the Result return is theatre.

DaemonBuilder::default() (v1.1.0+)

Equivalent to Daemon::new(). Enables Default-bound generics.

use proc_daemon::DaemonBuilder;

let builder = DaemonBuilder::default();
# let _ = builder;

DaemonBuilder

Fluent builder. Every method takes self and returns Self so they chain. Methods are documented in declaration order below.

Subsystem registration

with_task<F, Fut>(name: &str, task_fn: F) -> Self

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: any Fn(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
# }

with_subsystem<S>(subsystem: S) -> Self where S: Subsystem

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 _ = &target;
                    }
                }
            }
            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
# }

with_subsystem_fn<F>(name: &str, register_fn: F) -> Self

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
# }

Signal configuration

with_signal_config(config: SignalConfig) -> Self

Customize which signals are handled. See signal.

with_signals(sigterm: bool, sigint: bool) -> Self

Shortcut for the two most common signals.

without_signals() -> Self

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
# }

Config-watch

with_config_path<P: Into<PathBuf>>(path: P) -> Self

Override the path the config-watch feature watches. Defaults to Config::work_dir.join(DEFAULT_CONFIG_FILE) or ./daemon.toml.

Terminal methods

build(self) -> Result<Daemon>

Construct the Daemon without starting it. Validates the config and sets up the shutdown coordinator, subsystem manager, and signal handler.

async run(self) -> Result<()>

build() + Daemon::run() in one call. The common shape.


The Subsystem Trait

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 respect shutdown.cancelled().
  • name: stable identifier; appears in logs, events, and metadata.
  • health_check: optional; the daemon main loop polls these every monitoring.health_check_interval_ms when monitoring.health_checks = true.
  • restart_policy: applies if the future returns Err(_) 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).

Multiple ways to define a subsystem

Function

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))
# }

Closure

let _closure = |mut shutdown: proc_daemon::ShutdownHandle| async move {
    shutdown.cancelled().await;
    proc_daemon::Result::Ok(())
};

Struct + impl

See DatabaseSync example above.


RestartPolicy

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,
};

SubsystemState & SubsystemEvent

SubsystemState

#[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).

SubsystemMetadata

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,
}

SubsystemEvent

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().


ShutdownHandle & ShutdownCoordinator

ShutdownHandle (held by subsystems)

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>;
}

Typical use inside a subsystem

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(())
}

Forced-shutdown awareness

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(())
}

ShutdownReason

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
}

ShutdownCoordinator (held by the daemon)

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);
}

wait_initiated() — new in v1.1.0

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
        }
    }
}
# }

Config & ConfigBuilder

Programmatic construction

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()
# }

From TOML file

# 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   = 5000
use proc_daemon::Config;

# fn example() -> proc_daemon::Result<Config> {
Config::load_from_file("daemon.toml")
# }

From environment

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
# }

Configuration sub-types

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 }

Live snapshots (config-watch)

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.


Error Handling

Result<T> and Error

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}");
    }
    _ => {}
}
# }

Constructors

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);

ErrorCode

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

serde feature

Enable features = ["serde"] to serialize Error and ErrorCode into structured logs or external transport. source: dyn Error fields are skipped during serialization.

backtrace feature

Enable features = ["backtrace"] to attach backtraces to errors created via BacktraceError::new(...) and BacktraceError::with_source(...). Backtrace capture follows RUST_BACKTRACE.


Optional modules

metrics feature

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 drop

resources

Cross-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)>`.

Signal handling

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"

Instance locking

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.

Object pools

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");
}

ipc feature

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

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.

Coordination primitives

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.

High-resolution timing

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:?}");

Scheduler hints

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 Flags

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 Support

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:

  • profiling is Unix-only (target-gated). On Windows the feature is inert — no compile error, but CpuProfiler is not exported. Use heap-profiling (dhat) cross-platform or a Windows-native profiler.
  • scheduler-hints-unix is a no-op on non-Unix.
  • async-std is marked legacy and slated for removal in v2.0.0.

Best Practices

  1. Always select! against cancelled() 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.

  2. 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.

  3. Use RestartPolicy::ExponentialBackoff for network-facing subsystems. Always will hot-loop on persistent failures; Never will silently die.

  4. Set force_shutdown_timeout > shutdown_timeout > 0 and kill_timeout > force_shutdown_timeout. The builder validates this; mis-ordered timeouts fail at build().

  5. Prefer Daemon::new() over Daemon::builder(Config::default()) or Daemon::with_defaults()?. They're equivalent; new() is shorter and infallible (v1.1.0+).

  6. Enable mimalloc for 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.

  7. Treat Daemon::run() as the last call in main(). It owns the runtime for the process lifetime. If you need to share state with subsystems, capture it before calling run.

  8. Subscribe to SubsystemEvent rather than polling get_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.