Reactor
struct ReactorAn OS-event-loop handle with a uniform API over epoll and kqueue.
Typical usage (single-threaded server loop):
from flare.runtime import Reactor, Event, INTEREST_READ
var reactor = Reactor()
reactor.register(listen_fd, UInt64(0), INTEREST_READ)
var events = List[Event]()
while True:
var n = reactor.poll(-1, events)
for i in range(n):
var evt = events[i]
if evt.is_wakeup():
continue
# dispatch by token ...
Implements Movable
__init__
def __init__(out self) Create a new reactor with its wakeup fd pre-registered.
Raises NetworkError: If any of the underlying syscalls fails (epoll_create1, eventfd, pipe, kqueue, initial registration).
__deinit__
def __deinit__(deinit self) Close the reactor fd and the wakeup fds.
register
def register(mut self, fd: Int32, token: UInt64, interest: Int) Register fd with the reactor under token with the given interest bits.
- Args
fd: The file descriptor to watch.token: Opaque value returned in events for this fd. Must not equalWAKEUP_TOKEN.interest: Bitmask ofINTEREST_READ | INTEREST_WRITE.
Raises NetworkError: If fd is already registered, token equals
the sentinel, interest is 0, or the underlying syscall
fails.
register_exclusive
def register_exclusive(mut self, fd: Int32, token: UInt64, interest: Int) Register fd with EPOLLEXCLUSIVE semantics on Linux.
Used by the multi-worker scheduler so all workers share a single
listener fd: each worker registers the same fd in its own epoll
instance with EPOLLEXCLUSIVE, and the kernel wakes only one
worker per accept-event instead of all of them. This eliminates
the SO_REUSEPORT 4-tuple-hash distribution variance that drives
head-of-line tail latency under high concurrency on this
benchmark probe.
On macOS / kqueue there is no exclusive-wakeup flag; this method
falls back to plain register. Modern macOS kernels still
deliver each EVFILT_READ event to one watcher at a time on
the same listener fd (the rest see EAGAIN on accept),
so the practical behaviour is similar minus the kernel-level
fairness guarantee.
Registration uses EPOLLIN | EPOLLEXCLUSIVE and deliberately
omits the EPOLLRDHUP that register adds for read interest:
epoll_ctl(2) rejects that combination with EINVAL. Wakeups
are exclusive, not balanced: the kernel wakes one waiting worker
per event, so a lightly loaded server may keep landing new
connections on the same worker.
EPOLLEXCLUSIVE is only valid on the initial EPOLL_CTL_ADD
— modifying an existing registration to add it is rejected by
the kernel. The shared-listener path therefore registers once
per worker and never modifies the listener entry.
- Args
fd: Listener fd, expected to be shared across workers.token: Opaque value returned in events. Must not equalWAKEUP_TOKEN.interest: Bitmask; in practice alwaysINTEREST_READfor the listener path.
Raises NetworkError: Same conditions as register.
modify
def modify(mut self, fd: Int32, interest: Int) Change the interest bits for an already-registered fd.
The token remains the same; pass the interest you want going forward.
- Args
fd: Previously-registered fd.interest: New interest bits (must be non-zero).
Raises NetworkError: If fd is not registered or the syscall fails.
unregister
def unregister(mut self, fd: Int32) Remove an fd from the reactor.
- Args
fd: Previously-registered fd.
Raises NetworkError: If fd is not registered.
poll
def poll(mut self, timeout_ms: Int, mut out: List[Event], max_events: Int = Int(64)) -> Int Wait for events and append them to out.
The caller owns out — existing contents are replaced (cleared
first). On success returns the number of events appended. Wakeup
events are included; callers filter via Event.is_wakeup().
- Args
timeout_ms: -1 = block indefinitely, 0 = poll (return immediately), positive = max milliseconds to wait.out: Output list. Cleared on entry.max_events: Maximum events to return in one call.
Raises NetworkError: On underlying syscall failure (EINTR is handled internally and returns 0).
wakeup
def wakeup(self) Trigger a wakeup event so a sleeping poll returns promptly.
Safe to call from any thread (Stage 2+ will exercise this). On Linux, writes 8 bytes to the eventfd; on macOS, writes 1 byte to the self-pipe. Errors are swallowed because the caller has no meaningful recovery — a dropped wakeup just means the reactor will wait out the current timeout.
wake_fd
def wake_fd(self) -> Int The fd another thread writes to make poll return.
Lets a peer wake this reactor without holding a reference to it
(flare.runtime._balance publishes it for connection
hand-off). Write 8 bytes; poll drains them.
registered_count
def registered_count(self) -> Int Return the number of fds currently registered (excludes wakeup).
is_registered
def is_registered(self, fd: Int32) -> Bool Return True if fd is currently registered on the reactor.