Expand description
Blocking socket ⇄ AsyncRead/AsyncWrite adapters: the byte source for
every reactor-free driver in the workspace.
socket --read--> reader pump thread --Vec<u8> chunks--> ChannelReader
|
whatever drives the protocol future
|
socket <--write-- writer pump thread <--framed bytes-- ChannelWriter§Why this lives in epics-base-rs and not in a protocol crate
It was written once, inside epics-pva-rs’s blocking server driver, and
the obvious next move was to promote it within that crate so the PVA client
could reach it too. Measured, that destination is wrong:
epics-ca-rs does not depend on epics-pva-rs and must not — the only
crate that depends on both is epics-bridge-rs, which sits above them
(doc/calink-rtems-design.md §3.3). A primitive promoted inside
epics-pva-rs is one the CA client structurally cannot call, so the next CA
increment writes a third copy — exactly the outcome “one seam, two callers”
exists to prevent.
So it lands here, beside the rest of its family: runtime::task::spawn,
block_on_sync/park_on, StackSizeClass, spawn_dedicated_thread,
enter_ioc_thread. Every protocol crate can reach it, and none of them owns
it.
§The seam is the byte source, not the frame pipeline
Nothing here parses anything. Both pumps move Vec<u8> and neither knows
whether the bytes are PVA frames, CA messages, or noise; the protocol future
on the other side of the adapters is untouched and uncompiled-differently.
That is what makes a driver built on this primitive arguable from the hosted
driver’s own tests: same parser, same select!, same handlers, different
implementors of two dyn traits.
§Two facts that must not be re-derived
- No fd dup. The read and write roles come from one descriptor
shared through an
Arc, viaimpl Read for &TcpStream/impl Write for &TcpStream— nevertry_clone.try_cloneisfcntl(F_DUPFD_CLOEXEC), and on RTEMS 6 that cannot work for a socket: RTEMS’sfcntlhas noF_DUPFD_CLOEXECcase at all (cpukit/libcsupport/src/fcntl.c:146-220falls todefault: errno = EINVAL), and even plainF_DUPFDfails becauseduplicate_iopcalls the file’sopen_hwhile rtems-libbsd installsrtems_bsd_sysgen_nodeopson every socket. Measured on the target:dup,F_DUPFDandF_DUPFD_CLOEXECall fail on a socket whileF_DUPFDon/dev/consolesucceeds. A caller that reaches fortry_clonecompiles and fails at runtime on target only. - A blocking write needs a deadline, not a per-syscall timeout.
SO_SNDTIMEObounds eachwritesyscall, so a peer that accepts one byte per tick never trips it and holds the pump thread indefinitely — and a target that does not implement the option at all (VxWorks 7) has no bound whatever.write_frame_deadlinetherefore waits for writability against its own deadline and sets no socket option.
§Lifecycle: a pump you cannot spawn without holding the thing that ends it
spawn_reader_pump and spawn_writer_pump each return an adapter and
a guard, and there is no way to obtain the former without the latter. The
guards’ Drop is what retires the threads, which is what makes every exit
path — clean return, ?, and a panic unwinding through the caller — covered
without any cleanup written on an error branch.
The two guards end their threads differently because the threads park differently:
| guard | how its thread is parked | how the guard returns it |
|---|---|---|
ReaderPumpGuard | inside a blocking read behind an effectively-infinite SO_RCVTIMEO | shutdown(Shutdown::Both) on the shared socket, then join |
WriterPumpGuard | inside recv() on the frame channel | drop the only strong sender, then join |
A caller that needs a specific teardown order — writer down first so frames emitted on the way out reach the wire, then reader — gets it by dropping the guards in that order, or by declaring them in the reverse of it.
The reader guard’s row is a POSIX contract: shutdown waking a thread
parked in a blocking read is what unix (RTEMS included) provides and
Windows does not (measured, PR #56 CI 2026-07-24 — the parked read
outlived a 120 s bound). That is why exec_backend, the only
configuration that runs these pumps in production, refuses Windows at
compile time (lib.rs), and why the tests asserting this contract are
#[cfg(unix)].
§Where the async goes
block_on_sync is the single bridge, in both pumps. On a bare thread it
parks; on a multi-thread runtime worker it hands the worker off first. It is
not blocking_send, which panics inside a runtime context and would
make this module unusable from a hosted worker.
§Before the pumps: the dial
A reactor-free driver cannot await a connect either, so the blocking
connect needs a thread just as the two pumps do — and for the same reason,
at the same band. DialPool owns those threads. It is here rather than in
a protocol crate for the reason the pumps are: epics-ca-rs and
epics-pva-rs both dial and neither may depend on the other.
Structs§
- Channel
Reader AsyncReadover a channel of byte chunks — the blocking stand-in for a socket read half.- Channel
Writer AsyncWriteover a channel of frames — the blocking stand-in for a socket write half.- Dial
Pool - A bounded, permanent set of threads that own this role’s blocking TCP dials.
- Guarded
Reader - A
ChannelReaderthat owns its pump guard. - Guarded
Writer - A
ChannelWriterthat owns its pump guard. SeeGuardedReader. - Pump
Config - Everything a caller needs to set up on a socket before its pumps start.
- Reader
Pump Guard - The spawned reader pump, woken and joined on every exit path.
- Writer
Pump Guard - The spawned writer pump and the only strong frame sender, retired together on every exit path.
Constants§
- DEFAULT_
READ_ CHUNK - One blocking read, sized to match the frame readers that consume it so the byte arrival pattern is the hosted one.
- MAX_
DIAL_ WORKERS - How many dial threads one
DialPoolmay ever create.
Functions§
- circuit_
roster - The role roster a circuit’s
drive_socket_blockingpool must be built with:[reader, writer], both onSmallstacks. The two bands are the caller’s to choose (seedrive_socket_blocking’s docs). - drive_
socket_ blocking - Drive one already-connected socket with two blocking pumps, returning the two owning adapters.
- is_
socket_ timeout - A blocking socket op hit its
SO_RCVTIMEO/SO_SNDTIMEO. - pending_
bytes - Bytes pending in the socket receive queue via
FIONREAD. - spawn_
reader_ pump - Drive
sock’s read half on a pooledworker, yielding theAsyncReadhalf of the seam and the guard that retires the job. - spawn_
writer_ pump - Drive
sock’s write half on a pooledworker, yielding theAsyncWritehalf of the seam and the guard that retires the job. Infallible for the same reason asspawn_reader_pump. - write_
frame_ deadline - Write one whole frame under a deadline, not merely a per-syscall timeout.