Skip to content

[FPC/Windows] TCP_NODELAY missing after 3.3.4 keep-alive — ~200ms stall on reused connections #565

Description

@freitasjca

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions