registry-io 1.0.0

High-performance event/callback registry for Rust. Sync-first with optional async. Lock-free reads, zero-allocation hot path, sub-50ns notify target. Designed as the foundation primitive for portfolio crates needing fast in-process notification.
Documentation

Highlights

  • Cross-platform — Linux, macOS, Windows.
  • Lock-free reads via ArcSwap snapshots. Many threads can notify concurrently with no coordination.
  • Zero allocation on the no-panic sync notify path.
  • Panic isolation — a panicking handler does not stop siblings nor propagate to the caller. Optional on_panic callback for observability. Works for both sync handlers and async futures.
  • Priority orderingregister_with_priority(i32, ...). Higher fires first; ties broken in registration order.
  • SyncRegistry<E> — generic over the event type. Handlers receive &E.
  • AsyncRegistry<E> (feature: async) — same lock-free storage, futures-returning handlers, concurrent or sequential dispatch.
  • RAII guardsregister_guard returns a HandlerGuard / AsyncHandlerGuard that unregisters on drop.
  • Send + Sync — share registries freely across threads.

Status

Stable. Production-ready. registry-io 1.0.0 is the first stable release. The public API is frozen per the contract in docs/STABILITY-1.0.md; breaking changes require a major-version bump. Performance, panic-isolation, and zero-allocation guarantees are measured and locked in.

Headline numbers (full table in docs/PERFORMANCE.md):

  • Sync notify, 1 handler, 1 thread — 10.1 ns
  • Sync notify, 4 handlers, 16 threads contended — 24.7 ns
  • Async notify concurrent, 1 handler — 177 ns
  • Async notify_sequential, 1 handler — 53 ns
  • Sync notify hot path heap allocations — 0 (verified by dhat)

Guarantees:

  • Zero unsafe in the public API.
  • Lock-free reads via ArcSwap snapshots.
  • Zero allocation on the sync notify no-panic path.
  • Panic isolation — one panicking handler does not stop siblings or propagate to the caller.
  • Priority-ordered dispatch with stable ties.
  • RAII unregistration via HandlerGuard / AsyncHandlerGuard.
  • Send + Sync on every public type.
  • Cross-platform — Linux, macOS, Windows on stable + MSRV 1.85.0.

When to use it

Use registry-io when you have:

  • Multiple components that need notification when something happens (config reload, file change, transaction commit, metric event, etc.).
  • Fast, in-process handlers measured in microseconds or less.
  • A need to register and unregister handlers dynamically.
  • Performance-critical paths where channel allocation would dominate.

Do not use registry-io when you have:

  • Cross-process or cross-network delivery needs — use NATS, Redis pub/sub, or similar message brokers.
  • Heavy handler workloads requiring backpressure — use tokio::sync::broadcast or channels.
  • Event sourcing or durability requirements — use a real event log.

 

Quick start

[dependencies]
registry-io = "1.0"
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use registry_io::SyncRegistry;

let registry: SyncRegistry<u32> = SyncRegistry::new();
let total = Arc::new(AtomicU32::new(0));

let sink = Arc::clone(&total);
let id = registry.register(move |value| {
    sink.fetch_add(*value, Ordering::Relaxed);
});

registry.notify(&5);
registry.notify(&7);
assert_eq!(total.load(Ordering::Relaxed), 12);

assert!(registry.unregister(id));

Priority ordering

use std::sync::{Arc, Mutex};
use registry_io::SyncRegistry;

let registry: SyncRegistry<()> = SyncRegistry::new();
let order = Arc::new(Mutex::new(Vec::<&'static str>::new()));

let o = Arc::clone(&order);
let _ = registry.register_with_priority(100, move |_| o.lock().unwrap().push("audit"));
let o = Arc::clone(&order);
let _ = registry.register(move |_| o.lock().unwrap().push("business"));
let o = Arc::clone(&order);
let _ = registry.register_with_priority(-50, move |_| o.lock().unwrap().push("cleanup"));

registry.notify(&());
assert_eq!(order.lock().unwrap().as_slice(), &["audit", "business", "cleanup"]);

RAII guards

use std::sync::Arc;
use registry_io::SyncRegistry;

let registry = Arc::new(SyncRegistry::<u32>::new());
{
    let _guard = registry.register_guard(|n| println!("scoped: {n}"));
    registry.notify(&1);
} // guard drops here -> handler is unregistered
assert!(registry.is_empty());

Panic isolation

use registry_io::SyncRegistry;

let registry: SyncRegistry<()> = SyncRegistry::new();
registry.on_panic(|info| {
    eprintln!(
        "handler {} panicked: {}",
        info.handler_id(),
        info.message().unwrap_or("<opaque>")
    );
});

let _ = registry.register(|_| panic!("oops"));
let _ = registry.register(|_| println!("still ran"));
registry.notify(&()); // returns cleanly; both effects observed

Async handlers (feature: async)

[dependencies]
registry-io = { version = "1.0", features = ["async"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use registry_io::r#async::AsyncRegistry;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let registry: AsyncRegistry<u32> = AsyncRegistry::new();
    let total = Arc::new(AtomicU32::new(0));

    for _ in 0..4 {
        let sink = Arc::clone(&total);
        let _ = registry.register(move |value| {
            let sink = Arc::clone(&sink);
            let v = *value;
            async move {
                tokio::task::yield_now().await;
                sink.fetch_add(v, Ordering::Relaxed);
            }
        });
    }

    // Concurrent dispatch — all 4 futures run in parallel.
    registry.notify(&10).await;
    assert_eq!(total.load(Ordering::Relaxed), 40);

    // Sequential dispatch — awaits each handler in priority order.
    registry.notify_sequential(&10).await;
    assert_eq!(total.load(Ordering::Relaxed), 80);
}

Same lock-free read path as SyncRegistry. Panics in handler futures are caught via an internal CatchUnwind adapter and surfaced through on_panic, just like sync handlers.

See examples/ for runnable programs and docs/API.md for the full reference.


Design philosophy

  • Sync-first. The fast path is synchronous, runs on the calling thread, allocates nothing, and dispatches in nanoseconds.
  • Lock-free reads. Multiple threads can call notify() concurrently without contention.
  • Zero allocation on the hot path. Notify walks the handler list and dispatches without any heap allocation in the no-panic case.
  • Focused scope. This is a local, in-process notification primitive. Not a message bus, not a distributed event system, not a pub/sub broker.

Documentation


Standards

  • REPS (Rust Efficiency & Performance Standards) governs every decision. See REPS.md.
  • MSRV: Rust 1.85.
  • Edition: 2024.
  • Cross-platform: Linux, macOS, Windows.

License

Dual-licensed under either of:

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.