# proc-daemon v1.1.0 — Performance & Ergonomics
**Date:** 2026-05-19
**Compare:** `v1.0.1...v1.1.0`
## Headline
A non-breaking minor release focused on the two highest-impact
items from the v1.0.1 audit: completing the `parking_lot`
migration that v1.0.0-RC.1 only partially landed, and fixing a
latent shutdown-latency bug in the daemon main loop where the
configured `health_check_interval` could delay teardown by up
to its full duration. Plus one ergonomic addition that
eliminates the most common boilerplate at the entry point.
No public API removals, no signature changes on existing
items, no MSRV change. Consumers can upgrade with
`cargo update -p proc-daemon`. Source compiles unchanged.
Three changes carry the release:
1. **`parking_lot` everywhere it matters.** The
v1.0.0-RC.1 changelog advertised *"Replaced
`std::sync::Mutex` with `parking_lot::Mutex` in object
pools — 2-3x faster under contention"* — but only object
pools, `shutdown.rs`, and `metrics.rs` were actually
converted. `subsystem.rs` (the busiest module —
`SubsystemEntry::metadata` is locked on every state
transition and every stats query), `resources.rs` (the
history `RwLock`), and `daemon.rs`'s `RotatingFileWriter`
(locked on every log line) still used `std::sync` types.
v1.1.0 completes the migration.
2. **Shutdown latency is no longer bounded by the
health-check interval.** The previous main loop did
`if shutdown { break; } ... sleep(interval).await;`,
meaning a shutdown initiated 1 µs after the sleep began
waited the full interval before being noticed. v1.1.0
replaces the sleep with `tokio::select!` between the
interval and a new `ShutdownCoordinator::wait_initiated()`
broadcast wait, so shutdown is observed within
microseconds regardless of `health_check_interval` (which
defaults to 30s).
3. **`Daemon::new()` exists.** Most users just want
`Daemon::new().with_task(...).run().await` instead of the
four-line ceremony involving `Config::new()?`. The new
constructor is infallible (`Config::default()` is valid by
construction) and documented as the preferred entry point.
## What changed
### Changed — internal locks switched to `parking_lot`
`src/subsystem.rs` (22 lock sites), `src/resources.rs`
(history `RwLock` + 2 write sites + 1 read site), and
`src/daemon.rs` (`RotatingFileWriter` mutex, hit on every log
line) all migrate from `std::sync` to `parking_lot`.
`parking_lot` was already a runtime dependency for `pool.rs`,
`shutdown.rs`, and `metrics.rs` — no new transitive deps.
Side effects of the swap:
- Every `.lock().unwrap()` / `.read().unwrap()` /
`.write().unwrap()` call collapses to just `.lock()` /
`.read()` / `.write()`. `parking_lot::Mutex::lock` returns
the guard directly, not a `Result`. **~24 `.unwrap()`
calls removed from hot paths.**
- 17 `# Panics ... mutex is poisoned` rustdoc clauses
removed from `SubsystemManager` methods — `parking_lot`
doesn't poison, so the documented panic was unreachable.
- `RotatingFileWriter` no longer needs the
`map_err(|_| io::Error::other("rotating log writer mutex
poisoned"))` shim on every log write.
Expected performance: 2–3× faster under contention; cheaper
fast path even uncontended; smaller binary (no
`PoisonError` machinery).
**Migration:** none. All affected types are internal.
### Changed — daemon main loop races shutdown against
the health-check interval
`src/daemon.rs:295` previously slept the full
`health_check_interval` between iterations, only checking
the shutdown flag once per cycle:
```rust
loop {
if self.shutdown_coordinator.is_shutdown() { break; }
// ... health checks ...
tokio::time::sleep(self.config.health_check_interval()).await;
}
```
With the default 30s interval, a shutdown initiated 1 µs after
the sleep began had to wait ~30s before the loop noticed.
The fix:
```rust
let interval = self.config.health_check_interval();
tokio::select! {
() = self.shutdown_coordinator.wait_initiated() => break,
() = tokio::time::sleep(interval) => {}
}
```
The matching async-std branch uses `futures::future::select`
with two pinned futures (async-std doesn't ship a `select!`
macro). The no-runtime branch retains the blocking sleep
since there's no executor to race against.
**Migration:** none. Behavior change is observable only to
operators measuring shutdown latency — there it improves by
up to `health_check_interval`.
### Changed — `console` dep 0.15 → 0.16
Semver-compatible internal dep bump. Affects only consumers
who enable the `console` feature. No surface change.
### Added — `Daemon::new()`
```rust
impl Daemon {
#[must_use]
pub fn new() -> DaemonBuilder {
DaemonBuilder::new(Config::default())
}
}
```
The most common entry shape is now:
```rust
Daemon::new()
.with_task("worker", my_service)
.run()
.await
```
`Daemon::with_defaults() -> Result<DaemonBuilder>` is retained
unchanged for backward compatibility, but its rustdoc now
recommends `Daemon::new()` as the preferred entry point — the
default `Config` is valid by construction, so the `Result`
return is theatre.
`#[allow(clippy::new_ret_no_self)]` is applied with an
inline rationale; this is an intentional convention well
established in the Rust ecosystem (`Daemon` is the running
thing; `DaemonBuilder` is the path to construct it).
### Added — `impl Default for DaemonBuilder`
```rust
impl Default for DaemonBuilder {
fn default() -> Self {
Self::new(Config::default())
}
}
```
Equivalent to `Daemon::new()` — enables
`DaemonBuilder::default()` directly and unlocks generic
contexts bounded by `Default`.
### Added — `ShutdownCoordinator::wait_initiated()`
```rust
impl ShutdownCoordinator {
#[cfg(feature = "tokio")]
pub async fn wait_initiated(&self) { ... }
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
pub async fn wait_initiated(&self) { ... }
}
```
Resolves the moment shutdown is initiated. Distinct from
[`wait_for_shutdown`], which additionally waits for all
subsystems to mark themselves ready and enforces the graceful
timeout.
This is the primitive the main loop fix above is built on,
but it's exposed publicly because integration patterns (e.g.,
a custom supervisor coordinating multiple `proc-daemon`
instances) regularly want the same `tokio::select!` arm.
**Tokio implementation** uses the existing
`tokio::sync::broadcast::Sender` already in `ShutdownInner`
(no new sync primitives). **async-std** falls back to
exponential-backoff polling (1ms → 50ms cap), matching the
shape of `cancelled()` on the same backend.
### Internal — clippy and doc cleanup
- `clippy::significant_drop_tightening` in
`RotatingFileWriterGuard::{write, flush}` resolved with
explicit `drop(inner)` after the last guard use.
- `clippy::new_ret_no_self` on `Daemon::new` allow-listed
with rationale.
- `clippy::too_many_lines` on `Daemon::run` allow-listed —
the function is a single state machine (startup → main
loop → graceful shutdown ladder) and splitting it would
scatter related state.
## Verification
Local matrix (Windows 11, `x86_64-pc-windows-msvc`, Rust 1.95.0):
- `cargo build` (default features) ✓
- `cargo build --all-features` ✓
- `cargo build --no-default-features --features "tokio toml metrics console json-logs config-watch ipc mimalloc high-res-timing scheduler-hints lockfree-coordination mmap-config heap-profiling full windows-monitoring"` ✓
- `cargo fmt --check` ✓
- `cargo clippy --all-targets -- -D warnings` (default features) ✓
- `cargo clippy --no-default-features --features "tokio toml metrics console json-logs config-watch ipc mimalloc high-res-timing scheduler-hints lockfree-coordination mmap-config heap-profiling full windows-monitoring" --all-targets -- -D warnings` ✓
- `cargo test --all-features` — **39 unit + 5 integration + 4 doc tests pass, 0 failed**.
- `cargo audit` ✓ exit 0 (three documented allow-listed advisories, unchanged from v1.0.1).
CI matrix re-runs on `ubuntu-latest`, `macos-latest`,
`windows-latest` across the feature combinations plus
`MSRV Check`, `Security Audit`, `Documentation`,
`Code Quality`, `Miri (Memory Safety)`, and
`Performance Benchmarks`.
## Migration from `1.0.1`
No source-level migration required. `cargo update -p proc-daemon`
is sufficient. Specific notes:
- Consumers who pattern-match on `Error::*` variants
produced by mutex poisoning paths in `subsystem` /
`resources` / the rotating log writer should know that
those code paths no longer exist —
`parking_lot::Mutex::lock()` cannot fail. The relevant
variants on `Error` are unchanged; they're simply
unreachable from these internal sites now.
- Callers using `Daemon::with_defaults()?` continue to work
unchanged. New code should prefer `Daemon::new()`.
- Callers using `ShutdownCoordinator::is_shutdown()` in a
busy loop to detect shutdown can now switch to
`wait_initiated().await` for a properly asynchronous wait.
## Stability commitment
`1.1.0` is a minor release per the policy stated in v1.0.1:
- **Minor (`1.x.0`)** — pure additions to the public surface,
new opt-in features, internal performance work that
doesn't change behavior. MSRV bumps allowed (none in this
release; MSRV stays at 1.82.0).
- **Patch (`1.x.y`)** — bug fixes, security fixes, dep bumps
inside semver-compatible ranges, doc and CI improvements.
- **Major (`2.0.0`)** — anything that removes / renames /
changes the signature of a public symbol, retires a
feature flag, or adds a non-opt-in runtime dep.
The next planned patch line is `1.1.x`. v2.0.0 is tracked in
`.dev/V2-ROADMAP.md` (local-only).
## Release ceremony
```bash
# After the version bump + CHANGELOG + tag prep:
git tag -a v1.1.0 -m "Release v1.1.0 — Performance & Ergonomics"
git push origin main
git push origin v1.1.0
# Publish.
cargo publish -p proc-daemon
# Verify the index updated.
GitHub release title: `v1.1.0 — Performance & Ergonomics`.
Not tagged as pre-release.
---
**Full Changelog:** https://github.com/jamesgober/proc-daemon/compare/v1.0.1...v1.1.0