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

  1. Dispatch events to subscribed Managers.

  2. Maintain subscriptions (which managers handle which event types).

  3. Bridge routers via the Switch (cross-process / cross-node distribution).

  4. 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 component WARNING and 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:

  1. A component produces an event and puts it into the Router queue.

  2. The Router dispatches the event to all subscribed Managers.

  3. A Manager may:

    • set event.response and finish (response is sent automatically at the end of the handler)

    • emit additional events via self.router_queue.put(...)

    • start Workers

  4. Workers may send events back to their Manager via event_send(...).

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


Shareable events

Some events are marked as shareable.

A shareable event is allowed to leave the local appliance and be forwarded through the Switch to other routers.

  • @event(..., shareable=True) marks an event type as shareable.

  • Non-shareable events are handled locally.

Use shareable events when you want distributed processing across multiple router instances.


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_TIMEOUT

    • how long events are kept in the Router cache

  • EDRI_CACHE_INFO_MESSAGE

    • interval 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_TIMEOUT seconds (default 30). When a Manager subscribes with a from_time, the Router immediately replays all cached events of that type (matched by request/response direction) that arrived at or after from_time. With no from_time, the Router replays the entire cached window for that event type (everything still inside EDRI_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 with from_time set 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_TIMEOUT are 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 NOTHING instead of plain INSERT.

  • 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_HOST

  • EDRI_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.