Expand description
Backoff for a server accept loop, shared by every accept loop in the workspace.
§The defect this exists to close
accept() can fail without the listener being broken — EMFILE/ENFILE
when the fd table is full, ENOMEM/ENOBUFS under memory pressure. On
those the pending connection stays queued, so a loop that logs and
immediately retries spins at 100% CPU and floods the log, at exactly the
moment the machine has no resources to spare. Two of this workspace’s
accept loops did that.
C rsrv has the shape: epicsThreadSleep(15.0); continue; at all three of
its failure points (caservertask.c:92, :102, :118).
§Why the retry/give-up decision is not made from the error
The obvious design is to classify the io::Error — retry EMFILE, return
on EBADF. Measured on this host, that is not expressible:
| errno | io::ErrorKind |
|---|---|
EBADF (9) — listener fd is gone | Uncategorized |
ENOTSOCK (88) — not a socket | Uncategorized |
EMFILE (24) — fd table full, transient | Uncategorized |
EINVAL (22) | InvalidInput |
The fatal cases and the transient one collapse into the same variant, so
ErrorKind cannot separate them. Matching raw_os_error() instead would
work per-platform, but the one kind that is distinguishable —
InvalidInput — is precisely what RTEMS returns spuriously from socket
calls while its libc omits the BSD sin_len byte, so a rule keyed on it
would make every RTEMS accept loop quit on its first call.
So there is no decision to make: the loop always retries, exactly as C does. The only thing this type computes is how long to wait first.
§There is no give-up, and that is C parity
An earlier revision returned after 64 consecutive failures. That was a
deliberate deviation and it was wrong: C rsrv has no such path
(caservertask.c:82-121 — errlogPrintf, epicsThreadSleep(15.0),
continue, at every one of its three failure points, with
cantProceed("Unreachable. Perpetual thread.") after the loop to say so).
On target the deviation meant an IOC that had seen a minute of EMFILE
stopped accepting for the life of the process while every other part of
it went on looking healthy — the worst available outcome, and unrecoverable
without a reboot, where C would have resumed the moment an fd freed.
What is kept from that revision is the geometric backoff: FIRST to
CEILING, which recovers from a transient blip about 15× faster than C’s
flat 15 s while still capping the retry rate once saturated.
§Saying so without a subscriber
C prints on every failed accept, through errlogPrintf, which reaches
the console whatever the log configuration. Ours does both: a tracing
event per failure for a host with a subscriber (C parity, exactly), and an
errlog line on the 1st, 2nd, 4th, 8th … consecutive failure, which
reaches an RTEMS console that has no subscriber at all
(crate::runtime::log::errlog_sev_printf). Powers of two because the
backoff ceiling is 1 s where C’s is 15 s: printing every failure at our
cadence would be 15× C’s console traffic on a serial line. It needs no
clock, and it cannot suppress the first occurrence.
Structs§
- Accept
Backoff - Consecutive-failure counter for one accept loop.