| Safe Haskell | Safe-Inferred |
|---|---|
| Language | GHC2021 |
Test.Network.Ports
Description
Utility module providing functions to find and allocate random ports.
Ports are handed out from a private band below the kernel's ephemeral range
(see portBandStart), tracked in a process-global registry so no two
allocations in one test process can collide, and verified free with an
actual bind before being returned.
Rationale: consumers (etcd, cardano-node, warp, ...) bind their port some
time after allocation, and tests that restart their subprocess leave the
port unbound in between. Any OS-assigned loopback port (a bind(0), or the
source port of a connect(2)) handed out during such a window used to be
able to land on the reserved port and pin it, since the kernel draws those
from ip_local_port_range. Allocating below that range makes the windows
collision-free against everything except an explicit bind of a specific
port in the band by a concurrent process; concurrent test processes of the
same user coordinate through per-port file locks (see tryLockPort), which
the kernel releases when the owning process dies.
Synopsis
- portBandStart :: PortNumber
- portBandEnd :: PortNumber
- reservedPortsRef :: IORef (Set PortNumber)
- nextCandidateRef :: IORef PortNumber
- ephemeralRangeCheck :: ()
- allocatePort :: Maybe (PortNumber -> PortNumber) -> IO PortNumber
- getRandomPort :: IO PortNumber
- withFreePort :: (PortNumber -> IO a) -> IO a
- withFreePortAndDerived :: (PortNumber -> PortNumber) -> (PortNumber -> IO a) -> IO a
- randomUnusedTCPPorts :: Int -> IO [Int]
- randomUnusedTCPPortsWithDerived :: (PortNumber -> PortNumber) -> Int -> IO [Int]
- portLocksRef :: IORef [FileLock]
- tryLockPort :: PortNumber -> IO Bool
- portLockDir :: IO FilePath
- bindSpecificLoopback :: PortNumber -> IO Socket
- c_socket :: CInt -> CInt -> CInt -> IO CInt
- afInet :: CInt
- sockStream :: CInt
- sockNonBlock :: CInt
- sockCloexec :: CInt
- openStreamSocketCloexec :: IO Socket
Documentation
portBandStart :: PortNumber Source #
Lowest port handed out by the allocator. The band must sit below the ephemeral range (32768 and up on Linux by default, 49152 on darwin) so that OS-assigned ports can never land on an allocated one.
portBandEnd :: PortNumber Source #
One past the highest port handed out by the allocator.
reservedPortsRef :: IORef (Set PortNumber) Source #
All ports ever handed out (or found occupied) in this process, including derived companion ports. Never released: the band is large enough for any test run, and never reusing a port means a subprocess still draining its listen socket cannot break a later allocation.
nextCandidateRef :: IORef PortNumber Source #
Next candidate port. Seeded from the pid so concurrent test processes on one machine start in different parts of the band.
ephemeralRangeCheck :: () Source #
Warn (once) when the kernel's ephemeral range overlaps the band, which would void the no-collision-with-OS-assigned-ports guarantee.
allocatePort :: Maybe (PortNumber -> PortNumber) -> IO PortNumber Source #
Allocate one port from the band, optionally together with a derived
companion port. The primary (and companion, when given) are registered in
reservedPortsRef and proven bindable before being returned. Ports that
turn out to be occupied by something outside this process stay registered,
so they are never tried again.
getRandomPort :: IO PortNumber Source #
Find a free TCPv4 port for listening on localhost.
The port is unique for the lifetime of the test process, see allocatePort.
withFreePort :: (PortNumber -> IO a) -> IO a Source #
Find a free TCPv4 port and pass it to the given action.
NOTE: Should be used only for testing.
withFreePortAndDerived :: (PortNumber -> PortNumber) -> (PortNumber -> IO a) -> IO a Source #
Like withFreePort but also reserves the derived companion port,
in the same sense as randomUnusedTCPPortsWithDerived. Use this for tests
that spin up a subprocess (such as etcd) which itself binds a port
computed from the configured one.
randomUnusedTCPPorts :: Int -> IO [Int] Source #
Find the specified number of free ports, mutually unique for the lifetime of the test process.
NOTE: Should be used only for testing.
randomUnusedTCPPortsWithDerived :: (PortNumber -> PortNumber) -> Int -> IO [Int] Source #
Find count free TCPv4 ports such that for each returned port p, the
derived port derive p is also free (and reserved alongside it).
This is needed for tests that drive a subprocess which itself opens a
companion port computed from the configured one; e.g. etcd, whose client
port is listen - 2622 in this codebase.
portLocksRef :: IORef [FileLock] Source #
All port locks taken by this process, held only to keep them reachable for the process lifetime.
tryLockPort :: PortNumber -> IO Bool Source #
Take the machine-wide lock for a port, so concurrent test processes (of the same user) cannot allocate it too. The lock is an OS file lock held for the process lifetime; the kernel releases it when the process dies, so crashed runs cannot leak reservations. Returns False when someone else holds the lock. Degrades to True (no cross-process protection, the in-process registry still applies) when the lock cannot be taken for any reason other than contention.
portLockDir :: IO FilePath Source #
Fixed, per-user lock directory. Deliberately not TMPDIR, which differs between nix shells and would defeat cross-process coordination.
bindSpecificLoopback :: PortNumber -> IO Socket Source #
sockStream :: CInt Source #
sockNonBlock :: CInt Source #
sockCloexec :: CInt Source #
openStreamSocketCloexec :: IO Socket Source #
Create an AF_INET stream socket with the close-on-exec flag set
ATOMICALLY at creation. socket sets it via a separate
fcntl, and a subprocess spawned by a concurrent thread in the window
between socket() and that fcntl inherits the fd: once the parent binds the
shared file description and closes its copy, the subprocess keeps the port
bound (invisibly, as an UNCONN socket) for its whole lifetime. With ~40
etcd spawns racing hundreds of sentinel sockets per suite run, that window
was hit regularly.