# Connector
> **Security warning:** The Connector authenticates to the Switch with a pre-shared key (`EDRI_SWITCH_SECRET` / `EDRI_SWITCH_SECRET_FILE`) but the link is **not** encrypted, and both sides exchange Python pickle, which is not safe against a compromised peer. Connecting to a non-loopback Switch requires the secret. Keep Switch traffic on a trusted, isolated network and add a VPN, mTLS, or service mesh for anything sensitive.
The **Connector** bridges a local {doc}`/router` to the {doc}`/switch` — a TCP/IP hub that connects multiple EDRI instances across a network.
Without a Connector, events stay inside a single EDRI process tree. With a Connector, shareable events can travel to other machines.
---
## Where the Connector fits
```mermaid
flowchart LR
subgraph A["EDRI Instance A"]
direction LR
RA[Router] <--> CA[Connector]
end
subgraph B["EDRI Instance B"]
direction LR
CB[Connector] <--> RB[Router]
end
CA <-->|"TCP/IP (Switch)"| CB
```
The Connector lives inside the EDRI process tree, started from the main thread like any other component.
---
## How it works internally
The Connector runs as its own process. Inside it there is a main loop plus two background threads, all sharing one `Socket` helper that owns the TCP connection to the Switch:
```mermaid
flowchart TD
subgraph Conn["Connector (process)"]
direction TB
Main["main loop (run)
connect/reconnect Socket
send UUID + demands
restart receiver if needed"]
Recv["receiver thread
reads Switch frames"]
Send["sender thread
sender queue → Switch"]
end
Router["local Router"] -. pipe .-> Main
Main -->|"SubscribedNew"| D1["send updated demands to the Switch"]
Main -->|"LastEvents / SendFrom"| D2["cache-replay coordination"]
Main -->|"other shareable events"| Send
Recv -->|"remote demands"| D3["update remote map
SubscribeConnector
to Router"]
Recv -->|"remote events"| Router
```
> **Legend:** The dotted arrow (`-.->`) represents a [`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe) — the inter-process communication channel between the Router and the Connector process. Solid arrows represent data flow within the same process or across the TCP connection.
### Responsibilities
| Part | Purpose |
|-------------------|--------------------------------------------------------------------------|
| `Socket` | Owns the TCP connection; authenticated handshake, framing, (re)connect, `send_demands`, `send_router`, `receive` |
| main loop (`run`) | Connects, propagates local subscription changes as demands, and routes local shareable events to the sender queue |
| `receiver` thread | Reads from the Switch: applies remote demands and delivers remote events to the local Router |
| `sender` thread | Serializes and sends queued shareable events to the Switch |
---
## Event continuity (best-effort replay)
The Connector helps shareable events survive a brief network interruption, backed by the Router's in-memory cache rather than a durable queue:
1. The Router caches every routed event for `EDRI_CACHE_TIMEOUT` seconds, tagged with the originating Router UUID and a stable event ID.
2. On (re)connect, the Connector re-announces its Router UUID and demands, then asks the peer — via a `LastEvents` exchange — for the **ID of the last event it received** from this instance.
3. The peer replays, from its cache, every shareable event recorded **after** that ID (`SendFrom`), then resumes normal flow.
This means:
* Recovery is **pull-based replay from a marker**, not an acknowledged queue — each side tells the other where to resume. Events delivered after the marker are not re-sent (approximate deduplication), and events before the outage's end are replayed from the cache.
* The cache window is the **only** safety net: an outage longer than `EDRI_CACHE_TIMEOUT` (default 30 s) drops the older events permanently. This is why `EDRI_CACHE_TIMEOUT` must be at least 10 s.
* Duplication is possible in edge cases: if an event was delivered but its acknowledgement had not yet been replicated before the connection dropped, the marker-based replay may resend it. Handlers should be **idempotent** if duplicate events are unacceptable.
* Short outages (seconds) are transparent to the application.
---
## Configuration
### Environment variables
| Variable | Default | Description |
|---------------------------|-------------|--------------------------------------------------|
| `EDRI_SWITCH_HOST` | `localhost` | Switch hostname or IP address |
| `EDRI_SWITCH_PORT` | `8899` | Switch TCP port |
| `EDRI_SWITCH_SECRET` | *(unset)* | Pre-shared key; must match the Switch. Required unless the Switch is on loopback |
| `EDRI_SWITCH_SECRET_FILE` | *(unset)* | Path to a one-key-per-line file; takes precedence over `EDRI_SWITCH_SECRET` |
| `EDRI_SWITCH_MAX_MESSAGE` | `67108864` | Largest frame payload the Connector will send, in bytes (64 MiB) |
| `EDRI_CACHE_TIMEOUT` | `30` | How long events stay in the local cache (seconds)|
| `EDRI_CACHE_INFO_MESSAGE` | `60` | Log interval for cache statistics (seconds) |
### Starting the Connector
```python
from uuid import UUID
from edri import EDRI
edri = EDRI()
# Optional: assign a specific Router ID for reconnection
router_id = UUID("12345678-1234-5678-1234-567812345678")
edri.start_connector(router_id=router_id)
edri.run()
```
If no `router_id` is provided, a random UUID is generated.
### Connecting to a remote Switch
```python
import os
os.environ["EDRI_SWITCH_HOST"] = "192.168.1.100"
os.environ["EDRI_SWITCH_PORT"] = "8899"
from edri import EDRI
edri = EDRI()
edri.start_connector()
edri.run()
```
---
## Shareable events
Only events marked as **shareable** are forwarded through the Connector to the Switch:
```python
@event(shareable=True)
class CrossNodeEvent(Event):
"""This event can leave the local appliance."""
data: str
response: CrossNodeResponse
```
An event without `shareable=True` will never leave the local Router, even if a Connector is active.
---
## Multiple Connectors
An EDRI instance may have at most one active Connector. For multi-node setups, the topology is typically:
```mermaid
flowchart TD
SW[Switch]
SW <--> E1["EDRI #1
Router + Connector"]
SW <--> E2["EDRI #2
Router + Connector"]
SW <--> E3["EDRI #3
Router + Connector"]
```
Each router connects through its own Connector to the same Switch. The Switch handles message distribution.
---
## Demand (subscription) propagation
The Connector implements **demand propagation** — it tells the Switch which event types the local Router is interested in:
1. When a Manager subscribes to an event type, the Router records it and notifies the Connector (a `SubscribedNew` event).
2. The Connector's main loop detects the change and sends the updated demand list to the Switch (`Socket.send_demands`).
3. The Switch forwards the demands to all other connected routers.
4. Remote routers use this information to decide which events to send.
This means:
* Remote routers only send events that the local router actually wants.
* If no local Manager subscribes to `SomeEvent`, that event type is never forwarded from the Switch.
* Demand changes are propagated automatically when Managers start/restart.
---
## Resilience and reconnection
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> CONNECTING: start()
CONNECTING --> ACTIVE: connected
CONNECTING --> RETRY: connection failure
ACTIVE --> RETRY: connection loss
RETRY --> CONNECTING: linear backoff
(+5 s per attempt,
capped at EDRI_CACHE_TIMEOUT)
```
* On connection failure, the Connector retries with **linear** backoff — the delay starts at 5 s and grows by 5 s per attempt, capped at `EDRI_CACHE_TIMEOUT`.
* During retry, events destined for the Switch are queued locally.
* When reconnected, queued events are sent before new ones.
* The Router cache (`EDRI_CACHE_TIMEOUT`) keeps additional backup.
---
## Security
The Connector authenticates to the Switch with a pre-shared key and then exchanges pickle-serialized frames over a raw TCP socket. This has the following implications:
* **Pre-shared key** — `EDRI_SWITCH_SECRET` (or `EDRI_SWITCH_SECRET_FILE`) must match the Switch's. The mutual handshake proves both sides hold the key and binds the Router UUID into the transcript, so an outsider cannot connect or impersonate a router. The Connector refuses to dial a non-loopback Switch without a secret. Prefer `EDRI_SWITCH_SECRET_FILE` with a mounted secret over an inline environment value, which is readable through `/proc`, `docker inspect`, and pod specs.
* **Shared key, not per-peer** — every node in a cluster holds the same key, so authentication is against outsiders, not between members. Restrict which peers are given the key.
* **No transport encryption** — frames travel over TCP in the clear. The per-frame MAC authenticates but does not conceal; a network-level attacker can read every event payload. If payloads contain credentials or personal data, use a VPN, mTLS proxy, or service mesh.
* **Pickle is not safe against a compromised peer** — an authenticated peer can still send arbitrary pickle, which the other side unpickles.
* **Event payloads may appear in logs** — error-level logging includes serialized event contents. Configure log levels appropriately in production.
### Recommended network setup
| Environment | Recommended isolation |
|-------------|----------------------|
| Development | Same machine or private Docker network |
| Production | Private subnet + firewall (allow only specific peer IPs) |
| Kubernetes | NetworkPolicy restricting ingress to Switch port 8899 |
| Multi-datacenter | VPN or WireGuard tunnel between sites |
## Pickle compatibility
Both sides must run compatible Python versions, and event class definitions must match. If you change event classes, restart all connected instances to avoid deserialization errors.
---
## Gotchas & Notes
* The Connector only forwards **shareable** events (`@event(shareable=True)`).
* The Connector must be able to resolve `EDRI_SWITCH_HOST`. Use IP addresses for production to avoid DNS issues.
* The Switch must be running and reachable before the Connector starts.
* Each EDRI instance can have at most one Connector.
---
## See also
* {doc}`/switch` — the Switch server that Connectors connect to
* {doc}`/router` — local event dispatching, shareable events
* {doc}`/event` — the `shareable` flag on events
* {doc}`/env` — environment variables for Switch and cache
* {doc}`/deployment` — deploying distributed EDRI