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 Router to the 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

        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:

        flowchart TD
    subgraph Conn["Connector (process)"]
        direction TB
        Main["main loop (run)<br/>connect/reconnect Socket<br/>send UUID + demands<br/>restart receiver if needed"]
        Recv["receiver thread<br/>reads Switch frames"]
        Send["sender thread<br/>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<br/>SubscribeConnector<br/>to Router"]
    Recv -->|"remote events"| Router
    

Legend: The dotted arrow (-.->) represents a 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

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

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:

@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:

        flowchart TD
    SW[Switch]
    SW <--> E1["EDRI #1<br/>Router + Connector"]
    SW <--> E2["EDRI #2<br/>Router + Connector"]
    SW <--> E3["EDRI #3<br/>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

        stateDiagram-v2
    [*] --> IDLE
    IDLE --> CONNECTING: start()
    CONNECTING --> ACTIVE: connected
    CONNECTING --> RETRY: connection failure
    ACTIVE --> RETRY: connection loss
    RETRY --> CONNECTING: linear backoff<br/>(+5 s per attempt,<br/>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.

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

  • Switch — the Switch server that Connectors connect to

  • Router — local event dispatching, shareable events

  • Event — the shareable flag on events

  • Environment Variables — environment variables for Switch and cache

  • Deployment — deploying distributed EDRI