diff --git a/README.md b/README.md index 3928e1d..c86a3ea 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,13 @@ A _provider_ is the HTTP transport that owns the socket and hands requests to yo | ----------------------------------------------------------------------------------------------- | ----------------------- | :------------------: | :-------------------------: | | **Indy** _(Delphi default for self-hosted)_ | _(none)_ |    ✔️ |     n/a | | **`fphttpserver`** _(FPC default for self-hosted)_ | _(none)_ |    n/a |     ✔️ | -| 🆕 **[horse-provider-crosssocket](https://github.com/freitasjca/horse-provider-crosssocket)** | `HORSE_CROSSSOCKET` |    ✔️ |     ✔️ | +| 🆕 **[horse-provider-crosssocket](https://github.com/freitasjca/horse-provider-crosssocket)** | `HORSE_PROVIDER_CROSSSOCKET` |    ✔️ |     ✔️ | | 🆕 **[horse-provider-mormot](https://github.com/freitasjca/horse-provider-mormot)** | `HORSE_PROVIDER_MORMOT` |    ✔️ |     ✔️ | | 🆕 **[horse-provider-ics](https://github.com/freitasjca/horse-provider-ics)** _(Delphi; Win + Linux/macOS)_ | `HORSE_PROVIDER_ICS` |    ✔️ |     ❌ | | 🆕 **[HTTP.sys](./doc/httpsys.md)** _(Windows kernel-mode driver for ultra-low latency)_ | `HORSE_PROVIDER_HTTPSYS` |    ✔️ |     ✔️ | | 🆕 **[epoll](./doc/epoll.md)** _(Linux-native asynchronous event loop)_ | `HORSE_PROVIDER_EPOLL` |    ✔️ |     ✔️ | -| 🆕 **[horse-provider-ics](https://github.com/freitasjca/horse-provider-ics)** _(Delphi; Win + Linux/macOS)_ | `HORSE_PROVIDER_ICS` |    ✔️ |     ❌ | | 🆕 **[IOCP](./doc/iocp.md)** _(Windows-native asynchronous I/O completion ports)_ | `HORSE_PROVIDER_IOCP` |    ✔️ |     ✔️ | +| 🆕 **[horse-provider-nghttp2](https://github.com/freitasjca/horse-provider-nghttp2)** _(HTTP/2 + gRPC; requires [Delphi-nghttp2](https://github.com/freitasjca/Delphi-nghttp2))_ | `HORSE_PROVIDER_NGHTTP2` |    ✔️ |     ✔️ | > **Note** — Apache / ISAPI / CGI / FastCGI Application types (below) do **not** use any of these Providers. The host process (Apache, IIS, the web server) owns the socket; Horse runs in-process. See [Providers & Application types](./doc/providers.md) for the full model. @@ -123,6 +123,8 @@ A _provider_ is the HTTP transport that owns the socket and hands requests to yo > **OverbyteICS installation** — the ICS Provider requires [OverbyteICS](https://wiki.overbyte.eu/wiki/index.php/ICS_Download) (v9.x). **Install ICS following the official ICS instructions** — download/clone ICS and add its `Source/` folder to your project search path (ICS is not Boss-installable). For TLS, the OpenSSL libraries ship with ICS (DLLs on Windows, `.so` on Linux). The ICS Provider is **Delphi only — Windows and POSIX (Linux64 / macOS)** via ICS's own `Ics.Posix.*` message pump (on Linux use `HORSE_APPTYPE_DAEMON` + `THorseICSLinuxDaemonApp.Run`); a **Lazarus/FPC** port is not viable — ICS's POSIX layer rides the Delphi POSIX RTL and ICS compiles out OpenSSL under FPC. Its distinctive value is ICS's OpenSSL 3.x / 4.x stack (TLS 1.3, SNI, mTLS). See [horse-provider-ics](https://github.com/freitasjca/horse-provider-ics) for setup, the A–K test suite, and known limitations. +> **Delphi-nghttp2 installation** — `boss install github.com/freitasjca/horse-provider-nghttp2` pulls the provider and its [`Delphi-nghttp2`](https://github.com/freitasjca/Delphi-nghttp2) dependency automatically. At runtime, **libnghttp2 ≥ 1.59** must be present (dynamic-loaded — no link-time dependency): on Windows, download the prebuilt DLL from the [curl for Windows bundle](https://curl.se/windows/); on Linux, `sudo apt install libnghttp2-14`; on macOS, `brew install nghttp2`. For TLS and gRPC, OpenSSL 3.x or 1.1 is auto-detected. **FPC trunk 3.3.1 required** (FPC 3.2.2 is a hard blocker). See [horse-provider-nghttp2](https://github.com/freitasjca/horse-provider-nghttp2) for the full setup guide, TLS/mTLS config, and gRPC samples. + > **HttpSys** — **no install**: the `Horse.Provider.HttpSys` unit ships with Horse and binds directly to Windows' `httpapi.dll` (http.sys), so there's no external library. Set `HORSE_PROVIDER_HTTPSYS` (Windows; Delphi or Lazarus). Because http.sys is a kernel-mode, machine-wide HTTP stack, binding a non-`localhost` host or a privileged port needs a one-time URL reservation (`netsh http add urlacl url=http://+:9000/ user=Everyone`) or Administrator rights; HTTPS uses the Windows certificate store via `netsh http add sslcert`. It is mutually exclusive with the CrossSocket / mORMot / ICS Providers (one transport per build). > **IOCP** — **no install**: the `Horse.Provider.IOCP` unit ships with Horse and binds directly to Windows' input/output completion ports using Winsock2 API for extremely high performance and scalability on Windows self-hosted application types. Set `HORSE_PROVIDER_IOCP` (Windows; Delphi or Lazarus). It is mutually exclusive with Indy, HttpSys and other socket providers (one transport per build). @@ -199,6 +201,7 @@ This is a list of middlewares that are created by the Horse community, please cr | [isaquepinheiro/horse-jsonbr](https://github.com/HashLoad/JSONBr) |    ✔️ |     ❌ | | [IagooCesaar/Horse-JsonInterceptor](https://github.com/IagooCesaar/Horse-JsonInterceptor) |    ✔️ |     ❌ | | [dliocode/horse-datalogger](https://github.com/dliocode/horse-datalogger) |    ✔️ |     ❌ | +| [marcobreveglieri/horse-prometheus-metrics](https://github.com/marcobreveglieri/horse-prometheus-metrics) |    ✔️ |     ❌ | | [weslleycapelari/horse-documentation](https://github.com/weslleycapelari/horse-documentation) |    ✔️ |     ❌ | | [weslleycapelari/horse-validator](https://github.com/weslleycapelari/horse-validator) |    ✔️ |     ❌ | | [regyssilveira/horse-rate-limit](https://github.com/regyssilveira/horse-rate-limit) |    ✔️ |     ✔️ | diff --git a/README.pt-BR.md b/README.pt-BR.md index 4fa8986..dbc0c6f 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -109,13 +109,13 @@ Um _provider_ é o transporte HTTP que é dono do socket e entrega requisições | ----------------------------------------------------------------------------------------------- | ----------------------- | :------------------: | :-------------------------: | | **Indy** _(padrão Delphi para self-hosted)_ | _(nenhum)_ |    ✔️ |     n/a | | **`fphttpserver`** _(padrão FPC para self-hosted)_ | _(nenhum)_ |    n/a |     ✔️ | -| 🆕 **[horse-provider-crosssocket](https://github.com/freitasjca/horse-provider-crosssocket)** | `HORSE_CROSSSOCKET` |    ✔️ |     ✔️ | +| 🆕 **[horse-provider-crosssocket](https://github.com/freitasjca/horse-provider-crosssocket)** | `HORSE_PROVIDER_CROSSSOCKET` |    ✔️ |     ✔️ | | 🆕 **[horse-provider-mormot](https://github.com/freitasjca/horse-provider-mormot)** | `HORSE_PROVIDER_MORMOT` |    ✔️ |     ✔️ | | 🆕 **[horse-provider-ics](https://github.com/freitasjca/horse-provider-ics)** _(Delphi; Win + Linux/macOS)_ | `HORSE_PROVIDER_ICS` |    ✔️ |     ❌ | | 🆕 **[HTTP.sys](./doc/httpsys.pt-BR.md)** _(driver de modo kernel do Windows para ultra-baixa latência)_ | `HORSE_PROVIDER_HTTPSYS` |    ✔️ |     ✔️ | | 🆕 **[epoll](./doc/epoll.pt-BR.md)** _(event loop assíncrono nativo do Linux)_ | `HORSE_PROVIDER_EPOLL` |    ✔️ |     ✔️ | -| 🆕 **[horse-provider-ics](https://github.com/freitasjca/horse-provider-ics)** _(Delphi; Win + Linux/macOS)_ | `HORSE_PROVIDER_ICS` |    ✔️ |     ❌ | | 🆕 **[IOCP](./doc/iocp.pt-BR.md)** _(portas de conclusão de E/S assíncronas nativas do Windows)_ | `HORSE_PROVIDER_IOCP` |    ✔️ |     ✔️ | +| 🆕 **[horse-provider-nghttp2](https://github.com/freitasjca/horse-provider-nghttp2)** _(HTTP/2 + gRPC; requer [Delphi-nghttp2](https://github.com/freitasjca/Delphi-nghttp2))_ | `HORSE_PROVIDER_NGHTTP2` |    ✔️ |     ✔️ | > **Nota** — Os tipos de aplicação Apache / ISAPI / CGI / FastCGI (abaixo) **não** usam nenhum desses Providers. O processo host (Apache, IIS, o webserver) é dono do socket; o Horse roda in-process. Veja [Providers e Tipos de aplicação](./doc/providers.pt-BR.md) para o modelo completo. @@ -123,6 +123,8 @@ Um _provider_ é o transporte HTTP que é dono do socket e entrega requisições > **Instalação do OverbyteICS** — o Provider ICS requer o [OverbyteICS](https://wiki.overbyte.eu/wiki/index.php/ICS_Download) (v9.x). **Instale o ICS seguindo as instruções oficiais do ICS** — baixe/clone o ICS e adicione a pasta `Source/` ao _search path_ do seu projeto (o ICS não é instalável via Boss). Para TLS, as bibliotecas OpenSSL acompanham o ICS (DLLs no Windows, `.so` no Linux). O Provider ICS é **somente Delphi — Windows e POSIX (Linux64 / macOS)** via o pump de mensagens próprio do ICS (`Ics.Posix.*`) (no Linux use `HORSE_APPTYPE_DAEMON` + `THorseICSLinuxDaemonApp.Run`); um port para **Lazarus/FPC não é viável** — a camada POSIX do ICS usa a RTL POSIX do Delphi e o ICS desativa o OpenSSL no FPC. Seu diferencial é a pilha OpenSSL 3.x / 4.x do ICS (TLS 1.3, SNI, mTLS). Veja [horse-provider-ics](https://github.com/freitasjca/horse-provider-ics) para configuração, a suíte de testes A–K e limitações conhecidas. +> **Instalação do Delphi-nghttp2** — `boss install github.com/freitasjca/horse-provider-nghttp2` instala o provider e sua dependência [`Delphi-nghttp2`](https://github.com/freitasjca/Delphi-nghttp2) automaticamente. Em tempo de execução, **libnghttp2 ≥ 1.59** deve estar disponível (carregada dinamicamente — sem dependência em tempo de link): no Windows, baixe a DLL pré-compilada do [pacote curl for Windows](https://curl.se/windows/); no Linux, `sudo apt install libnghttp2-14`; no macOS, `brew install nghttp2`. Para TLS e gRPC, o OpenSSL 3.x ou 1.1 é detectado automaticamente em tempo de execução. **FPC trunk 3.3.1 obrigatório** (FPC 3.2.2 é um bloqueador definitivo). Veja [horse-provider-nghttp2](https://github.com/freitasjca/horse-provider-nghttp2) para o guia completo de configuração, TLS/mTLS e exemplos de gRPC. + > **HttpSys** — **sem instalação**: a unit `Horse.Provider.HttpSys` acompanha o Horse e usa diretamente a `httpapi.dll` do Windows (http.sys), portanto não há biblioteca externa. Defina `HORSE_PROVIDER_HTTPSYS` (Windows; Delphi ou Lazarus). Como o http.sys é uma pilha HTTP em modo kernel e de escopo da máquina, vincular um host diferente de `localhost` ou uma porta privilegiada exige uma reserva de URL única (`netsh http add urlacl url=http://+:9000/ user=Everyone`) ou direitos de Administrador; o HTTPS usa o repositório de certificados do Windows via `netsh http add sslcert`. É mutuamente exclusivo com os Providers CrossSocket / mORMot / ICS (um transporte por build). > **IOCP** — **sem instalação**: a unit `Horse.Provider.IOCP` acompanha o Horse e se vincula diretamente às portas de conclusão de E/S (Input/Output Completion Ports) do Windows utilizando a API Winsock2 para altíssimo desempenho e escalabilidade em tipos de aplicação self-hosted no Windows. Defina `HORSE_PROVIDER_IOCP` (Windows; Delphi ou Lazarus). É mutuamente exclusivo com Indy, HttpSys e outros providers de socket (um transporte por build). @@ -199,6 +201,7 @@ Esta é uma lista de middlewares criados pela comunidade Horse — abra um PR se | [isaquepinheiro/horse-jsonbr](https://github.com/HashLoad/JSONBr) |    ✔️ |     ❌ | | [IagooCesaar/Horse-JsonInterceptor](https://github.com/IagooCesaar/Horse-JsonInterceptor) |    ✔️ |     ❌ | | [dliocode/horse-datalogger](https://github.com/dliocode/horse-datalogger) |    ✔️ |     ❌ | +| [marcobreveglieri/horse-prometheus-metrics](https://github.com/marcobreveglieri/horse-prometheus-metrics) |    ✔️ |     ❌ | | [weslleycapelari/horse-documentation](https://github.com/weslleycapelari/horse-documentation) |    ✔️ |     ❌ | | [weslleycapelari/horse-validator](https://github.com/weslleycapelari/horse-validator) |    ✔️ |     ❌ | | [regyssilveira/horse-rate-limit](https://github.com/regyssilveira/horse-rate-limit) |    ✔️ |     ✔️ | diff --git a/src/Horse.Provider.FPC.HTTPApplication.pas b/src/Horse.Provider.FPC.HTTPApplication.pas index d300ce5..b05c7f3 100644 --- a/src/Horse.Provider.FPC.HTTPApplication.pas +++ b/src/Horse.Provider.FPC.HTTPApplication.pas @@ -1,6 +1,20 @@ -unit Horse.Provider.FPC.HTTPApplication; +unit Horse.Provider.FPC.HTTPApplication; { PATCH-FPCHTTP-1: ListenWithConfig override — same root cause as PATCH-CONSOLE-1. } +{ PATCH-FPCHTTP-2: TCP_NODELAY on every accepted connection via OnAllowConnect. + fphttpserver never calls fpSetSockOpt(TCP_NODELAY) on accepted sockets; without + it the classic Nagle + delayed-ACK interaction can add ~40 ms per request on + non-loopback links (server holds the last segment until Nagle flushes, client + waits for that ACK before sending the next request). + Fix: hook TSocketServer.OnAllowConnect (fires immediately after fpAccept, before + the connection thread is created) and set TCP_NODELAY on the raw descriptor. + BENCH-FPCHTTP-1 confirmed (2026-08-28): re-test with both keepalive+TCP_NODELAY + compiled correctly showed the same 44 ms stall. TCP_NODELAY had no effect — + the root cause is fphttpserver's keepalive loop poll interval (~40 ms fixed), + NOT Nagle. TCP_NODELAY is retained: harmless with KeepConnections=False and + genuinely useful on non-loopback links (VMs, containers, production hosts). + Guards: FPC >= 3.3.1 (custhttpapp needed to reach the embedded server); + UNIX (fpSetSockOpt path; non-UNIX body is a documented no-op). } {$IF DEFINED(FPC)} {$MODE DELPHI}{$H+} @@ -42,6 +56,9 @@ THorseProvider = class(THorseProviderAbstract) class procedure DoGetModule(Sender: TObject; ARequest: TRequest; var ModuleClass: TCustomHTTPModuleClass); {$IF FPC_FULLVERSION >= 30301} class procedure EnableServerKeepAlive(const AApplication: THTTPApplication); + { PATCH-FPCHTTP-2 } + class procedure EnableServerNoDelay(const AApplication: THTTPApplication); + class procedure SetNoDelayOnAccept(Sender: TObject; ASocket: Longint; var Allow: Boolean); {$ENDIF} public class property Host: string read GetHost write SetHost; @@ -71,14 +88,18 @@ implementation uses Horse.WebModule, Horse.Response - {$IF FPC_FULLVERSION >= 30301}, custhttpapp{$ENDIF}; + {$IF FPC_FULLVERSION >= 30301}, custhttpapp{$ENDIF} + {$IFDEF UNIX}, Sockets{$ENDIF}; {$IF FPC_FULLVERSION >= 30301} const - { Per-request keep-alive lifetime handed to the embedded fphttpserver so its - threaded connection loop (TFPHTTPConnectionThread) actually reuses sockets - instead of closing after one request. Reset after every request. } DEFAULT_KEEPALIVE_TIMEOUT_MS = 15000; + {$IFDEF UNIX} + { TCP_NODELAY = 1 on every POSIX platform (Linux/macOS/FreeBSD/Solaris). + Declared here to avoid pulling in platform-specific units (linux.pp / bsd.pp) + just for this one constant — the value is mandated by POSIX and never changes. } + HORSE_TCP_NODELAY = 1; + {$ENDIF} type { descendants declared in this unit so protected members are reachable @@ -120,8 +141,49 @@ class procedure THorseProvider.EnableServerKeepAlive(const AApplication: THTTPAp THorseEmbeddedServerAccess(LServer).KeepConnectionTimeout := DEFAULT_KEEPALIVE_TIMEOUT_MS; end; end; + +{ PATCH-FPCHTTP-2 — TCP_NODELAY on each accepted connection. + TSocketServer.OnAllowConnect fires immediately after fpAccept() returns the + raw descriptor, before CreateStream wraps it and before the connection thread + starts. Setting TCP_NODELAY here applies it to every accepted socket without + subclassing TEmbeddedHttpServer. The Allow parameter is intentionally left + unchanged (default True) — this hook is used only for the setsockopt call. } +{$IFDEF UNIX} +class procedure THorseProvider.SetNoDelayOnAccept(Sender: TObject; ASocket: Longint; var Allow: Boolean); +var + LNoDelay: LongInt; +begin + LNoDelay := 1; + fpSetSockOpt(ASocket, IPPROTO_TCP, HORSE_TCP_NODELAY, @LNoDelay, SizeOf(LNoDelay)); +end; +{$ELSE} +class procedure THorseProvider.SetNoDelayOnAccept(Sender: TObject; ASocket: Longint; var Allow: Boolean); +begin + { TCP_NODELAY via fpSetSockOpt is a POSIX path; Windows FPC would need + WinSock2.setsockopt. The Nagle stall was only measured on Linux loopback, + so this non-UNIX branch is a documented no-op for now. } +end; {$ENDIF} +class procedure THorseProvider.EnableServerNoDelay(const AApplication: THTTPApplication); +var + LHandler: TFPHTTPServerHandler; + LServer: TEmbeddedHttpServer; +begin + LHandler := AApplication.HTTPHandler; + if LHandler = nil then + Exit; + LServer := THorseHTTPServerHandlerAccess(LHandler).HTTPServer; + { OnAllowConnect is PROTECTED on TFPCustomHttpServer (forwarded from the + inner TInetServer but not re-published as public by TEmbeddedHttpServer). + Use the friend class, the same pattern as KeepConnections / KeepConnectionTimeout. + TSocketServer.Accept calls it immediately after fpAccept and before + TSocketStream is created — the raw descriptor is valid for setsockopt. } + if LServer <> nil then + THorseEmbeddedServerAccess(LServer).OnAllowConnect := THorseProvider.SetNoDelayOnAccept; +end; +{$ENDIF} // FPC_FULLVERSION >= 30301 + class function THorseProvider.GetDefaultHTTPApplication: THTTPApplication; begin if HTTPApplicationIsNil then @@ -186,18 +248,30 @@ class procedure THorseProvider.InternalListen; LHTTPApplication.Address := FHost; LHTTPApplication.Initialize; {$IF FPC_FULLVERSION >= 30301} - { FPC-KEEPALIVE-1 — TFPCustomHttpServer.KeepConnections defaults to False, - so the server closes the TCP connection after EVERY response (verified on - the wire: HTTP/1.1, no Connection header, immediate close). Every - keep-alive client then reconnects per request; curl/WinHTTP hide it by - reconnecting silently, pooling clients (TCrossHttpClient) surface it as - stale-connection races. Enabling it restores normal HTTP/1.1 semantics. - Neither THTTPApplication nor TFPHTTPServerHandler forwards KeepConnections, - but the embedded TFPCustomHttpServer exists from handler construction, so - it can be set directly on the live instance — done here after Initialize - and before Run, while the server is fully configured but not yet - accepting. } - EnableServerKeepAlive(LHTTPApplication); + { FPC-KEEPALIVE-1 / BENCH-FPCHTTP-1 — keepalive REMOVED (confirmed root cause). + Sequence of events: + 1. FPC-KEEPALIVE-1 (earlier): KeepConnections=True + KeepConnectionTimeout=15000 + added to prevent stale-connection races with persistent clients. + 2. First P1 bench (2026-08-28): 44 ms stall appeared; TCP_NODELAY was also + added (PATCH-FPCHTTP-2) but the build used a STALE PPU — TCP_NODELAY never + compiled in. Removed keepalive, dropped to 0.501 ms. + 3. Second P1 bench (2026-08-28): re-enabled keepalive with TCP_NODELAY compiled + correctly via the friend-class fix. Stall STILL 44 ms — Nagle ruled out. + Root cause: fphttpserver's TFPHTTPConnectionThread keepalive loop calls + select(fd, ~40 ms) between requests to poll for graceful-shutdown signals. + Every response cycle waits one full poll interval even when the next request + is already queued. This is a fixed constant in the fphttpserver source; + without patching fphttpserver itself it cannot be reduced. + Consequence: KeepConnections=False (the default). The server closes the TCP + connection after each response without advertising Connection: close. + Persistent-connection clients that reuse the socket receive ECONNRESET; + TCrossHttpClient handles this via PATCH-CSHTTP-3 (one retry on stale reuse). + Other clients (h2load, curl) reconnect transparently. Per-request TCP + connect + OS thread creation cost: ~0.5 ms on loopback. This is the correct + trade-off — a consistent 0.5 ms is far better than a consistent 44 ms. } + { PATCH-FPCHTTP-2 — TCP_NODELAY on every accepted socket (Nagle ruled out as the + 44 ms stall cause; retained for non-loopback deployments). } + EnableServerNoDelay(LHTTPApplication); {$ENDIF} FRunning := True; DoOnListen; diff --git a/src/Horse.pas b/src/Horse.pas index 1814648..af84d38 100644 --- a/src/Horse.pas +++ b/src/Horse.pas @@ -113,6 +113,7 @@ {$IFDEF HORSE_ISAPI} {$DEFINE HORSE_HOST_ISAPI} {$ENDIF} {$IFDEF HORSE_CGI} {$DEFINE HORSE_HOST_CGI} {$ENDIF} {$IFDEF HORSE_FCGI} {$DEFINE HORSE_HOST_FCGI} {$ENDIF} +{$IFDEF HORSE_NGHTTP2} {$DEFINE HORSE_PROVIDER_NGHTTP2} {$ENDIF} { =========================================================================== PATCH-HORSE-1 — Architecturally-impossible combination guard (expanded) @@ -183,6 +184,21 @@ {$IFEND} {$IFEND} +{$IF DEFINED(HORSE_PROVIDER_NGHTTP2)} + {$IF DEFINED(HORSE_HOST_ISAPI)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 cannot combine with HORSE_HOST_ISAPI — IIS owns the socket; a self-hosted Provider cannot coexist.'} + {$ENDIF} + {$IF DEFINED(HORSE_HOST_APACHE)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 cannot combine with HORSE_HOST_APACHE — Apache owns the socket; a self-hosted Provider cannot coexist.'} + {$ENDIF} + {$IF DEFINED(HORSE_HOST_CGI)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 cannot combine with HORSE_HOST_CGI — the web server owns the socket; a self-hosted Provider cannot coexist.'} + {$ENDIF} + {$IF DEFINED(HORSE_HOST_FCGI)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 cannot combine with HORSE_HOST_FCGI — FastCGI talks to a web server; a self-hosted Provider cannot coexist.'} + {$ENDIF} +{$IFEND} + { Rule 2 — cross-platform Application-type mismatch } {$IF DEFINED(HORSE_APPTYPE_VCL) and DEFINED(FPC)} {$MESSAGE FATAL 'HORSE_APPTYPE_VCL is Delphi-only — use HORSE_APPTYPE_LCL for Lazarus/FPC.'} @@ -196,7 +212,7 @@ { Rule 3 — HORSE_NOPROVIDER × anything else } {$IF DEFINED(HORSE_NOPROVIDER)} - {$IF DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT) or DEFINED(HORSE_PROVIDER_ICS) or DEFINED(HORSE_APPTYPE_VCL) or DEFINED(HORSE_APPTYPE_DAEMON) or DEFINED(HORSE_APPTYPE_LCL) or DEFINED(HORSE_HOST_APACHE) or DEFINED(HORSE_HOST_ISAPI) or DEFINED(HORSE_HOST_CGI) or DEFINED(HORSE_HOST_FCGI)} + {$IF DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT) or DEFINED(HORSE_PROVIDER_ICS) or DEFINED(HORSE_PROVIDER_NGHTTP2) or DEFINED(HORSE_APPTYPE_VCL) or DEFINED(HORSE_APPTYPE_DAEMON) or DEFINED(HORSE_APPTYPE_LCL) or DEFINED(HORSE_HOST_APACHE) or DEFINED(HORSE_HOST_ISAPI) or DEFINED(HORSE_HOST_CGI) or DEFINED(HORSE_HOST_FCGI)} {$MESSAGE FATAL 'HORSE_NOPROVIDER is mutually exclusive with all HORSE_PROVIDER_*, HORSE_APPTYPE_*, and HORSE_HOST_* defines — remove one.'} {$IFEND} {$IFEND} @@ -211,10 +227,19 @@ {$IF DEFINED(HORSE_PROVIDER_MORMOT) and DEFINED(HORSE_PROVIDER_ICS)} {$MESSAGE FATAL 'HORSE_PROVIDER_MORMOT and HORSE_PROVIDER_ICS are mutually exclusive — pick exactly one transport Provider per build.'} {$IFEND} -{$IF DEFINED(HORSE_PROVIDER_HTTPSYS) and (DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT))} +{$IF DEFINED(HORSE_PROVIDER_NGHTTP2) and DEFINED(HORSE_PROVIDER_CROSSSOCKET)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 and HORSE_PROVIDER_CROSSSOCKET are mutually exclusive — pick exactly one transport Provider per build.'} +{$IFEND} +{$IF DEFINED(HORSE_PROVIDER_NGHTTP2) and DEFINED(HORSE_PROVIDER_MORMOT)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 and HORSE_PROVIDER_MORMOT are mutually exclusive — pick exactly one transport Provider per build.'} +{$IFEND} +{$IF DEFINED(HORSE_PROVIDER_NGHTTP2) and DEFINED(HORSE_PROVIDER_ICS)} + {$MESSAGE FATAL 'HORSE_PROVIDER_NGHTTP2 and HORSE_PROVIDER_ICS are mutually exclusive — pick exactly one transport Provider per build.'} +{$IFEND} +{$IF DEFINED(HORSE_PROVIDER_HTTPSYS) and (DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT) or DEFINED(HORSE_PROVIDER_NGHTTP2))} {$MESSAGE FATAL 'HORSE_PROVIDER_HTTPSYS is mutually exclusive with other transport Providers — pick exactly one per build.'} {$IFEND} -{$IF DEFINED(HORSE_PROVIDER_EPOLL) and (DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT) or DEFINED(HORSE_PROVIDER_HTTPSYS))} +{$IF DEFINED(HORSE_PROVIDER_EPOLL) and (DEFINED(HORSE_PROVIDER_CROSSSOCKET) or DEFINED(HORSE_PROVIDER_MORMOT) or DEFINED(HORSE_PROVIDER_HTTPSYS) or DEFINED(HORSE_PROVIDER_NGHTTP2))} {$MESSAGE FATAL 'HORSE_PROVIDER_EPOLL is mutually exclusive with other transport Providers — pick exactly one per build.'} {$IFEND} { =========================================================================== } @@ -266,6 +291,13 @@ interface {$ELSE} {$MESSAGE ERROR 'HORSE_PROVIDER_IOCP is only supported on Windows.'} {$ENDIF} + {$ELSEIF DEFINED(HORSE_PROVIDER_NGHTTP2)} + { FPC branch of the nghttp2 selector. Cross-product FPC units + (Nghttp2.FPC.Daemon, Nghttp2.FPC.LCL, Nghttp2.FPC.HTTPApplication) + aren't shipped in v2.0 — HORSE_APPTYPE_* on FPC currently falls through + to the plain console-shape provider. Add them in a follow-up when + FPC/Lazarus deployments materialise. } + Horse.Provider.Nghttp2, {$ELSEIF DEFINED(HORSE_APPTYPE_DAEMON)} Horse.Provider.FPC.Daemon, {$ELSEIF DEFINED(HORSE_APPTYPE_LCL)} @@ -334,6 +366,15 @@ interface {$ELSE} Horse.Provider.ICS, { Console-shape — Delphi default for ICS } {$ENDIF} +{$ELSEIF DEFINED(HORSE_PROVIDER_NGHTTP2)} + System.SysUtils, + {$IF DEFINED(HORSE_APPTYPE_VCL)} + Horse.Provider.Nghttp2.VCL, { Windows GUI host — auto-Listen from FormCreate } + {$ELSEIF DEFINED(HORSE_APPTYPE_DAEMON)} + Horse.Provider.Nghttp2.Daemon, { Windows Service (TService) or Linux daemon (SIGTERM handler) } + {$ELSE} + Horse.Provider.Nghttp2, { Console-shape — Delphi default for nghttp2 } + {$ENDIF} {$ELSE} System.SysUtils, Horse.Provider.Console, @@ -481,6 +522,8 @@ interface {$ELSE} THorseProvider = Horse.Provider.ICS.THorseProviderICS; {$ENDIF} +{$ELSEIF DEFINED(HORSE_PROVIDER_NGHTTP2)} + THorseProvider = Horse.Provider.Nghttp2.THorseProviderNghttp2; {$ELSEIF DEFINED(HORSE_APPTYPE_DAEMON)} THorseProvider = {$IF DEFINED(FPC)} diff --git a/tests/src/FPCHttpKeepaliveTest.dpr b/tests/src/FPCHttpKeepaliveTest.dpr new file mode 100644 index 0000000..2ae86a5 --- /dev/null +++ b/tests/src/FPCHttpKeepaliveTest.dpr @@ -0,0 +1,141 @@ +program FPCHttpKeepaliveTest; + +{ Regression test for BENCH-FPCHTTP-1. + Root cause: KeepConnections=True enabled fphttpserver's keepalive loop, which + polls with a fixed ~40 ms select() interval between requests. Every response + cycle waited one full interval even when the next request was already queued. + Fix (FPC-KEEPALIVE-1 removed from Horse.Provider.FPC.HTTPApplication): revert + to KeepConnections=False so the server closes the TCP connection after each + response and exits the per-request thread immediately. + + What this test checks: + 1. All REQUEST_COUNT sequential GET requests complete in <= STALL_THRESHOLD_MS. + The stall is exactly ~40 ms on every affected kernel, so 35 ms gives clear + headroom above normal scheduling jitter (<5 ms on a quiet loopback) while + comfortably below the stall value. + 2. All responses carry HTTP 200 and the body 'pong'. + 3. (Implicit) No deadlock or hang — the test exits within TIMEOUT_S seconds. + + Build (from horse/tests/src/): + fpc -Mdelphi -Sh \ + -Fu"../../src:modules/horse/src" \ + FPCHttpKeepaliveTest.dpr + + No HORSE_* define is needed: on FPC with no provider define, Horse.pas + selects Horse.Provider.FPC.HTTPApplication automatically (line 306 / 541 + in the patched Horse.pas). Indy never enters the picture on FPC. +} + +{$MODE DELPHI}{$H+} + +uses + {$IFDEF UNIX}cthreads,{$ENDIF} + SysUtils, Classes, fphttpclient, + Horse, Horse.Commons; + +const + TEST_PORT = 9901; + REQUEST_COUNT = 30; + STALL_THRESHOLD_MS = 35; { any single request above this = stall detected } + SERVER_STARTUP_MS = 1500; { allow fphttpserver to finish binding } + +procedure PingHandler(Req: THorseRequest; Res: THorseResponse; Next: TNextProc); +begin + Res.Send('pong'); +end; + +type + TServerThread = class(TThread) + protected + procedure Execute; override; + end; + +procedure TServerThread.Execute; +begin + FreeOnTerminate := True; + try + THorse.Listen(TEST_PORT, '127.0.0.1'); + except + on E: Exception do + Writeln('Server: ', E.Message); + end; +end; + +var + LClient: TFPHTTPClient; + LStart: QWord; + LElapsed, LMax, LTotal: QWord; + LBody: string; + LStatusCode: Integer; + LFailed: Boolean; + I: Integer; +begin + Writeln('BENCH-FPCHTTP-1 regression test'); + Writeln(' requests : ', REQUEST_COUNT); + Writeln(' stall threshold: ', STALL_THRESHOLD_MS, ' ms'); + + THorse.Get('/ping', PingHandler); + + TServerThread.Create(False); + Sleep(SERVER_STARTUP_MS); + + LClient := TFPHTTPClient.Create(nil); + LFailed := False; + LMax := 0; + LTotal := 0; + try + for I := 1 to REQUEST_COUNT do + begin + LStart := GetTickCount64; + try + LBody := LClient.Get(Format('http://127.0.0.1:%d/ping', [TEST_PORT])); + LStatusCode := LClient.ResponseStatusCode; + except + on E: Exception do + begin + Writeln('FAIL: request ', I, ' raised ', E.ClassName, ': ', E.Message); + LFailed := True; + Continue; + end; + end; + + LElapsed := GetTickCount64 - LStart; + LTotal := LTotal + LElapsed; + if LElapsed > LMax then + LMax := LElapsed; + + if LStatusCode <> 200 then + begin + Writeln(Format('FAIL: request %2d status=%d body="%s"', [I, LStatusCode, LBody])); + LFailed := True; + end + else if LBody <> 'pong' then + begin + Writeln(Format('FAIL: request %2d body="%s" (expected "pong")', [I, LBody])); + LFailed := True; + end + else if LElapsed > STALL_THRESHOLD_MS then + begin + Writeln(Format('FAIL: request %2d %3d ms > %d ms -- BENCH-FPCHTTP-1 stall detected', + [I, LElapsed, STALL_THRESHOLD_MS])); + LFailed := True; + end; + end; + finally + LClient.Free; + end; + + Writeln(Format(' max latency : %d ms', [LMax])); + Writeln(Format(' avg latency : %d ms', [LTotal div REQUEST_COUNT])); + + if LFailed then + begin + Writeln('RESULT: FAIL'); + ExitCode := 1; + end + else + begin + Writeln(Format('RESULT: OK (all %d requests <= %d ms)', [REQUEST_COUNT, LMax])); + ExitCode := 0; + end; +end.