# Router The **Router** is the central dispatcher in EDRI. * It receives {doc}`/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 {ref}`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](#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](#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)= ## 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)= ## 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](#delivery-guarantees) and [Non-blocking fan-out](#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: {doc}`/health`. --- ## 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](#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.