The radicle-node’s networking is driven by a reactor: a single-threaded
event loop built on mio (epoll/kqueue) that handles
all of the node’s non-blocking I/O. It is implemented in
crates/radicle-node/src/reactor.rs.
The reactor knows nothing about the radicle protocol. It only manages sockets and timers, dispatches readiness events to a service, and performs whatever actions the service asks for in return. This keeps the node’s protocol and state logic free of any direct contact with file descriptors or mio.
The loop
The reactor owns a dedicated thread that blocks on a single poll()
syscall (Runtime::run). Each iteration wakes for one of three reasons:
- I/O readiness on a registered socket — a connection has data to read, or has become writable.
- A wake from another thread, sent through the
Controller’sWaker. - A timeout firing, set previously via
Action::SetTimer.
When poll() returns, the reactor:
- Calls
service.tick()to advance the service’s notion of time. - Fires
service.timer_reacted()if any timers expired. - Dispatches I/O events to the resources, passing the resulting
reactions to the service (
handle_events). - Drains control messages (commands, shutdown) if it was woken.
- Pulls actions out of the service and executes them
(
handle_actions).
If the service spends more than LAG_TIMEOUT (100ms) handling a batch,
the reactor logs a warning — a signal that the node is too slow to keep
up with its I/O.
The two halves
There is a clean split between the generic I/O plumbing and the node’s logic:
Reactor— the public handle. It spawns the reactor thread and hands out aControllerused to send commands or shutdown from other threads.Runtime— the blocking event loop that runs on the reactor thread.ReactionHandler— the trait the service implements. The reactor calls into it (tick,transport_reacted,handle_command, …) and pullsActions back out of it via itsIteratorimpl.
Resources
The reactor manages two kinds of resources, distinguished by whether they can be written to:
- Listeners — read-only sources that spawn new resources, e.g. a
TcpListeneraccepting incoming connections. - Transports — full read/write connections, i.e. the peer sessions.
Both implement EventHandler, whose handle method converts raw mio
readiness into service-level reactions, and interests, which tells
the reactor which events the resource currently cares about.
Reactions and actions
The data flow each iteration is:
- The reactor polls and receives I/O events.
- For each event, the resource’s
EventHandler::handleproduces reactions, which are passed to the service vialistener_reacted/transport_reacted. - The service processes them and yields
Actions. - The reactor executes those actions in
handle_action.
The actions a service can emit are:
RegisterListener/RegisterTransport— start polling an already-active resource.UnregisterListener/UnregisterTransport— stop polling a resource and hand it back to the service (e.g. to pass to a worker thread or close it). The reactor itself never closes file descriptors.Send— write bytes to a transport.SetTimer— schedule a timeout that firestimer_reactedlater.
Why it’s structured this way
Because the service never touches mio or sockets directly — it only reacts to events and emits actions — the node’s core protocol logic can run single-threaded without locks on shared state, while the reactor still multiplexes many concurrent connections on one thread.