Runs N single-threaded reactor loops in parallel, one per worker thread,
all sharing a single listener fd. Each worker registers the
listener fd in its own reactor with EPOLLEXCLUSIVE (on Linux
= 4.5) so the kernel wakes exactly one worker per accept event.
On macOS / kqueue the flag is unavailable and registration falls
back to plain register — the practical behaviour is similar
because non-blocking accept returns EAGAIN on the losers.
This is the redesign of the multicore path. the prior implementation
used N independent listeners bound with SO_REUSEPORT; the
kernel then distributed accepted connections across workers by
hashing each connection's 4-tuple to one of the N listeners. That
scheme has a known fairness gap under bursty arrival: a
256-connection storm from a wrk2-style load generator can land
80+ conns on one worker and 30 on another, producing
hundreds-of-millisecond head-of-line tail latency on the
overloaded worker(s). benchmark/configs/throughput_mc
shows the failure mode at p99 ≈ 1.7 s; the
EPOLLEXCLUSIVE-based shared-listener design collapses the
same workload's p99 to the millisecond range.
Shutdown path: shutdown() flips a heap-allocated Bool that
every worker polls on each reactor iteration. A stable heap address
is used (instead of a field on the Scheduler struct) so the flag
survives the NRVO-or-not move from Scheduler.start back to the
caller. The main thread joins all workers, and only then closes the
shared listener fd (once), before shutdown() returns.
The listener is never closed to wake the workers. On Linux an fd that
is also registered in a worker's epoll instance is kept alive by
the kernel, so close() from another thread triggers no EPOLLHUP
and the workers stay blocked in epoll_wait until the 100 ms poll
timeout fires; and closing it early let a worker that polled a batch
before it saw the flag accept on a reused number (CONC-05). The
heap flag is what breaks the loop.
This module is :trait:Frontend-generic: every worker calls
:meth:Frontend.run_worker once with its assigned listener fd
and the shared stopping flag. The frontend value is moved
(per-worker copies are made via F.copy(); if that's expensive
users should wrap their handler's expensive state behind an
Pointer or a similar shared-reference holder).
The scheduler used to import directly from
:mod:flare.http._server_reactor_impl,
:mod:flare.http._unified_reactor_impl,
:mod:flare.http.handler, :mod:flare.http.server,
:mod:flare.http.static_response, and :mod:flare.http2.server.
That layering violation (runtime depending on protocol modules)
is gone now; the scheduler depends only on the
:trait:flare.runtime.Frontend trait and concrete impls live
where their protocol does
(:class:flare.http.HttpFrontend /
:class:flare.http.StaticHttpFrontend).
Known limitations:
- The stopping flag is a heap-allocated byte written from the main
thread and read from each worker through
Atomic[UInt8]
release-store / acquire-load (store_stop_flag /
load_stop_flag). This gives the workers a proper happens-before
edge on shutdown and lowers to a plain mov on x86-64 (TSO) and
to stlr / ldar on ARM64. The per-iteration re-materialisation
of the pointer inside the frontend's serving loop still defeats the
optimiser's LICM so every iteration re-reads the flag.
- Worker panics that escape :meth:
Frontend.run_worker are caught and
discarded in _worker_entry because pthread has no exception
channel. is_running() still reports True until the
ThreadHandle is joined, which is a mildly wrong signal for a
worker that crashed rather than shut down cleanly. Plumbing a
per-worker error cell back to the Scheduler is also scheduled for a future release.
.