JAEB - Just Another Event Bus
In-process, snapshot-driven event bus for Tokio applications.
JAEB provides:
- sync + async listeners via a unified
subscribeAPI - automatic dispatch-mode selection based on handler trait
- explicit listener unsubscription via
Subscriptionhandles or RAIISubscriptionGuard - dependency injection via handler structs
- retry policies with configurable strategy for async handlers
- dead-letter stream for terminal failures
- explicit
Result-based error handling - graceful shutdown with in-flight task completion
- idempotent shutdown
- optional Prometheus-compatible metrics via the
metricscrate - structured tracing with per-handler spans
- summer-rs integration via summer-jaeb and
#[event_listener]macro support summer-jaeb-macros
Installation
[]
= { = "0.3.2" }
= { = "1", = ["macros", "rt-multi-thread"] }
To enable metrics instrumentation:
[]
= { = "0.3.2", = ["metrics"] }
Quick Start
use Duration;
use ;
;
;
;
async
API Overview
EventBus
EventBus::new(buffer) -> Result<EventBus, EventBusError>-- create a new bus with the given channel capacityEventBus::builder()-- builder for fine-grained configuration (buffer size, timeouts, concurrency limits)subscribe(handler) -> Result<Subscription, EventBusError>-- dispatch mode inferred from traitsubscribe_with_policy(handler, policy) -> Result<Subscription, EventBusError>-- policy type is compile-time checked (FailurePolicyfor async,NoRetryPolicyfor sync)subscribe_dead_letters(handler) -> Result<Subscription, EventBusError>publish(event) -> Result<(), EventBusError>try_publish(event) -> Result<(), EventBusError>-- non-blocking, returnsChannelFullwhen immediate dispatch capacity is unavailableunsubscribe(subscription_id) -> Result<bool, EventBusError>shutdown() -> Result<(), EventBusError>-- idempotent, drains in-flight tasksasync fn is_healthy() -> bool-- checks if the internal control loop is still running
EventBus is Clone -- all clones share the same underlying runtime state.
Handler Traits
EventHandler<E>-- async handler, dispatched on a spawned task (requiresE: Clone)SyncEventHandler<E>-- sync handler, awaited inline during dispatch
The dispatch mode is selected automatically based on which trait is implemented.
The IntoHandler<E, Mode> trait performs the conversion; the Mode parameter
(AsyncMode / SyncMode) is inferred, so callers simply write bus.subscribe(handler).
Subscription & SubscriptionGuard
Subscription holds a SubscriptionId and a bus handle. Call subscription.unsubscribe()
to remove the handler, or use bus.unsubscribe(id) directly.
SubscriptionGuard is an RAII wrapper that automatically unsubscribes the listener when
dropped. Convert a Subscription via subscription.into_guard(). Call guard.disarm()
to prevent the automatic unsubscribe.
Types
Event-- blanket trait implemented for allT: Send + Sync + 'staticHandlerResult = Result<(), Box<dyn Error + Send + Sync>>FailurePolicy { max_retries, retry_strategy, dead_letter }-- for async handlersNoRetryPolicy { dead_letter }-- for sync handlers (or async handlers that don't need retries)IntoFailurePolicy<M>-- sealed trait enforcing compile-time policy/handler compatibilityDeadLetter { event_name, subscription_id, attempts, error, event, failed_at, listener_name }SubscriptionId-- opaque handler ID (wrapsu64)
Feature Flags
| Flag | Default | Description |
|---|---|---|
metrics |
off | Enables Prometheus-compatible instrumentation via the metrics crate |
When the metrics feature is enabled, the bus records:
eventbus.publish(counter, per event type)eventbus.handler.duration(histogram, per event type)eventbus.handler.error(counter, per event type)eventbus.handler.join_error(counter, per event type)
Observability
JAEB uses the tracing crate throughout. Key spans and events:
event_bus.publish/event_bus.subscribe/event_bus.shutdown-- top-level operationseventbus.handlerspan -- per-handler execution withevent,mode, andlistener_idfieldshandler.retry(warn) -- logged on each retry attempthandler.failed(error) -- logged when retries are exhausted- async handler panics are surfaced as task failures and follow retry/dead-letter policy
Architecture
JAEB uses a split control/data architecture:
- A snapshot registry (
ArcSwap<RegistrySnapshot>) stores listeners and middleware in immutable per-type slots for low-overhead publish-path reads. - A lightweight control loop handles async failure notifications, dead-letter routing, and shutdown coordination.
Dispatch uses two lanes per event type:
- sync lane: serialized by a per-type gate (FIFO for sync dispatch)
- async lane: spawned in background and not blocked by sync backlog
Semantics
publishwaits for dispatch and sync listeners to finish.publishdoes not wait for async listeners to finish.- async handler failures can be retried based on
FailurePolicy. - sync handlers execute exactly once -- retries are not supported; passing a
FailurePolicywith retries to a sync handler is a compile error viaIntoFailurePolicy<M>. - after retries are exhausted (async) or on first failure (sync), dead-letter events are emitted when
dead_letter: true. - dead-letter handlers themselves cannot trigger further dead letters (recursion guard).
shutdownwaits for in-flight async listeners (with optional timeout).- after shutdown, all operations return
EventBusError::Stopped.
Error Variants
EventBusError::Stopped-- shutdown has started or the bus is stoppedEventBusError::ChannelFull-- publish saturation limit reached (try_publishonly)EventBusError::InvalidConfig(ConfigError)-- invalid builder/constructor configuration (zero buffer size, zero concurrency)EventBusError::MiddlewareRejected(String)-- a middleware rejected the event before it reached any listenerEventBusError::ShutdownTimeout-- the configuredshutdown_timeoutexpired and in-flight async tasks were forcibly aborted
Examples
See examples/jaeb-demo for a working demo that includes:
- async and sync handlers with retry policies
- dead-letter logging
- Prometheus metrics exporter
- structured tracing setup
Run it with:
RUST_LOG=info,jaeb=trace
Notes
- JAEB requires a running Tokio runtime.
- Events must be
Send + Sync + 'static. Async handlers additionally require events to beClone. - Events are in-process only (no persistence, replay, or broker integration).
- The crate enforces
#![forbid(unsafe_code)].
License
jaeb is distributed under the MIT License.
Copyright (c) 2025-2026 Linke Thomas
This project uses third-party libraries. See THIRD-PARTY-LICENSES for the full list of dependencies, their versions, and their respective license terms.