Router¶
The Router is the central dispatcher in EDRI.
It receives Event instances from Managers, Workers, and external connectors.
It delivers events to subscribed Managers.
It bridges distributed routers through a Connector, which links to the Switch.
The Router is transport-agnostic and producer-agnostic. It does not care where an event originated.
What the Router does¶
Dispatch events to subscribed Managers.
Maintain subscriptions (which managers handle which event types).
Bridge routers via the Switch (cross-process / cross-node distribution).
Cache events to replay to (re)subscribing Managers — surviving a restarted Manager or a temporary Switch outage.
Subscriptions¶
Managers subscribe to events using @request / @response decorators.
At startup, those subscriptions are registered so the Router knows which Manager handles which event type.
Delivery rule: fan-out¶
The Router delivers an event to all Managers subscribed to that event type in the matching direction. This is a fan-out pattern, not load balancing.
The Router keeps two separate subscription maps — one for requests, one for responses — and picks the map based on whether the event carries a response. A Manager can subscribe to the same event type as a request (@request), as a response (@response), or both. The Router performs no other filtering beyond type and direction.
Consequence: If you deploy two identical replicas of a Manager, both will handle every matching event independently. The Router does not partition events among subscribers — every subscriber receives every matching event. There is no built-in competing-consumer or work-queue semantics.
If you need coordination rules (for example "only one manager should act"), enforce them at the application design level. For use cases that require exactly-once execution (scheduled jobs, payments, notifications), see Singleton operations.
Non-blocking fan-out¶
The Router's main loop never writes to a subscriber pipe directly. Each connection has its own worker thread and an in-memory outbound queue; the loop enqueues and moves on. A Manager that is slow to drain its pipe therefore delays only its own delivery, never routing for other components.
Every write to a pipe goes through its worker — fan-out, subscribe/unsubscribe acks, SubscribedNew, and cache replay alike — so each pipe has a single writer and per-connection order is preserved (one FIFO worker per connection). Every consumer still observes events in the Router's serialization order.
Backpressure is applied per connection:
Backlog at or above
EDRI_ROUTER_OUTBOUND_QUEUE_SOFT_LIMIT(default 800) flags the componentWARNINGand logs; events keep queueing.Backlog reaching
EDRI_ROUTER_OUTBOUND_QUEUE_HARD_LIMIT(default 5000) disconnects and restarts the component. It re-subscribes and replays from the Router cache, so handlers must be idempotent.
Event flow¶
A typical flow looks like this:
A component produces an event and puts it into the Router queue.
The Router dispatches the event to all subscribed Managers.
A Manager may:
set
event.responseand finish (response is sent automatically at the end of the handler)emit additional events via
self.router_queue.put(...)start Workers
Workers may send events back to their Manager via
event_send(...).The Manager decides what to emit back into the Router.
The Router distinguishes a "request" event (no response set) from a "response" event (response set) and routes each via its own subscription map — see Delivery rule: fan-out.
Router cache¶
The Router maintains an internal cache of recent events.
The goal is to withstand a temporary connection drop to the Switch:
events are stored locally for a limited time
when the Switch connection recovers, routing can continue without losing all queued work
Router cache configuration¶
EDRI_CACHE_TIMEOUThow long events are kept in the Router cache
EDRI_CACHE_INFO_MESSAGEinterval for logging cache size / cache statistics
Delivery guarantees¶
Event delivery semantics: best effort with bounded in-memory replay.
The Router gives subscribers a best-effort replay window backed by the in-memory cache. Nothing is persisted to disk. This is not a durable queue system.
What is guaranteed¶
Replay on (re)subscribe. Every routed event is cached for
EDRI_CACHE_TIMEOUTseconds (default 30). When a Manager subscribes with afrom_time, the Router immediately replays all cached events of that type (matched by request/response direction) that arrived at or afterfrom_time. With nofrom_time, the Router replays the entire cached window for that event type (everything still insideEDRI_CACHE_TIMEOUT).Crash recovery with replay. A dead Manager is detected the next time the Router sends it an event and the send fails with a broken pipe — in an otherwise idle system this is reliably the periodic
HealthCheck. The Router drops the stale subscription and restarts the component withfrom_timeset to the moment of failure. On re-subscribe, the restarted Manager replays everything still in the cache.Loop prevention. An event received from the Switch is never forwarded back out through the Connector it arrived on.
What is NOT guaranteed¶
No durable delivery. Events older than
EDRI_CACHE_TIMEOUTare evicted from the cache permanently. A restart, crash, or outage longer than the cache window causes permanent event loss.No exactly-once. Events may be duplicated in edge cases: a Manager that crashes after processing an event but before the Router detects the failure will receive that event again on restart (replay from cache). Handlers should be idempotent.
No persistence across Router restart. The cache is in-memory only. A Router restart clears it completely.
Failure matrix¶
Scenario |
Event loss possible? |
Duplication possible? |
Recovery mechanism |
|---|---|---|---|
Manager restart (within cache window) |
No |
Yes (replayed events) |
Router replays from cache on re-subscribe |
Manager crash during handler |
No (within window) |
Yes |
Restart + replay from cache |
Manager outbound backlog past hard limit |
No (within window) |
Yes |
Disconnect + restart + replay from cache |
Connector/Switch outage (within cache window) |
Possible; recoverable when source Router still retains the event and reconnect markers cover the lost range |
Possible |
Marker-based replay on reconnect |
Connector/Switch outage (exceeding cache window) |
Yes |
No |
Events evicted from cache, permanently lost |
Router crash |
Yes (all cache cleared) |
No |
Full restart, no recovery |
Handler timeout |
Yes (event may be lost) |
No |
Logged, Manager continues |
Serialization failure |
Yes (event cannot be sent) |
No |
Logged, event dropped |
Idempotence¶
Because duplicate delivery is possible (especially after a Manager restart), handlers should be idempotent where duplicate execution would cause problems. For example:
Use
INSERT ... ON CONFLICT DO NOTHINGinstead of plainINSERT.Check for existing state before creating resources.
Use the event
_id(UUID) for deduplication where the application has access to storage.
Health checking (Router)¶
The Router includes a built-in HealthChecker that schedules periodic HealthCheck events (which double as a heartbeat to every Manager's pipe), aggregates the replies, escalates the status of components that fall silent, and restarts a component when delivering to it fails with a broken pipe or its outbound backlog exceeds the hard limit (see Delivery guarantees and Non-blocking fan-out).
The full picture — the status endpoint, the per-component records and vitals, and the component-side health() / collect_custom_vitals() hooks — lives on its own page: Health checking.
Topology endpoint¶
GET /edri/topology returns the live routing graph assembled from Router state:
every connected component with the request and response event types it is
subscribed to, its health status, and its outbound-worker backlog (queue depth,
slow and failed flags, and the seconds a send has been stalled once it
passes EDRI_ROUTER_OUTBOUND_STALL_SECONDS, default 5); plus the connector
edge — the shareable types crossing to the Switch. It serves HTML to a browser
and JSON to an API client from the same URL.
The endpoint is enabled by default only outside production. Set
EDRI_API_ENABLE_TOPOLOGY=true to expose it in production, or false to
disable it elsewhere; while disabled the URL returns 404 and is absent from
the generated API documentation.
It surfaces what the health page cannot: an event type with more than one handler, and a component that still holds a subscription while its health has gone bad (events routed to a pipe nobody reads).
It also lists unrouted events — application event types the Router had to
drop because nothing was subscribed, with a drop count and the time of the last
drop. An entry is marked HANDLER GONE when the Router still holds a (now
empty) subscription for that type and NOT WIRED when it holds none.
A type appears only after an event of it is actually dropped, and its entry is
cleared again the moment anything subscribes to that type — a restarting
component replays the requests it missed from the Router cache,
so a restart leaves nothing behind (a restart longer than
EDRI_CACHE_TIMEOUT does lose events, but they are outside the replay window
and are reported in the log, not here). The Router's own events (health checks,
EdriHealth, this endpoint) are never listed. The tally is in-memory and resets
when the Router restarts.
Switch integration¶
The Switch connects routers to each other.
Routers publish shareable events to the Switch.
Routers receive shareable events from the Switch.
Switch configuration¶
EDRI_SWITCH_HOSTEDRI_SWITCH_PORT
Notes¶
The Router routes by event type and direction (request vs. response).
It is independent of transport (REST, WebSocket, HTML, etc.).
It does not inspect API metadata or perform payload-level filtering.