Skip to main content

Module accept

Module accept 

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

errnoio::ErrorKind
EBADF (9) — listener fd is goneUncategorized
ENOTSOCK (88) — not a socketUncategorized
EMFILE (24) — fd table full, transientUncategorized
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-121errlogPrintf, 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§

AcceptBackoff
Consecutive-failure counter for one accept loop.

Constants§

CEILING
Longest delay between two accept attempts. Also the steady-state log rate while a listener stays broken: one line per ceiling.
FIRST
Delay after the first failure. Doubles per consecutive failure.