Issue: Windows FPC keep-alive regression in 3.3.4 — TCP_NODELAY never applied
Related to #563 (introduced the regression) and #564 (Linux regression tests that validate the fix works there but do not cover Windows).
Summary
Horse 3.3.4 enables keep-alive on Windows FPC (via EnableServerKeepAlive, which has no
platform guard), but THorseNoDelaySocketHandler and the body of ConfigureServerTransport
are both inside {$IFDEF UNIX}. So Windows FPC gets keep-alive on and TCP_NODELAY never
set — the exact Nagle + delayed-ACK combination that produces the stall.
On Windows the delayed-ACK default is 200 ms (documented), versus Linux's ~40 ms that
PR #563 closed.
What 3.3.4 ships
In Horse.Provider.FPC.HTTPApplication.pas:
{$IF FPC_FULLVERSION >= 30301} // no UNIX guard here
class procedure EnableServerKeepAlive(...);
// sets KeepConnections := True, KeepConnectionTimeout := 15000
// runs on Windows FPC 3.3.1+ ✓
class procedure ConfigureServerTransport(...);
{$IFDEF UNIX} // entire body is Unix-only
// wires THorseNoDelaySocketHandler via OnGetSocketHandler
{$ENDIF}
// NOP on Windows ✗
{$IFDEF UNIX}
THorseNoDelaySocketHandler = class(TSocketHandler)
function Accept: Boolean; override;
// fpSetSockOpt(IPPROTO_TCP, TCP_NODELAY) ← Unix only
end;
{$ENDIF}
Result on Windows FPC 3.3.1+:
- Keep-alive: on (connections are reused)
- TCP_NODELAY: never set
- Effect: fphttpserver writes headers and body as two separate
send() calls;
Nagle holds the second until the peer ACKs the first; the peer, with nothing
to send, waits out its 200 ms delayed-ACK timer.
Before 3.3.4 this could not happen because keep-alive was off by default — the first
request on each connection was always on a fresh socket, and Linux quickack/Windows
fast-retransmit made that fast. 3.3.4 enables keep-alive universally, making every
second-and-later request on a reused connection on Windows FPC hit the 200 ms stall.
Root cause (wire-level evidence)
The identical mechanism on Linux was established by wire capture (tcpdump) before
PR #563 and confirmed by timing: after the stall the body segment left
11–14 µs after the ACK arrived, three times, while the ACK delay itself varied
(42.5 / 43.8 / 43.9 ms) — the ACK is what released it. The fix (TCP_NODELAY)
reduced p50 from 43.98 ms → 0.07 ms on Linux. On Windows, with the 200 ms default,
the stall is larger and the CI job (fpc-trunk-keepalive) is Linux-only, so it
cannot catch this.
The wire-level evidence was the basis for PR #563 and is summarised in that PR's discussion.
PR #564 provides FPCHttpKeepaliveTest.dpr, which validates the fix on Linux/FPC trunk:
30 requests reuse one connection with max 1 ms latency. That test passes cleanly on Linux
but does not run on Windows (the CI job is Ubuntu-only), so the gap here is invisible to it.
Fix
Extend THorseNoDelaySocketHandler.Accept to also cover Windows, using
WinSock2.setsockopt:
function THorseNoDelaySocketHandler.Accept: Boolean;
var
LEnabled: LongInt;
begin
Result := inherited Accept;
if not Result then
Exit;
LEnabled := 1;
{$IFDEF UNIX}
fpSetSockOpt(Socket.Handle, IPPROTO_TCP, TCP_NODELAY, @LEnabled, SizeOf(LEnabled));
{$ELSE} { Windows }
WinSock2.setsockopt(TSocket(Socket.Handle), IPPROTO_TCP, TCP_NODELAY,
PAnsiChar(@LEnabled), SizeOf(LEnabled));
{$ENDIF}
end;
ConfigureServerTransport also needs its {$IFDEF UNIX} lifted (or a parallel
Windows branch added) so the factory is wired on Windows too — otherwise
THorseNoDelaySocketHandler is never instantiated there.
The uses clause needs {$IF DEFINED(MSWINDOWS)}, WinSock2{$ENDIF} added, since
Sockets (the current UNIX-side import) is a POSIX unit.
Constants IPPROTO_TCP and TCP_NODELAY are both 6 and 1 on POSIX and in
WinSock2, so they can be shared as inline constants (same values, different origins).
Affected versions
Notes
Issue: Windows FPC keep-alive regression in 3.3.4 — TCP_NODELAY never applied
Related to #563 (introduced the regression) and #564 (Linux regression tests that validate the fix works there but do not cover Windows).
Summary
Horse 3.3.4 enables keep-alive on Windows FPC (via
EnableServerKeepAlive, which has noplatform guard), but
THorseNoDelaySocketHandlerand the body ofConfigureServerTransportare both inside
{$IFDEF UNIX}. So Windows FPC gets keep-alive on and TCP_NODELAY neverset — the exact Nagle + delayed-ACK combination that produces the stall.
On Windows the delayed-ACK default is 200 ms (documented), versus Linux's ~40 ms that
PR #563 closed.
What 3.3.4 ships
In
Horse.Provider.FPC.HTTPApplication.pas:Result on Windows FPC 3.3.1+:
send()calls;Nagle holds the second until the peer ACKs the first; the peer, with nothing
to send, waits out its 200 ms delayed-ACK timer.
Before 3.3.4 this could not happen because keep-alive was off by default — the first
request on each connection was always on a fresh socket, and Linux quickack/Windows
fast-retransmit made that fast. 3.3.4 enables keep-alive universally, making every
second-and-later request on a reused connection on Windows FPC hit the 200 ms stall.
Root cause (wire-level evidence)
The identical mechanism on Linux was established by wire capture (tcpdump) before
PR #563 and confirmed by timing: after the stall the body segment left
11–14 µs after the ACK arrived, three times, while the ACK delay itself varied
(42.5 / 43.8 / 43.9 ms) — the ACK is what released it. The fix (TCP_NODELAY)
reduced p50 from 43.98 ms → 0.07 ms on Linux. On Windows, with the 200 ms default,
the stall is larger and the CI job (
fpc-trunk-keepalive) is Linux-only, so itcannot catch this.
The wire-level evidence was the basis for PR #563 and is summarised in that PR's discussion.
PR #564 provides
FPCHttpKeepaliveTest.dpr, which validates the fix on Linux/FPC trunk:30 requests reuse one connection with max 1 ms latency. That test passes cleanly on Linux
but does not run on Windows (the CI job is Ubuntu-only), so the gap here is invisible to it.
Fix
Extend
THorseNoDelaySocketHandler.Acceptto also cover Windows, usingWinSock2.setsockopt:ConfigureServerTransportalso needs its{$IFDEF UNIX}lifted (or a parallelWindows branch added) so the factory is wired on Windows too — otherwise
THorseNoDelaySocketHandleris never instantiated there.The
usesclause needs{$IF DEFINED(MSWINDOWS)}, WinSock2{$ENDIF}added, sinceSockets(the current UNIX-side import) is a POSIX unit.Constants
IPPROTO_TCPandTCP_NODELAYare both 6 and 1 on POSIX and inWinSock2, so they can be shared as inline constants (same values, different origins).
Affected versions
Notes
FPCHttpKeepaliveTest.dpr(PR test(fpc): keep-alive and WebSocket EAGAIN regression tests #564) would serve as the regression test for this fixon Linux. A Windows equivalent (or a Windows runner in the CI job) would pin it there.
setsockoptbranch compiled in our working copy before Corrige latência do keep-alive no FPC e prepara a versão 3.3.4 #563 was filedbut was not separately load-tested — no Windows FPC CI in our setup either.
fpc-trunk-keepalive) is Ubuntu-only. Adding a Windows/FPC runnerwould catch this class of regression automatically going forward.