File Coverage

blib/lib/PAGI/Server.pm
Criterion Covered Total %
statement 876 1161 75.4
branch 356 638 55.8
condition 207 367 56.4
subroutine 86 108 79.6
pod 10 12 83.3
total 1535 2286 67.1


line stmt bran cond sub pod time code
1             package PAGI::Server;
2 107     107   21322048 use strict;
  107         186  
  107         3878  
3 107     107   538 use warnings;
  107         163  
  107         19974  
4              
5             our $VERSION = '0.002000';
6              
7             # Future::XS support - opt-in via PAGI_FUTURE_XS=1 environment variable
8             # Must be loaded before Future to take effect, so we check env var in BEGIN
9             # Note: We declare these without initialization so BEGIN block values persist
10             our ($FUTURE_XS_AVAILABLE, $FUTURE_XS_ENABLED);
11             BEGIN {
12 107 50   107   293 $FUTURE_XS_AVAILABLE = eval { require Future::XS; 1 } ? 1 : 0;
  107         9431  
  0         0  
13 107         230 $FUTURE_XS_ENABLED = 0; # Default to disabled
14              
15 107 50       3343 if ($ENV{PAGI_FUTURE_XS}) {
    50          
16 0 0       0 if ($FUTURE_XS_AVAILABLE) {
17             # Future::XS is already loaded from the availability check
18 0         0 $FUTURE_XS_ENABLED = 1;
19             } else {
20 0         0 die <<"END_FUTURE_XS_ERROR";
21             PAGI_FUTURE_XS=1 set but Future::XS is not installed.
22              
23             To install Future::XS:
24             cpanm Future::XS
25              
26             Or unset the PAGI_FUTURE_XS environment variable.
27             END_FUTURE_XS_ERROR
28             }
29             } elsif ($FUTURE_XS_AVAILABLE) {
30             # Available but not requested - unload it
31 0         0 delete $INC{'Future/XS.pm'};
32             }
33             }
34              
35 107     107   7719 use parent 'IO::Async::Notifier';
  107         6238  
  107         760  
36 107     107   253229 use IO::Async::Listener;
  107         622682  
  107         6015  
37 107     107   20901 use IO::Async::Stream;
  107         1138895  
  107         2852  
38 107     107   2491 use IO::Async::Loop;
  107         27124  
  107         2389  
39 107     107   35562 use IO::Async::Timer::Periodic;
  107         149105  
  107         4582  
40 107     107   35317 use IO::Socket::INET;
  107         783346  
  107         833  
41 107     107   40237 use Future;
  107         158  
  107         2058  
42 107     107   2199 use Future::AsyncAwait;
  107         12968  
  107         679  
43              
44 107     107   5609 use Scalar::Util qw(weaken refaddr);
  107         153  
  107         5525  
45 107     107   406 use Socket qw(sockaddr_family unpack_sockaddr_in unpack_sockaddr_un AF_UNIX AF_INET);
  107         178  
  107         6060  
46 107     107   514 use POSIX ();
  107         245  
  107         1860  
47              
48 107     107   83163 use PAGI::Server::Connection;
  107         394  
  107         39909  
49 107     107   56482 use PAGI::Server::Protocol::HTTP1;
  107         429  
  107         15602  
50              
51              
52             # Check TLS module availability (cached at load time for banner display)
53             our $TLS_AVAILABLE;
54             BEGIN {
55 107 50   107   313 $TLS_AVAILABLE = eval {
56 107         49047 require IO::Async::SSL;
57 107         9429338 require IO::Socket::SSL;
58 107         14249 1;
59             } ? 1 : 0;
60             }
61              
62 7     7 0 218446 sub has_tls { return $TLS_AVAILABLE }
63              
64             # Check HTTP/2 module availability (cached at load time)
65             our $HTTP2_AVAILABLE;
66             BEGIN {
67 107 50   107   274 $HTTP2_AVAILABLE = eval {
68 107         50665 require PAGI::Server::Protocol::HTTP2;
69 107         1004 PAGI::Server::Protocol::HTTP2->available;
70             } ? 1 : 0;
71             }
72              
73 14     14 0 233447 sub has_http2 { return $HTTP2_AVAILABLE }
74              
75             # Windows doesn't support Unix signals - signal handling is conditional
76 107     107   680 use constant WIN32 => $^O eq 'MSWin32';
  107         149  
  107         2151026  
77              
78             =encoding utf8
79              
80             =head1 NAME
81              
82             PAGI::Server - PAGI Reference Server Implementation
83              
84             =head1 SYNOPSIS
85              
86             use IO::Async::Loop;
87             use PAGI::Server;
88              
89             # If using Future::IO libraries (Async::Redis, SSE->every, etc.)
90             # load the IO::Async implementation BEFORE loading them:
91             use Future::IO::Impl::IOAsync;
92              
93             my $loop = IO::Async::Loop->new;
94              
95             my $server = PAGI::Server->new(
96             app => \&my_pagi_app,
97             host => '127.0.0.1',
98             port => 5000,
99             );
100              
101             $loop->add($server);
102             $server->listen->get; # Start accepting connections
103              
104             See L for details on Future::IO configuration.
105              
106             =head1 DESCRIPTION
107              
108             PAGI::Server is a reference implementation of a PAGI-compliant HTTP server.
109             It supports HTTP/1.1, WebSocket, and Server-Sent Events (SSE) as defined
110             in the PAGI specification. It prioritizes spec compliance and code clarity
111             over performance optimization. It serves as the canonical reference for how
112             PAGI servers should behave.
113              
114             =head1 PROTOCOL SUPPORT
115              
116             B
117              
118             =over 4
119              
120             =item * HTTP/1.1 (full support including chunked encoding, trailers, keepalive)
121              
122             =item * HTTP/2 (B - via nghttp2, h2 over TLS and h2c cleartext)
123              
124             =item * WebSocket (RFC 6455, including over HTTP/2 via RFC 8441)
125              
126             =item * Server-Sent Events (SSE, including over HTTP/2)
127              
128             =back
129              
130             B
131              
132             =over 4
133              
134             =item * HTTP/3 (QUIC) - Under consideration
135              
136             =back
137              
138             For HTTP/2, see L.
139              
140             =head1 UNIX DOMAIN SOCKET SUPPORT (EXPERIMENTAL)
141              
142             B The API is subject to change in future
143             releases. Please report issues at L.
144              
145             Unix domain sockets provide efficient local communication between a reverse
146             proxy (nginx, HAProxy, etc.) and PAGI::Server running on the same machine.
147             They bypass the TCP/IP stack entirely, reducing latency and overhead compared
148             to connecting over C<127.0.0.1>.
149              
150             =head2 When to Use Unix Sockets
151              
152             =over 4
153              
154             =item * B — nginx or HAProxy on the same host handles
155             TLS termination, HTTP/2 negotiation, and static files, forwarding dynamic
156             requests to PAGI over a Unix socket.
157              
158             =item * B — frameworks like TechEmpower FrameworkBenchmarks use
159             Unix sockets to eliminate network variable from application benchmarks.
160              
161             =item * B — services on the same host communicate without
162             network overhead.
163              
164             =back
165              
166             B If clients connect over the network (remote browsers,
167             API consumers), use TCP. Unix sockets only accept connections from processes
168             on the same machine.
169              
170             =head2 Basic Usage
171              
172             B
173              
174             my $server = PAGI::Server->new(
175             app => $app,
176             socket => '/tmp/pagi.sock',
177             );
178              
179             B
180              
181             pagi-server --socket /tmp/pagi.sock ./app.pl
182              
183             B
184              
185             pagi-server --socket /tmp/pagi.sock --workers 4 ./app.pl
186              
187             In multi-worker mode, the parent process creates the Unix socket and all
188             worker processes inherit the file descriptor via C. The kernel
189             distributes incoming connections across workers.
190              
191             =head2 How It Works
192              
193             =over 4
194              
195             =item 1. On startup, any existing file at the socket path is removed (stale
196             socket cleanup).
197              
198             =item 2. The server creates and binds a C Unix domain socket
199             at the specified path using C (multi-worker) or
200             C with C<< family => 'unix' >> (single-worker).
201              
202             =item 3. If C is set, C is called immediately after
203             binding to set the file permissions.
204              
205             =item 4. In multi-worker mode, the parent creates the socket before forking.
206             Workers inherit the listening fd and each runs its own C
207             wrapping the inherited handle.
208              
209             =item 5. On graceful shutdown (SIGTERM/SIGINT), the socket file is unlinked
210             by both the single-worker C path and the multi-worker
211             C<_initiate_multiworker_shutdown()> path.
212              
213             =back
214              
215             =head2 nginx Configuration
216              
217             B
218              
219             upstream pagi_backend {
220             server unix:/var/run/myapp/pagi.sock;
221             keepalive 32;
222             }
223              
224             server {
225             listen 80;
226             server_name myapp.example.com;
227              
228             location / {
229             proxy_pass http://pagi_backend;
230              
231             # Required for upstream keepalive:
232             proxy_http_version 1.1;
233             proxy_set_header Connection "";
234              
235             # Forward client info (since PAGI can't see it over Unix socket):
236             proxy_set_header Host $host;
237             proxy_set_header X-Real-IP $remote_addr;
238             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
239             proxy_set_header X-Forwarded-Proto $scheme;
240             }
241             }
242              
243             B The C directive is critical for performance. Without
244             it, nginx opens a new Unix socket connection for every request.
245             C and C are required
246             for keepalive to work.
247              
248             B
249              
250             server {
251             listen 443 ssl http2;
252             ssl_certificate /etc/ssl/myapp.crt;
253             ssl_certificate_key /etc/ssl/myapp.key;
254              
255             location / {
256             proxy_pass http://pagi_backend;
257             # ... same proxy headers as above
258             }
259             }
260              
261             nginx handles TLS and HTTP/2 with clients, then speaks plain HTTP/1.1 to
262             PAGI over the Unix socket. This is the recommended production pattern.
263              
264             =head2 Socket Permissions and Security
265              
266             By default, the socket file inherits permissions from the process umask.
267             Use C to set explicit permissions:
268              
269             # CLI
270             pagi-server --socket /var/run/myapp/pagi.sock --socket-mode 0660 ./app.pl
271              
272             # Programmatic
273             PAGI::Server->new(
274             app => $app,
275             socket => '/var/run/myapp/pagi.sock',
276             socket_mode => 0660,
277             );
278              
279             B
280              
281             =over 4
282              
283             =item * B, not C. Directories like
284             C or C prevent symlink attacks and provide
285             an additional permission layer.
286              
287             =item * B with a shared group.> Create a group (e.g., C)
288             that both the application user and the nginx user belong to:
289              
290             sudo groupadd myapp
291             sudo usermod -aG myapp www-data # nginx user
292             sudo usermod -aG myapp myappuser # app user
293             sudo mkdir -p /var/run/myapp
294             sudo chown myappuser:myapp /var/run/myapp
295             sudo chmod 0750 /var/run/myapp
296              
297             =item * B> for automatic directory management:
298              
299             # /etc/systemd/system/myapp.service
300             [Service]
301             User=myappuser
302             Group=myapp
303             RuntimeDirectory=myapp
304             RuntimeDirectoryMode=0750
305             ExecStart=/usr/local/bin/pagi-server \
306             --socket /run/myapp/pagi.sock \
307             --socket-mode 0660 \
308             --workers 4 \
309             /opt/myapp/app.pl
310              
311             systemd creates C on service start and cleans it up on stop.
312              
313             =back
314              
315             =head2 TLS Over Unix Sockets
316              
317             TLS can be used over Unix sockets, though this is unusual — normally the
318             reverse proxy handles TLS termination. When TLS is configured on a Unix
319             socket listener, the server logs an info-level note suggesting reverse proxy
320             TLS termination instead.
321              
322             The combination is allowed because it has legitimate uses (encrypted
323             inter-container communication, compliance requirements). All major ASGI
324             servers (Uvicorn, Hypercorn, Granian) also allow it.
325              
326             =head2 HTTP/2 Over Unix Sockets
327              
328             h2c (HTTP/2 cleartext) works over Unix sockets. This is useful for gRPC
329             backends or reverse proxies that support HTTP/2 to upstreams (e.g., Envoy).
330             Note that nginx does B currently support HTTP/2 to upstream backends
331             (except for gRPC via C).
332              
333             =head2 Scope Differences
334              
335             For Unix socket connections, the PAGI scope differs from TCP connections:
336              
337             =over 4
338              
339             =item * B is absent> — Unix sockets have no peer IP address or
340             port. The C key is omitted entirely from the scope hashref (not set
341             to C). This is spec-compliant: the PAGI specification marks C
342             as optional.
343              
344             =item * B is C<[$socket_path, undef]>>> — instead of C<[$host, $port]>.
345              
346             =back
347              
348             B Any middleware that accesses C<< $scope->{client} >>
349             must check C<< exists $scope->{client} >> first. For client IP identification
350             behind a reverse proxy, use C or C headers
351             instead of C<< $scope->{client} >>. The C
352             middleware (if available) handles this automatically.
353              
354             B Unix socket connections log C as the client IP in the
355             access log instead of an IP address.
356              
357             =head2 Stale Socket Cleanup
358              
359             If a socket file already exists at the configured path (e.g., from a previous
360             crash), it is automatically removed before binding. This matches the behavior
361             of Starman, Gunicorn, Uvicorn, and other production servers. The socket file
362             is also removed during graceful shutdown (SIGTERM/SIGINT).
363              
364             If the server is killed with SIGKILL (C), the socket file will
365             B be cleaned up. It will be removed on the next startup.
366              
367             =head1 MULTI-LISTENER SUPPORT (EXPERIMENTAL)
368              
369             B The API is subject to change in future
370             releases.
371              
372             A single PAGI::Server instance can listen on multiple endpoints
373             simultaneously. This is useful for:
374              
375             =over 4
376              
377             =item * B — load
378             balancers probe a TCP port while nginx uses the Unix socket.
379              
380             =item * B — serve different interfaces on different ports.
381              
382             =item * B — listen on both old and new ports during
383             a transition.
384              
385             =back
386              
387             =head2 Programmatic API
388              
389             my $server = PAGI::Server->new(
390             app => $app,
391             listen => [
392             { host => '0.0.0.0', port => 8080 },
393             { socket => '/tmp/pagi.sock', socket_mode => 0660 },
394             ],
395             );
396              
397             Each spec in the C array is a hashref with either C<< { host, port } >>
398             for TCP or C<< { socket } >> (with optional C) for Unix sockets.
399              
400             B Per-listener TLS configuration is not yet supported. TLS is configured
401             server-wide via the C constructor option and applies to all TCP listeners.
402             Unix socket listeners behind a reverse proxy do not need TLS — the proxy handles
403             TLS termination.
404              
405             =head2 CLI
406              
407             The C<--listen> flag is repeatable. The server auto-detects TCP vs Unix
408             socket: values containing C<:> are parsed as C, everything else
409             is treated as a Unix socket path.
410              
411             # TCP + Unix socket
412             pagi-server --listen 0.0.0.0:8080 --listen /tmp/pagi.sock ./app.pl
413              
414             # Multiple TCP ports
415             pagi-server --listen 0.0.0.0:8080 --listen 0.0.0.0:8443 ./app.pl
416              
417             # With workers
418             pagi-server --listen 0.0.0.0:8080 --listen /tmp/pagi.sock -w 4 ./app.pl
419              
420             # IPv6
421             pagi-server --listen [::1]:5000 ./app.pl
422              
423             C<--listen> is B with C<--host>, C<--port>, and
424             C<--socket>. C<--socket-mode> applies to all Unix socket listeners when
425             using C<--listen>.
426              
427             =head2 How It Works
428              
429             =over 4
430              
431             =item * In B, one C is created per
432             endpoint. All listeners share the same event loop and connection handler.
433              
434             =item * In B, the parent process creates all listening
435             sockets (Unix and TCP) before forking. Workers inherit all file descriptors
436             and create their own C for each inherited socket.
437              
438             =item * The C option applies only to TCP listeners. Unix socket
439             listeners always use the shared-socket model (parent creates, workers inherit).
440              
441             =item * On shutdown, all listeners are stopped and all Unix socket files are
442             cleaned up.
443              
444             =back
445              
446             =head2 Accessors
447              
448             $server->port; # Bound port of first TCP listener, or undef
449             $server->socket_path; # Path of first Unix socket listener, or undef
450             $server->listeners; # Arrayref of all listener specs
451              
452             =head2 Backward Compatibility
453              
454             The existing C/C constructor options continue to work exactly
455             as before. They are internally normalized to a single-element listener
456             array. The C option is similarly sugar for a single Unix socket
457             listener. Only C enables true multi-listener mode.
458              
459             =head1 SYSTEMD SOCKET ACTIVATION (EXPERIMENTAL)
460              
461             B The API is subject to change in future
462             releases.
463              
464             PAGI::Server supports systemd socket activation, which allows systemd to
465             create and hold listening sockets on behalf of the server. This enables
466             zero-downtime restarts: when the server is restarted, the kernel continues
467             to queue incoming connections on the socket without refusing or dropping
468             them, even during the gap between the old process exiting and the new one
469             starting.
470              
471             Benefits of systemd socket activation:
472              
473             =over 4
474              
475             =item * B — the kernel queues connections during restarts
476              
477             =item * B — systemd creates the socket as root, then drops privileges before exec
478              
479             =item * B — the server is started automatically when the first connection arrives
480              
481             =back
482              
483             =head2 Basic Setup
484              
485             Create two systemd unit files: a C<.socket> unit that describes the socket,
486             and a C<.service> unit for the server itself.
487              
488             B):>
489              
490             [Unit]
491             Description=PAGI Application Socket
492              
493             [Socket]
494             ListenStream=0.0.0.0:8080
495             Accept=no
496              
497             [Install]
498             WantedBy=sockets.target
499              
500             B):>
501              
502             [Unit]
503             Description=PAGI Application Socket
504              
505             [Socket]
506             ListenStream=/run/pagi/app.sock
507             SocketMode=0660
508             SocketUser=www-data
509             SocketGroup=www-data
510             Accept=no
511              
512             [Install]
513             WantedBy=sockets.target
514              
515             B):>
516              
517             [Unit]
518             Description=PAGI Application Server
519             Requires=pagi.socket
520              
521             [Service]
522             User=www-data
523             ExecStart=/usr/local/bin/pagi-server -E production ./app.pl
524             Restart=on-failure
525              
526             [Install]
527             WantedBy=multi-user.target
528              
529             C is B. PAGI::Server accepts connections itself via
530             C; systemd must not accept on its behalf.
531              
532             =head2 How Auto-Detection Works
533              
534             When PAGI::Server starts, it checks the C and C
535             environment variables set by systemd. If C matches the current
536             process PID, the server inspects each inherited file descriptor (starting at
537             fd 3) with C to determine its address.
538              
539             Each inherited socket is then matched against the configured listeners. For
540             example, if you configure C<< port => 8080 >> and systemd has a socket bound
541             to C<0.0.0.0:8080>, PAGI::Server will use the inherited fd instead of
542             creating a new socket.
543              
544             The same application code works identically with or without systemd:
545              
546             # Without systemd: PAGI::Server binds the socket itself
547             # With systemd: PAGI::Server inherits the socket from systemd
548             my $server = PAGI::Server->new(
549             app => $app,
550             host => '0.0.0.0',
551             port => 8080,
552             );
553              
554             After reading the inherited fds, PAGI::Server removes C,
555             C, and C from the environment (per the
556             C specification), so child processes do not re-inherit them.
557              
558             =head2 Unix Socket Cleanup
559              
560             Normally, PAGI::Server unlinks its Unix socket file on shutdown. For
561             systemd-activated Unix sockets, the socket file is B unlinked because
562             systemd owns the socket and will recreate it for the next activation.
563              
564             =head1 FD REUSE INTERNALS (EXPERIMENTAL)
565              
566             B The API is subject to change in future
567             releases.
568              
569             PAGI::Server uses a C environment variable to pass inherited
570             listening socket file descriptors to re-exec'd processes during hot restart
571             (see L). This mechanism also supports systemd
572             socket activation (see L).
573              
574             =head2 PAGI_REUSE Format
575              
576             The variable is a comma-separated list of C entries:
577              
578             # TCP listeners
579             PAGI_REUSE=127.0.0.1:8080:3,0.0.0.0:8443:4
580              
581             # Unix socket listeners
582             PAGI_REUSE=unix:/run/pagi/app.sock:5
583              
584             # Mixed
585             PAGI_REUSE=0.0.0.0:8080:3,unix:/run/pagi/app.sock:4
586              
587             Each entry encodes the address the socket is bound to and the file descriptor
588             number to use.
589              
590             =head2 Fd Matching
591              
592             When starting, C<_collect_inherited_fds()> parses C and/or
593             C, building a table of C<< address => fd >> pairs. During
594             C, each configured listener looks up its own address in the table.
595             If a match is found, the existing fd is used instead of calling C and
596             C. This allows the kernel's accept queue to be preserved across
597             restarts.
598              
599             =head2 File Descriptor Inheritance
600              
601             To ensure that listening socket fds are inherited across C,
602             PAGI::Server sets C<$^F = 1023> before creating sockets. Perl uses C<$^F>
603             (the maximum system file descriptor, equivalent to C in
604             spirit) to decide which fds receive the C close-on-exec flag:
605             fds with numbers greater than C<$^F> get C set automatically.
606             By raising C<$^F> to 1023, listen socket fds remain open across C
607             without requiring explicit C calls.
608              
609             =head1 HOT RESTART (EXPERIMENTAL)
610              
611             B The API and signal behaviour are subject
612             to change in future releases.
613              
614             Deploying new code normally requires a server restart. During that restart
615             there is a gap — however brief — where the listening socket is closed and
616             incoming connections are dropped. Hot restart eliminates this gap by having
617             the old master fork and exec a brand-new master process that B the
618             already-open listening sockets. Both the old master and the new master serve
619             requests during the transition; clients never see a refused connection.
620              
621             =head2 How It Works
622              
623             =over 4
624              
625             =item 1.
626              
627             An admin sends C to the running master: Cmaster_pidE>
628              
629             =item 2.
630              
631             The old master sets C (encoding each listening socket fd) and
632             C (its own PID) in the environment, then calls C
633             followed immediately by C of the original C command
634             (reconstructed from C).
635              
636             =item 3.
637              
638             The new master starts, finds the inherited file descriptors via C,
639             and reuses the existing listening sockets rather than calling C/C
640             again. The kernel's accept queue is preserved — no connections are dropped.
641              
642             =item 4.
643              
644             The new master spawns its worker pool and waits for each worker to complete
645             the lifespan startup handshake (heartbeat).
646              
647             =item 5.
648              
649             Once all workers are healthy, the new master sends C to the old
650             master (read from C).
651              
652             =item 6.
653              
654             The old master receives C, finishes in-flight requests within its
655             shutdown timeout, and exits cleanly.
656              
657             =back
658              
659             =head2 HUP vs USR2
660              
661             HUP — Rolling worker restart. Workers are replaced one by one.
662             Code loaded at master startup (middleware, startup modules)
663             is NOT reloaded. Use for: config changes picked up per-worker.
664              
665             USR2 — Full master re-exec. Everything reloaded from disk including
666             the perl binary, all modules, middleware stack. Use for:
667             code deploys, Perl upgrades, PAGI::Server upgrades.
668              
669             =head2 Deploy Workflow
670              
671             # Deploy new code
672             rsync -a ./lib/ /opt/myapp/lib/
673              
674             # Hot restart (zero downtime)
675             kill -USR2 $(cat /var/run/myapp/pagi.pid)
676              
677             # Verify (optional)
678             curl http://localhost:8080/health
679              
680             =head2 systemd Unit File
681              
682             [Service]
683             Type=forking
684             PIDFile=/var/run/myapp/pagi.pid
685             ExecStart=/usr/local/bin/pagi-server \
686             --host 0.0.0.0 --port 8080 \
687             --workers 4 --pid /var/run/myapp/pagi.pid \
688             --daemonize /opt/myapp/app.pl
689             ExecReload=/bin/kill -USR2 $MAINPID
690             KillMode=process
691              
692             =head2 Failure Handling
693              
694             The design ensures the old master never stops until the new master explicitly
695             sends C:
696              
697             =over 4
698              
699             =item * B — The old master logs the error and continues serving.
700             No new master is started.
701              
702             =item * B — The forked child exits before loading any code.
703             The old master notices (via C) and continues serving.
704              
705             =item * B — The old master never receives
706             C and continues serving indefinitely.
707              
708             =item * B — The new master exits without
709             sending C. The old master is unaffected.
710              
711             =back
712              
713             =head2 PERL5LIB and Module Paths
714              
715             When the new master is C'd it inherits the process environment, but
716             B any C<-I> flags that were on the original command line. If your
717             application uses C<-Ilib>, ensure C is set in the environment or
718             in the systemd unit file so the re-exec'd process can find your modules.
719             Alternatively, use the C<--lib> flag (C),
720             which is captured in C and replayed on re-exec.
721              
722             =head1 WINDOWS SUPPORT
723              
724             B
725              
726             The server relies on Unix-specific features that are not available on Windows:
727              
728             =over 4
729              
730             =item * B - SIGTERM, SIGINT, SIGHUP for graceful shutdown and worker management
731              
732             =item * B - Multi-worker mode requires real process forking, not thread emulation
733              
734             =item * B - The event loop has Unix-specific optimizations
735              
736             =back
737              
738             For Windows development, consider using WSL (Windows Subsystem for Linux) to
739             run PAGI::Server in a Linux environment. The PAGI specification and middleware
740             components can still be developed and unit-tested on Windows, but the reference
741             server implementation requires a Unix-like operating system.
742              
743              
744             =head1 CONSTRUCTOR
745              
746             =head2 new
747              
748             my $server = PAGI::Server->new(%options);
749              
750             Creates a new PAGI::Server instance. Options:
751              
752             =over 4
753              
754             =item app => \&coderef (required)
755              
756             The PAGI application coderef with signature: async sub ($scope, $receive, $send)
757              
758             =item host => $host
759              
760             Bind address (IP address or hostname). Default: C<'127.0.0.1'>
761              
762             The default binds only to the loopback interface, accepting connections only
763             from localhost. This is B - development
764             servers won't accidentally be exposed to the network.
765              
766             B
767              
768             '127.0.0.1' - Localhost only (default, secure for development)
769             '0.0.0.0' - All IPv4 interfaces (required for remote access)
770             '::' - All IPv6 interfaces (may also accept IPv4)
771             '192.168.1.100' - Specific interface only
772              
773             B where remote clients need
774             to connect, bind to all interfaces:
775              
776             my $server = PAGI::Server->new(
777             app => $app,
778             host => '0.0.0.0',
779             port => 8080,
780             );
781              
782             B When binding to C<0.0.0.0>, ensure appropriate firewall
783             rules are in place. For production, consider a reverse proxy (nginx, etc.)
784              
785             =item port => $port
786              
787             Bind port. Default: 5000
788              
789             =item socket => $path
790              
791             B Unix domain socket path for listening instead of TCP host:port.
792             B with C, C, and C.
793             See L for details.
794              
795             my $server = PAGI::Server->new(
796             app => $app,
797             socket => '/tmp/pagi.sock',
798             );
799              
800             =item socket_mode => $mode
801              
802             Set file permissions on the Unix domain socket after creation. The value
803             should be a numeric mode (e.g., C<0660>). If not specified, the socket
804             inherits the default permissions from the process umask. Silently ignored
805             if C is not set.
806              
807             my $server = PAGI::Server->new(
808             app => $app,
809             socket => '/tmp/pagi.sock',
810             socket_mode => 0660,
811             );
812              
813             =item listen => \@specs
814              
815             B Array of listener specifications for multi-endpoint listening.
816             Each spec is a hashref with either C<< { host, port } >> for TCP or
817             C<< { socket, socket_mode } >> for Unix domain sockets.
818             B with C, C, C, and C.
819             See L for details.
820              
821             my $server = PAGI::Server->new(
822             app => $app,
823             listen => [
824             { host => '0.0.0.0', port => 8080 },
825             { socket => '/tmp/pagi.sock', socket_mode => 0660 },
826             ],
827             );
828              
829             =item ssl => \%config
830              
831             Optional TLS/HTTPS configuration. B - see
832             L below.
833              
834             Configuration keys:
835              
836             =over 4
837              
838             =item cert_file => $path
839              
840             Path to the SSL certificate file (PEM format).
841              
842             =item key_file => $path
843              
844             Path to the SSL private key file (PEM format).
845              
846             =item ca_file => $path
847              
848             Optional path to CA certificate for client verification.
849              
850             =item verify_client => $bool
851              
852             If true, require and verify client certificates.
853              
854             =item min_version => $version
855              
856             Minimum TLS version -- a B, not a pin: the server negotiates the highest
857             version the client also supports at or above this. Default: C<'TLSv1_2'> (so
858             TLS 1.3 is used with modern clients). Options: C<'TLSv1_2'>, C<'TLSv1_3'>.
859              
860             =item cipher_list => $string
861              
862             OpenSSL cipher list. Default uses modern secure ciphers.
863              
864             =back
865              
866             Example:
867              
868             my $server = PAGI::Server->new(
869             app => $app,
870             ssl => {
871             cert_file => '/path/to/server.crt',
872             key_file => '/path/to/server.key',
873             },
874             );
875              
876             =item disable_tls => $bool
877              
878             Force-disable TLS even if ssl config is provided. Useful for testing
879             TLS configuration parsing without actually enabling TLS. Default: false.
880              
881             =item extensions => \%extensions
882              
883             Extensions to advertise (e.g., { fullflush => {} })
884              
885             =item access_log => $filehandle | undef
886              
887             Access log filehandle. Default: STDERR
888              
889             Set to C to disable access logging entirely. This eliminates
890             per-request I/O overhead, improving throughput by 5-15% depending on
891             workload. Useful for benchmarking or when access logs are handled
892             externally (e.g., by a reverse proxy).
893              
894             # Disable access logging
895             my $server = PAGI::Server->new(
896             app => $app,
897             access_log => undef,
898             );
899              
900             =item access_log_format => $format_or_preset
901              
902             Access log format string or preset name. Default: C<'clf'>
903              
904             Named presets:
905              
906             clf - PAGI default: IP, timestamp, method/path, status, duration
907             combined - Apache combined: adds Referer and User-Agent
908             common - Apache common: adds response size
909             tiny - Minimal: method, path, status, duration
910              
911             Custom format strings use Apache-style atoms. See L.
912              
913             my $server = PAGI::Server->new(
914             app => $app,
915             access_log_format => 'combined',
916             );
917              
918             =item log_level => $level
919              
920             Controls the verbosity of server log messages. Default: 'info'
921              
922             Valid levels (from least to most verbose):
923              
924             =over 4
925              
926             =item * B - Only errors (application errors, fatal conditions)
927              
928             =item * B - Warnings and errors (connection issues, timeouts)
929              
930             =item * B - Informational messages and above (startup, shutdown, worker spawning)
931              
932             =item * B - Everything (verbose diagnostics, frame-level details)
933              
934             =back
935              
936             my $server = PAGI::Server->new(
937             app => $app,
938             log_level => 'debug', # Very verbose
939             );
940              
941             B C<--log-level debug>
942              
943             =item quiet => $bool
944              
945             Suppress all log output except errors. Default: 0. When true, C<_log>
946             messages below the C level are dropped regardless of C;
947             error-level messages are always emitted. Worker processes run quiet
948             internally so only the master logs lifecycle events.
949              
950             B C<-q>, C<--quiet>
951              
952             =item workers => $count
953              
954             Number of worker processes for multi-worker mode. Default: 0 (single process mode).
955              
956             When set to a value greater than 0, the server uses a pre-fork model: it spawns
957             C<$count> independent worker processes, each with its own event loop, and each
958             running the C startup/shutdown cycle independently.
959              
960             B Each worker is a separate process. Anything your
961             application keeps in memory -- a cache, a counter, a registry, an open database
962             handle, a pub/sub hub stored in C<< $scope->{state} >> -- exists once B
963             worker> and does not propagate to the others. This is inherent to C, and it
964             is exactly how the wider async ecosystem behaves: gunicorn/uvicorn workers, and
965             processes on separate hosts, are isolated the same way.
966              
967             The practical consequences:
968              
969             =over 4
970              
971             =item * A B application -- or one whose shared state lives in an
972             external store such as a database or Redis -- scales across workers with no
973             special handling. This is the common case.
974              
975             =item * An application that relies on B will see it
976             diverge: each worker holds its own copy, and a value written by one worker is
977             invisible to the rest. Open per-worker resources (database connections and the
978             like) in the C startup -- which runs in each worker -- rather than
979             before calling C.
980              
981             =item * To share B across workers -- one worker publishes, handlers on
982             every worker receive -- you need an external broker, not in-memory state. See
983             L; its Redis backend is the cross-process layer, the
984             same role Django Channels' channel layer plays for ASGI.
985              
986             =back
987              
988             The same boundary applies when you scale to multiple B (for example
989             Kubernetes pods): nothing in process memory crosses it. In-memory state is
990             single-process; anything that must be shared has to live outside the process.
991              
992             =item listener_backlog => $number
993              
994             Value for the listener queue size. Default: 2048
995              
996             When in multi worker mode, the queue size for those workers inherits
997             from this value.
998              
999             =item reuseport => $bool
1000              
1001             Enable SO_REUSEPORT mode for multi-worker servers. Default: 0 (disabled).
1002              
1003             When enabled, each worker process creates its own listening socket with
1004             SO_REUSEPORT, allowing the kernel to load-balance incoming connections
1005             across workers. This can reduce accept() contention and improve p99
1006             latency under high concurrency.
1007              
1008             B Parent creates one socket before forking,
1009             all workers inherit and share that socket. Workers compete on a single
1010             accept queue (potential thundering herd).
1011              
1012             B Each worker creates its own socket with
1013             SO_REUSEPORT. The kernel distributes connections across sockets, each
1014             worker has its own accept queue (reduced contention).
1015              
1016             B
1017              
1018             =over 4
1019              
1020             =item * B: Full kernel-level load balancing. Recommended for high
1021             concurrency workloads.
1022              
1023             =item * B: SO_REUSEPORT allows multiple binds but does NOT provide
1024             kernel load balancing. May actually decrease performance compared to shared
1025             socket mode. Use with caution - benchmark before deploying.
1026              
1027             =back
1028              
1029             =item max_receive_queue => $count
1030              
1031             Maximum number of messages that can be queued in the WebSocket receive queue
1032             before the connection is closed. This is a DoS protection mechanism.
1033              
1034             B Message count (not bytes). Each WebSocket text or binary frame counts
1035             as one message regardless of size.
1036              
1037             B 1000 messages
1038              
1039             B The server sends a WebSocket close frame with code 1008
1040             (Policy Violation) and reason "Message queue overflow", then closes the
1041             connection.
1042              
1043             B
1044              
1045             =over 4
1046              
1047             =item * B Each queued message holds the full message payload.
1048             With default of 1000 messages and average 1KB messages, worst case is ~1MB
1049             per slow connection.
1050              
1051             =item * B Total memory risk = workers × max_connections × max_receive_queue × avg_message_size.
1052             For 4 workers, 100 connections each, 1000 queue, 1KB average = 400MB worst case.
1053              
1054             =item * B If your app processes messages quickly, the queue
1055             rarely grows. Default of 1000 is generous for most applications.
1056              
1057             =item * B If your app does expensive processing per message,
1058             consider lowering to 100-500 to limit memory exposure.
1059              
1060             =item * B If you have trusted clients sending rapid bursts,
1061             you may increase to 5000-10000, but monitor memory usage.
1062              
1063             =back
1064              
1065             B C<--max-receive-queue 500>
1066              
1067             =item max_ws_frame_size => $bytes
1068              
1069             Maximum size in bytes for a single WebSocket frame payload. When a client
1070             sends a frame larger than this limit, the connection is closed with a
1071             protocol error.
1072              
1073             B Bytes
1074              
1075             B 65536 (64KB) - matches Protocol::WebSocket default
1076              
1077             B The server closes the connection. The error is logged as
1078             "PAGI connection error: Payload is too big."
1079              
1080             B
1081              
1082             =over 4
1083              
1084             =item * B For chat apps or control messages, default 64KB is plenty.
1085              
1086             =item * B For binary data transfer via WebSocket, increase to 1MB-16MB
1087             depending on expected file sizes.
1088              
1089             =item * B Each connection can buffer up to max_ws_frame_size bytes
1090             during frame parsing. High values increase memory per connection.
1091              
1092             =item * B Lower values limit memory exhaustion from malicious clients
1093             sending oversized frames.
1094              
1095             =back
1096              
1097             B C<--max-ws-frame-size 1048576>
1098              
1099             =item max_connections => $count
1100              
1101             Maximum number of concurrent connections before returning HTTP 503.
1102             B (same as Mojolicious).
1103              
1104             When at capacity, new connections receive a 503 Service Unavailable
1105             response with a Retry-After header. This prevents resource exhaustion
1106             under heavy load.
1107              
1108             B
1109              
1110             my $server = PAGI::Server->new(
1111             app => $app,
1112             max_connections => 5000, # Higher limit for production
1113             );
1114              
1115             B C<--max-connections 5000>
1116              
1117             B Use C<< $server->connection_count >> and
1118             C<< $server->effective_max_connections >> to monitor usage.
1119              
1120             B
1121              
1122             For high-traffic production deployments, you'll likely want to increase
1123             this value. The optimal setting depends on your workload, available
1124             memory, and system file descriptor limits.
1125              
1126             I
1127              
1128             # Check current limits
1129             ulimit -n # Soft limit (per-process)
1130             cat /proc/sys/fs/file-max # System-wide limit
1131              
1132             # Increase for current session
1133             ulimit -n 65536
1134              
1135             # Permanent: add to /etc/security/limits.conf
1136             * soft nofile 65536
1137             * hard nofile 65536
1138              
1139             # Or for systemd services, in your unit file:
1140             [Service]
1141             LimitNOFILE=65536
1142              
1143             I
1144              
1145             # Check current limits
1146             ulimit -n # Soft limit
1147             sysctl kern.maxfilesperproc # Per-process max
1148              
1149             # Increase for current session
1150             ulimit -n 65536
1151              
1152             # Permanent: add to /etc/launchd.conf or use launchctl
1153             sudo launchctl limit maxfiles 65536 200000
1154              
1155             I Set C to roughly 80% of your file
1156             descriptor limit to leave headroom for database connections, log files,
1157             and other resources.
1158              
1159             =item write_high_watermark => $bytes
1160              
1161             B Maximum bytes to buffer in the socket write queue
1162             before applying backpressure. When exceeded, C<< $send->() >> calls will
1163             pause until the buffer drains below C.
1164             Default: 65536 (64KB).
1165              
1166             This prevents unbounded memory growth when the server writes data faster
1167             than the client can receive it. The default matches Python's asyncio
1168             transport defaults, providing a good balance between throughput and
1169             memory efficiency.
1170              
1171             B
1172              
1173             =over 4
1174              
1175             =item * B (e.g., 256KB-1MB) for high-throughput bulk transfers
1176             where you want fewer context switches and higher throughput at the cost
1177             of more per-connection memory.
1178              
1179             =item * B (e.g., 16KB-32KB) if supporting many concurrent
1180             connections where memory efficiency is critical.
1181              
1182             =back
1183              
1184             B
1185              
1186             # High-throughput file server - larger buffers
1187             my $server = PAGI::Server->new(
1188             app => $app,
1189             write_high_watermark => 262144, # 256KB
1190             write_low_watermark => 65536, # 64KB
1191             );
1192              
1193             =item write_low_watermark => $bytes
1194              
1195             B Threshold below which sending resumes after
1196             backpressure was applied. Must be less than or equal to
1197             C. Default: 16384 (16KB, which is high/4).
1198              
1199             A larger gap between high and low watermarks reduces oscillation
1200             (frequent pause/resume cycles). A smaller gap provides more responsive
1201             backpressure but may increase context switching.
1202              
1203             B
1204              
1205             # Minimize oscillation with wider gap
1206             my $server = PAGI::Server->new(
1207             app => $app,
1208             write_high_watermark => 131072, # 128KB
1209             write_low_watermark => 16384, # 16KB (8:1 ratio)
1210             );
1211              
1212             =item max_body_size => $bytes
1213              
1214             Maximum request body size in bytes. Default: 10,000,000 (10MB).
1215             Set to 0 for unlimited (not recommended for public-facing servers).
1216              
1217             Requests with Content-Length exceeding this limit receive HTTP 413
1218             (Payload Too Large). Chunked requests are also checked as data arrives.
1219              
1220             B
1221              
1222             my $server = PAGI::Server->new(
1223             app => $app,
1224             max_body_size => 50_000_000, # 50MB for file uploads
1225             );
1226              
1227             # Unlimited (use with caution)
1228             my $server = PAGI::Server->new(
1229             app => $app,
1230             max_body_size => 0,
1231             );
1232              
1233             B C<--max-body-size 50000000>
1234              
1235             B Without a body size limit, attackers can exhaust server
1236             memory with large requests. The 10MB default balances security with common
1237             use cases (file uploads, JSON payloads). Increase for specific needs, or
1238             use 0 only behind a reverse proxy that enforces its own limit.
1239              
1240             =over 4
1241              
1242             =item * A listening socket is created before forking
1243              
1244             =item * Worker processes are spawned using C<< $loop->fork() >> which properly
1245             handles IO::Async's C<$ONE_TRUE_LOOP> singleton
1246              
1247             =item * Each worker gets a fresh event loop and runs lifespan startup independently
1248              
1249             =item * Workers that exit are automatically respawned via C<< $loop->watch_process() >>
1250              
1251             =item * Workers that fail lifespan startup (exit code 2) are B respawned, since
1252             the failure would just repeat. If B worker fails startup so that no workers
1253             remain, the master logs an error and exits non-zero, rather than holding the
1254             listening socket with nothing serving -- so a process supervisor can restart it.
1255              
1256             =item * SIGTERM/SIGINT triggers graceful shutdown of all workers
1257              
1258             =back
1259              
1260             =item max_header_size => $bytes
1261              
1262             Maximum size in bytes of the request line and of the combined header block
1263             for an HTTP/1.x request. Default: 8192 (8KB). A request line exceeding this
1264             limit receives HTTP 414 (URI Too Long); a header block exceeding it receives
1265             HTTP 431 (Request Header Fields Too Large).
1266              
1267             B C<--max-header-size 16384>
1268              
1269             =item max_header_count => $count
1270              
1271             Maximum number of header fields permitted in an HTTP/1.x request. Default:
1272             100. Requests with more headers receive HTTP 431 (Request Header Fields Too
1273             Large). This bounds memory and CPU spent parsing maliciously large header
1274             sets.
1275              
1276             B C<--max-header-count 200>
1277              
1278             =item sync_file_threshold => $bytes
1279              
1280             Threshold in bytes for synchronous file reads. Files smaller than this value
1281             are read synchronously in the event loop; larger files use async I/O via
1282             a worker pool.
1283              
1284             B 65536 (64KB)
1285              
1286             Set to 0 for fully async file reads. This is recommended for:
1287              
1288             =over 4
1289              
1290             =item * Network filesystems (NFS, SMB, cloud storage)
1291              
1292             =item * High-latency storage (spinning disks under load)
1293              
1294             =item * Docker volumes with overlay filesystem
1295              
1296             =back
1297              
1298             The default (64KB) is optimized for local SSDs where small synchronous reads
1299             are faster than the overhead of async I/O.
1300              
1301             B C<--sync-file-threshold NUM>
1302              
1303             =item max_requests => $count
1304              
1305             Maximum number of requests a worker process will handle before restarting.
1306             After serving this many requests, the worker gracefully shuts down and the
1307             parent spawns a replacement.
1308              
1309             B 0 (disabled - workers run indefinitely)
1310              
1311             B
1312              
1313             =over 4
1314              
1315             =item * Long-running deployments where gradual memory growth is a concern
1316              
1317             =item * Applications with known memory leaks that can't be easily fixed
1318              
1319             =item * Defense against slow memory growth (~6.5 bytes/request observed in PAGI)
1320              
1321             =back
1322              
1323             B Only applies in multi-worker mode (C<< workers > 0 >>). In single-worker
1324             mode, this setting is ignored.
1325              
1326             B C<--max-requests 10000>
1327              
1328             Example: With 4 workers and max_requests=10000, total capacity before any
1329             restart is 40,000 requests. Workers restart individually without downtime.
1330              
1331             =item timeout => $seconds
1332              
1333             Connection idle timeout in seconds. Closes connections that are idle between
1334             requests (applies to keep-alive connections waiting for the next request).
1335              
1336             B 60
1337              
1338             B Each connection with a non-zero timeout creates a timer
1339             that is reset on every read event. For maximum throughput in high-performance
1340             scenarios, set C 0> to disable the idle timer entirely. This
1341             eliminates timer management overhead but means idle connections will never
1342             be automatically closed.
1343              
1344             B
1345              
1346             # Disable idle timeout for maximum performance
1347             my $server = PAGI::Server->new(
1348             app => $app,
1349             timeout => 0,
1350             );
1351              
1352             # Short timeout to reclaim connections quickly
1353             my $server = PAGI::Server->new(
1354             app => $app,
1355             timeout => 30,
1356             );
1357              
1358             B C<--timeout 0> or C<--timeout 30>
1359              
1360             B This differs from C (stall timeout during active
1361             request processing). The C applies between requests; the
1362             C applies during a request. For WebSocket and SSE, use
1363             C and C respectively.
1364              
1365             =item lifespan_startup_timeout => $seconds
1366              
1367             Maximum time in seconds to wait for the lifespan application to signal startup
1368             (by sending C or C). If the
1369             lifespan handler neither signals startup nor returns within this window, the
1370             server logs an error and aborts startup rather than blocking forever.
1371              
1372             B 30. Set to 0 to disable the timeout (the server will wait
1373             indefinitely for the startup signal).
1374              
1375             =item lifespan_mode => 'auto' | 'on' | 'off'
1376              
1377             Controls how the server treats the lifespan protocol.
1378              
1379             =over 4
1380              
1381             =item * B (default) -- run the lifespan handler if the application has one.
1382             If the application declines lifespan (by raising on the lifespan scope, or by
1383             returning without signalling startup), the server continues without it. This is
1384             the interoperable default: an application that does not implement lifespan still
1385             runs.
1386              
1387             =item * B -- require lifespan. A decline at startup is B: the server
1388             logs it and refuses to start. Use this for applications whose startup must
1389             succeed (for example, one that opens a database pool in lifespan).
1390              
1391             =item * B -- skip the lifespan protocol entirely. The application is never
1392             invoked with a C scope.
1393              
1394             =back
1395              
1396             An explicit C aborts startup in all modes.
1397              
1398             =item request_timeout => $seconds
1399              
1400             Maximum time in seconds a request can stall without any I/O activity before
1401             being terminated. This is a "stall timeout" - the timer resets whenever data
1402             is read from the client or written to the client.
1403              
1404             B 0 (disabled)
1405              
1406             B Creating per-request timers adds overhead that
1407             impacts throughput on high-performance workloads. For maximum performance,
1408             this is disabled by default. Most production deployments run behind a reverse
1409             proxy (nginx, haproxy) which provides its own timeout protection.
1410              
1411             B
1412              
1413             =over 4
1414              
1415             =item * Running PAGI directly without a reverse proxy
1416              
1417             =item * Small/internal apps where simplicity matters more than max throughput
1418              
1419             =item * Untrusted clients that might send data slowly or hang
1420              
1421             =item * Defense against application bugs that cause requests to hang indefinitely
1422              
1423             =back
1424              
1425             B
1426              
1427             =over 4
1428              
1429             =item * Timer starts when request processing begins
1430              
1431             =item * Timer resets on any read activity (receiving request body)
1432              
1433             =item * Timer resets on any write activity (sending response)
1434              
1435             =item * If timer expires (no I/O for N seconds), connection is closed
1436              
1437             =item * Not used for WebSocket/SSE (they have C/C)
1438              
1439             =back
1440              
1441             B
1442              
1443             # Enable 30 second stall timeout (recommended when not behind proxy)
1444             my $server = PAGI::Server->new(
1445             app => $app,
1446             request_timeout => 30,
1447             );
1448              
1449             B C<--request-timeout 30>
1450              
1451             B This differs from C (idle connection timeout). The
1452             C applies between requests on keep-alive connections. The
1453             C applies during active request processing.
1454              
1455             =item ws_idle_timeout => $seconds
1456              
1457             Maximum time in seconds a WebSocket connection can be idle without any
1458             activity (no messages sent or received) before being closed.
1459              
1460             B 0 (disabled - WebSocket connections can be idle indefinitely)
1461              
1462             When enabled, the timer resets on:
1463              
1464             =over 4
1465              
1466             =item * Sending any WebSocket frame (accept, send, ping, close)
1467              
1468             =item * Receiving any WebSocket frame from client
1469              
1470             =back
1471              
1472             B
1473              
1474             # Close idle WebSocket connections after 5 minutes
1475             my $server = PAGI::Server->new(
1476             app => $app,
1477             ws_idle_timeout => 300,
1478             );
1479              
1480             B C<--ws-idle-timeout 300>
1481              
1482             B For more sophisticated keep-alive behavior with ping/pong, use
1483             C<< $ws->keepalive($interval, $timeout) >> for protocol-level ping/pong.
1484              
1485             =item sse_idle_timeout => $seconds
1486              
1487             Maximum time in seconds an SSE connection can be idle without any events
1488             being sent before being closed.
1489              
1490             B 0 (disabled - SSE connections can be idle indefinitely)
1491              
1492             The timer resets each time an event is sent to the client (including
1493             comments and the initial headers).
1494              
1495             B
1496              
1497             # Close idle SSE connections after 2 minutes
1498             my $server = PAGI::Server->new(
1499             app => $app,
1500             sse_idle_timeout => 120,
1501             );
1502              
1503             B C<--sse-idle-timeout 120>
1504              
1505             B For SSE connections that may be legitimately idle, use
1506             C<< $sse->keepalive($interval) >> to send periodic comment keepalives.
1507              
1508             B Over HTTP/2 this timeout applies at the connection level,
1509             not per-stream. See L
1510             for details and recommendations.
1511              
1512             =item heartbeat_timeout => $seconds
1513              
1514             Worker liveness timeout in seconds. Only active in multi-worker mode
1515             (C<< workers > 0 >>). Has no effect when running without workers
1516             (C<< workers == 0 >>) — use C for idle connection management there.
1517              
1518             Each worker sends a heartbeat to the parent process via a Unix pipe at
1519             an interval of C. The parent checks for missed
1520             heartbeats every C. If a worker has not sent a
1521             heartbeat within C seconds, the parent kills it with
1522             SIGKILL and respawns a replacement.
1523              
1524             B Event loop starvation — when the worker's event
1525             loop is completely blocked and cannot process any events. This happens
1526             with blocking syscalls (C, synchronous DNS, blocking database
1527             drivers), deadlocks, runaway CPU-bound computation, or any code that
1528             does not yield to the event loop.
1529              
1530             B Slow async operations. A request handler
1531             that does C<< await $db->query(...) >> for 5 minutes is fine — the
1532             C returns control to the event loop, so heartbeats continue
1533             normally. This value should be larger than the maximum time you expect
1534             any single operation to block the event loop without yielding.
1535              
1536             B 50 (seconds). Set to 0 to disable.
1537              
1538             B
1539              
1540             # Tighter heartbeat for latency-sensitive service
1541             my $server = PAGI::Server->new(
1542             app => $app,
1543             workers => 4,
1544             heartbeat_timeout => 20,
1545             );
1546              
1547             # Disable heartbeat monitoring
1548             my $server = PAGI::Server->new(
1549             app => $app,
1550             workers => 4,
1551             heartbeat_timeout => 0,
1552             );
1553              
1554             B C<--heartbeat-timeout 20>
1555              
1556             =item shutdown_timeout => $seconds
1557              
1558             Maximum time in seconds to wait for graceful shutdown to complete.
1559             Default: 30. On SIGTERM/SIGINT the server stops accepting new connections
1560             and waits up to this many seconds for in-flight requests to finish before
1561             forcing remaining connections closed. In multi-worker mode it also bounds
1562             how long the master waits for a worker to exit after SIGTERM before
1563             escalating to SIGKILL.
1564              
1565             B C<--shutdown-timeout 30>
1566              
1567             =item validate_events => $bool
1568              
1569             Enable runtime validation of outbound events your application sends (HTTP,
1570             WebSocket, and SSE), catching malformed event structures with a clear error
1571             instead of undefined behavior. Default: off, but B when the
1572             environment is development (C<< $ENV{PAGI_ENV} eq 'development' >>, which
1573             C sets). Pass an explicit value to override the
1574             auto-detection. Intended for development; leave off in production to avoid
1575             the per-event overhead.
1576              
1577             =item loop_type => $backend
1578              
1579             Specifies the IO::Async::Loop subclass to use when calling C.
1580             This option is ignored when embedding the server in an existing loop.
1581              
1582             B Auto-detect (IO::Async chooses the best available backend)
1583              
1584             B
1585              
1586             'EPoll' - Linux epoll (recommended for Linux)
1587             'EV' - libev-based (cross-platform, requires EV module)
1588             'Poll' - POSIX poll() (portable fallback)
1589             'Select' - select() (most portable, least scalable)
1590              
1591             B
1592              
1593             my $server = PAGI::Server->new(
1594             app => $app,
1595             loop_type => 'EPoll',
1596             );
1597             $server->run;
1598              
1599             B C<--loop EPoll> (via pagi-server)
1600              
1601             B The specified backend module must be installed. For example,
1602             C 'EPoll'> requires L.
1603              
1604             =item h2_max_concurrent_streams => $count
1605              
1606             B<(Experimental - HTTP/2 support may change in future releases.)>
1607              
1608             Maximum number of concurrent HTTP/2 streams per connection. Each stream
1609             represents an in-flight request/response exchange, bounding concurrent
1610             resource consumption.
1611              
1612             B this does I defend the HTTP/2 Rapid Reset attack
1613             (CVE-2023-44487) — reset streams never count against this limit. Rapid Reset is
1614             defended by the incoming-C rate limit; see C.
1615              
1616             B 100
1617              
1618             This matches Apache httpd, H2O, and Hypercorn defaults. The RFC 7540 default
1619             is unlimited, but 100 is the industry consensus for a safe maximum.
1620              
1621             B Increase for API gateways handling many small concurrent requests.
1622             Decrease for memory-constrained environments or when each request is expensive.
1623              
1624             =item h2_rst_rate_limit => { burst => 1000, rate => 33 }
1625              
1626             B<(Experimental - HTTP/2.)> Token-bucket rate limit on incoming C
1627             frames, defending the HTTP/2 Rapid Reset attack (CVE-2023-44487). C
1628             tokens are available immediately and refill at C per second; each
1629             C consumes one. When the bucket empties the server sends GOAWAY and
1630             closes the connection. B at nghttp2's defaults (burst 1000, rate
1631             33), which only an abusive create-and-reset flood will hit. Raise the values to
1632             loosen. Requires C >= 0.008 (nghttp2 >= 1.57).
1633              
1634             =item h2_initial_window_size => $bytes
1635              
1636             B<(Experimental)>
1637              
1638             Initial HTTP/2 flow control window size per stream, in bytes. Controls how
1639             much data a client can send before the server must acknowledge receipt. Also
1640             affects how much response data the server can buffer per stream before the
1641             client acknowledges.
1642              
1643             B 65535 (64KB minus 1, the RFC 7540 default)
1644              
1645             B Increase to 131072-262144 for high-throughput file upload/download
1646             workloads where the default window causes flow control stalls on high-latency
1647             connections. The tradeoff is higher per-stream memory usage.
1648              
1649             =item h2_max_frame_size => $bytes
1650              
1651             B<(Experimental)>
1652              
1653             Maximum size of a single HTTP/2 frame payload, in bytes. Must be between
1654             16384 (16KB, the RFC minimum) and 16777215 (16MB, the RFC maximum).
1655              
1656             B 16384 (16KB, the RFC 7540 default)
1657              
1658             Most servers use the RFC default. Larger frames reduce framing overhead but
1659             increase head-of-line blocking within a stream.
1660              
1661             =item h2_enable_push => $bool
1662              
1663             B<(Experimental)>
1664              
1665             Enable HTTP/2 server push (SETTINGS_ENABLE_PUSH). When enabled, the server
1666             can proactively push resources to the client before they are requested.
1667              
1668             B 0 (disabled)
1669              
1670             Server push is effectively deprecated. Chrome removed support in 2022,
1671             and nginx deprecated it in version 1.25.1. Unless you have a specific use
1672             case requiring server push, leave this disabled.
1673              
1674             =item h2_enable_connect_protocol => $bool
1675              
1676             B<(Experimental)>
1677              
1678             Enable the Extended CONNECT protocol (RFC 8441, SETTINGS_ENABLE_CONNECT_PROTOCOL).
1679             Required for WebSocket-over-HTTP/2 tunneling.
1680              
1681             B 1 (enabled)
1682              
1683             When enabled, clients can use the Extended CONNECT method with a C<:protocol>
1684             pseudo-header to establish WebSocket connections over HTTP/2 streams. Disable
1685             this only if you do not need WebSocket support over HTTP/2.
1686              
1687             =item h2_max_header_list_size => $bytes
1688              
1689             B<(Experimental)>
1690              
1691             Maximum total size of the header block that the server will accept, in bytes.
1692             This is the sum of all header name lengths, value lengths, and 32-byte per-entry
1693             overhead as defined by RFC 7540 Section 6.5.2.
1694              
1695             B 65536 (64KB)
1696              
1697             Matches Hypercorn and Node.js defaults. Provides a guard against header-based
1698             memory exhaustion attacks while being generous enough for normal use including
1699             large cookies and authorization tokens.
1700              
1701             =back
1702              
1703             =head1 METHODS
1704              
1705             =head2 listen
1706              
1707             my $future = $server->listen;
1708              
1709             Starts listening for connections. Returns a Future that completes when
1710             the server is ready to accept connections.
1711              
1712             =head2 shutdown
1713              
1714             my $future = $server->shutdown;
1715              
1716             Initiates graceful shutdown. Returns a Future that completes when
1717             shutdown is complete.
1718              
1719             =head2 port
1720              
1721             my $port = $server->port;
1722              
1723             Returns the bound port number. Useful when port => 0 is used.
1724              
1725             =head2 socket_path
1726              
1727             my $path = $server->socket_path;
1728              
1729             Returns the Unix socket path of the first Unix socket listener,
1730             or C if no Unix socket listeners are configured.
1731              
1732             =head2 listeners
1733              
1734             my $listeners = $server->listeners;
1735              
1736             Returns an arrayref of all normalized listener specifications.
1737             Each entry is a hashref with C (C<'tcp'> or C<'unix'>)
1738             and type-specific keys (C/C for TCP, C for Unix).
1739              
1740             =head2 is_running
1741              
1742             my $bool = $server->is_running;
1743              
1744             Returns true if the server is accepting connections.
1745              
1746             =head2 connection_count
1747              
1748             my $count = $server->connection_count;
1749              
1750             Returns the current number of active connections.
1751              
1752             =head2 effective_max_connections
1753              
1754             my $max = $server->effective_max_connections;
1755              
1756             Returns the effective maximum connections limit. If C
1757             was set explicitly, returns that value. Otherwise returns the default
1758             of 1000.
1759              
1760             =head1 FILE RESPONSE STREAMING
1761              
1762             PAGI::Server supports efficient file streaming via the C and C
1763             keys in C events:
1764              
1765             # Stream entire file
1766             await $send->({
1767             type => 'http.response.body',
1768             file => '/path/to/file.mp4',
1769             more => 0,
1770             });
1771              
1772             # Stream partial file (for Range requests)
1773             await $send->({
1774             type => 'http.response.body',
1775             file => '/path/to/file.mp4',
1776             offset => 1000,
1777             length => 5000,
1778             more => 0,
1779             });
1780              
1781             # Stream from filehandle
1782             open my $fh, '<:raw', $file;
1783             await $send->({
1784             type => 'http.response.body',
1785             fh => $fh,
1786             length => $size,
1787             more => 0,
1788             });
1789             close $fh;
1790              
1791             The server streams files in 64KB chunks to avoid memory bloat. Small files
1792             (under 64KB) are read synchronously for speed; larger files use async I/O
1793             via a worker pool to avoid blocking the event loop.
1794              
1795             =head2 Production Recommendations for Static Files
1796              
1797             B
1798             serving to a reverse proxy:>
1799              
1800             =over 4
1801              
1802             =item 1. B
1803              
1804             Place a reverse proxy in front of PAGI::Server and let it handle static
1805             files directly. This provides:
1806              
1807             =over 4
1808              
1809             =item * Optimized file serving with kernel sendfile
1810              
1811             =item * Efficient caching and compression
1812              
1813             =item * Protection from slow client attacks
1814              
1815             =item * HTTP/2 and HTTP/3 support
1816              
1817             =back
1818              
1819             =item 2. B>
1820              
1821             For files that require authentication or authorization, use the XSendfile
1822             middleware to delegate file serving to the reverse proxy:
1823              
1824             use PAGI::Middleware::Builder;
1825              
1826             my $app = builder {
1827             enable 'XSendfile',
1828             type => 'X-Accel-Redirect', # For Nginx
1829             mapping => { '/var/www/protected/' => '/internal/' };
1830             $my_app;
1831             };
1832              
1833             See L for details.
1834              
1835             =back
1836              
1837             =head1 ENABLING TLS SUPPORT
1838              
1839             PAGI::Server supports HTTPS/TLS connections, but requires additional modules
1840             that are not installed by default. This keeps the base installation minimal
1841             for users who don't need TLS.
1842              
1843             =head2 When You Need TLS
1844              
1845             You need TLS if you want to:
1846              
1847             =over 4
1848              
1849             =item * Serve HTTPS traffic directly from PAGI::Server
1850              
1851             =item * Test TLS locally during development
1852              
1853             =item * Use client certificate authentication
1854              
1855             =back
1856              
1857             You B need TLS if you:
1858              
1859             =over 4
1860              
1861             =item * Use a reverse proxy (nginx, Apache) that handles TLS termination
1862              
1863             =item * Only serve HTTP traffic on localhost for development
1864              
1865             =item * Deploy behind a load balancer that provides TLS
1866              
1867             =back
1868              
1869             B Use a reverse proxy (nginx, HAProxy, etc.) for
1870             TLS termination. They offer better performance, easier certificate management,
1871             and battle-tested security. PAGI::Server's TLS support is primarily for
1872             development and testing.
1873              
1874             =head2 Installing TLS Modules
1875              
1876             To enable TLS support, install the required modules:
1877              
1878             B
1879              
1880             cpanm IO::Async::SSL IO::Socket::SSL
1881              
1882             B
1883              
1884             apt-get install libio-socket-ssl-perl
1885              
1886             B
1887              
1888             yum install perl-IO-Socket-SSL
1889              
1890             B
1891              
1892             perl -MIO::Async::SSL -MIO::Socket::SSL -e 'print "TLS modules installed\n"'
1893              
1894             =head2 Basic TLS Configuration
1895              
1896             Once the modules are installed, configure TLS with certificate and key files:
1897              
1898             my $server = PAGI::Server->new(
1899             app => $app,
1900             host => '0.0.0.0',
1901             port => 5000,
1902             ssl => {
1903             cert_file => '/path/to/server.crt',
1904             key_file => '/path/to/server.key',
1905             },
1906             );
1907              
1908             =head2 Generating Self-Signed Certificates (Development)
1909              
1910             For local development and testing, you can generate a self-signed certificate:
1911              
1912             B
1913              
1914             openssl req -x509 -newkey rsa:4096 -nodes \
1915             -keyout server.key -out server.crt -days 365 \
1916             -subj "/CN=localhost"
1917              
1918             B
1919              
1920             # Create config file
1921             cat > ssl.conf <
1922             [req]
1923             distinguished_name = req_distinguished_name
1924             x509_extensions = v3_req
1925             prompt = no
1926              
1927             [req_distinguished_name]
1928             CN = localhost
1929              
1930             [v3_req]
1931             subjectAltName = @alt_names
1932              
1933             [alt_names]
1934             DNS.1 = localhost
1935             DNS.2 = *.localhost
1936             IP.1 = 127.0.0.1
1937             EOF
1938              
1939             # Generate certificate
1940             openssl req -x509 -newkey rsa:4096 -nodes \
1941             -keyout server.key -out server.crt -days 365 \
1942             -config ssl.conf -extensions v3_req
1943              
1944             B
1945              
1946             # Start server
1947             pagi-server --app myapp.pl --ssl-cert server.crt --ssl-key server.key
1948              
1949             # Test with curl (ignore self-signed cert warning)
1950             curl -k https://localhost:5000/
1951              
1952             B
1953              
1954             For production, use certificates from a trusted CA (Let's Encrypt, etc.):
1955              
1956             # Let's Encrypt with certbot
1957             certbot certonly --standalone -d yourdomain.com
1958              
1959             # Then configure PAGI::Server
1960             my $server = PAGI::Server->new(
1961             app => $app,
1962             ssl => {
1963             cert_file => '/etc/letsencrypt/live/yourdomain.com/fullchain.pem',
1964             key_file => '/etc/letsencrypt/live/yourdomain.com/privkey.pem',
1965             },
1966             );
1967              
1968             =head2 Advanced TLS Configuration
1969              
1970             See the C option in L for details on:
1971              
1972             =over 4
1973              
1974             =item * Client certificate verification (C, C)
1975              
1976             =item * TLS version requirements (C)
1977              
1978             =item * Custom cipher suites (C)
1979              
1980             =back
1981              
1982             =head1 ENABLING HTTP/2 SUPPORT (EXPERIMENTAL)
1983              
1984             B The API and behavior may change in future
1985             releases. Please report issues and provide feedback.
1986              
1987             PAGI::Server provides native HTTP/2 support via the nghttp2 C library
1988             (L). When enabled, the server supports both TLS-based
1989             HTTP/2 (h2 via ALPN negotiation) and cleartext HTTP/2 (h2c).
1990              
1991             =head2 Requirements
1992              
1993             L must be installed, which requires the nghttp2 C library:
1994              
1995             # Install the C library
1996             brew install nghttp2 # macOS
1997             apt-get install libnghttp2-dev # Debian/Ubuntu
1998              
1999             # Install the Perl bindings
2000             cpanm Net::HTTP2::nghttp2
2001              
2002             =head2 Enabling HTTP/2
2003              
2004             B
2005              
2006             # HTTP/2 over TLS (recommended for production)
2007             pagi-server --http2 --ssl-cert cert.pem --ssl-key key.pem --app myapp.pl
2008              
2009             # HTTP/2 cleartext (h2c, for development/testing)
2010             pagi-server --http2 --app myapp.pl
2011              
2012             B
2013              
2014             my $server = PAGI::Server->new(
2015             app => $app,
2016             http2 => 1,
2017             ssl => { cert_file => 'cert.pem', key_file => 'key.pem' },
2018             );
2019              
2020             =head2 How It Works
2021              
2022             With TLS, the server advertises C

and C via ALPN during the

2023             TLS handshake. Clients that support HTTP/2 will negotiate C

automatically;

2024             others fall back to HTTP/1.1 transparently.
2025              
2026             Without TLS, the server detects HTTP/2 via the client connection preface
2027             (h2c mode). HTTP/1.1 clients are handled normally.
2028              
2029             =head2 HTTP/2 Features
2030              
2031             =over 4
2032              
2033             =item * Stream multiplexing (100 concurrent streams per connection by default)
2034              
2035             =item * HPACK header compression
2036              
2037             =item * Per-stream and connection-level flow control
2038              
2039             =item * GOAWAY graceful session shutdown
2040              
2041             =item * Stream state validation (RST_STREAM on protocol violations)
2042              
2043             =item * WebSocket over HTTP/2 via Extended CONNECT (RFC 8441)
2044              
2045             =back
2046              
2047             =head2 Conformance
2048              
2049             Tested against h2spec (the HTTP/2 conformance test suite): B<137/146 (93.8%)>.
2050             All 9 remaining failures are shared with the bare nghttp2 C library and cannot
2051             be fixed at the application level.
2052              
2053             Load tested with h2load: 60,000 requests across 50 concurrent connections with
2054             zero failures.
2055              
2056             See L for full compliance details.
2057              
2058             =head2 Configuration
2059              
2060             HTTP/2 protocol settings are tuned via constructor options prefixed with
2061             C. See L for details on:
2062              
2063             =over 4
2064              
2065             =item * C - Max streams per connection (default: 100)
2066              
2067             =item * C - Incoming C rate limit; HTTP/2 Rapid Reset defense (default burst 1000 / rate 33)
2068              
2069             =item * C - Flow control window (default: 65535)
2070              
2071             =item * C - Max frame payload (default: 16384)
2072              
2073             =item * C - Server push (default: disabled)
2074              
2075             =item * C - WebSocket over HTTP/2 (default: enabled)
2076              
2077             =item * C - Max header block size (default: 65536)
2078              
2079             =back
2080              
2081             =head1 SIGNAL HANDLING
2082              
2083             PAGI::Server responds to Unix signals for process management. Signal behavior
2084             differs between single-worker and multi-worker modes.
2085              
2086             =head2 Supported Signals
2087              
2088             =over 4
2089              
2090             =item B - Graceful shutdown
2091              
2092             Initiates graceful shutdown. The server stops accepting new connections,
2093             waits for active requests to complete (up to C seconds),
2094             then exits. In multi-worker mode, SIGTERM is forwarded to all workers.
2095              
2096             kill -TERM
2097              
2098             =item B - Graceful shutdown (Ctrl-C)
2099              
2100             Same behavior as SIGTERM. Triggered by Ctrl-C in the terminal. In multi-worker
2101             mode, the parent process catches SIGINT and coordinates shutdown of all workers
2102             to ensure proper lifespan.shutdown handling.
2103              
2104             kill -INT
2105             # or press Ctrl-C in terminal
2106              
2107             =item B - Graceful worker restart (multi-worker only)
2108              
2109             Performs a zero-downtime worker restart by spawning new workers before
2110             terminating old ones. Useful for recycling workers to reclaim leaked memory
2111             or reset per-worker state without dropping active connections.
2112              
2113             B This does NOT reload application code. New workers fork from the
2114             existing parent process and inherit the same loaded code. For code deploys,
2115             perform a full server restart (SIGTERM + start).
2116              
2117             kill -HUP
2118              
2119             In single-worker mode, SIGHUP is logged but ignored (no graceful restart
2120             possible without multiple workers).
2121              
2122             =item B - Increase worker count (multi-worker only)
2123              
2124             Spawns an additional worker process. Use this to scale up capacity dynamically.
2125              
2126             kill -TTIN
2127              
2128             =item B - Decrease worker count (multi-worker only)
2129              
2130             Gracefully terminates one worker process. The minimum worker count is 1;
2131             sending SIGTTOU when only one worker remains has no effect.
2132              
2133             kill -TTOU
2134              
2135             =back
2136              
2137             =head2 Signal Handling in Multi-Worker Mode
2138              
2139             When running with C<< workers => N >> (where N > 1):
2140              
2141             =over 4
2142              
2143             =item * Parent process manages the worker pool
2144              
2145             =item * Workers handle requests; parent handles signals
2146              
2147             =item * SIGTERM/SIGINT to parent triggers coordinated shutdown of all workers
2148              
2149             =item * Each worker runs lifespan.shutdown before exiting
2150              
2151             =item * Workers that crash are automatically respawned
2152              
2153             =item * Heartbeat monitoring detects workers with blocked event loops and replaces them automatically (see C)
2154              
2155             =back
2156              
2157             =head2 Examples
2158              
2159             B
2160              
2161             # Deploy new code, then signal graceful restart
2162             kill -HUP $(cat /var/run/pagi.pid)
2163              
2164             B
2165              
2166             # Add workers during peak hours
2167             kill -TTIN $(cat /var/run/pagi.pid)
2168             kill -TTIN $(cat /var/run/pagi.pid)
2169              
2170             # Remove workers during quiet periods
2171             kill -TTOU $(cat /var/run/pagi.pid)
2172              
2173             B
2174              
2175             # Stop accepting new connections, drain existing ones
2176             kill -TERM $(cat /var/run/pagi.pid)
2177              
2178             =cut
2179              
2180             sub _init {
2181 382     382   62254254 my ($self, $params) = @_;
2182              
2183 382 100       3544 $self->{app} = delete $params->{app} or die "app is required";
2184              
2185             # Extract listener-related params
2186 381         1508 my $listen = delete $params->{listen};
2187 381         1361 my $socket = delete $params->{socket};
2188 381         1349 my $socket_mode = delete $params->{socket_mode};
2189 381         1252 my $host = delete $params->{host};
2190 381         2338 my $port = delete $params->{port};
2191 381         1426 $self->{ssl} = delete $params->{ssl};
2192 381   100     3638 $self->{disable_tls} = delete $params->{disable_tls} // 0; # Extract early for validation
2193              
2194             # Validate SSL certificate files at startup (fail fast)
2195             # Skip validation if TLS is explicitly disabled
2196 381 100       1749 if (my $ssl = $self->{ssl}) {
2197 57 100       229 if ($self->{disable_tls}) {
2198             # Skip TLS setup and cert validation — ssl config is stored but not applied
2199 2         49 warn "PAGI::Server: TLS disabled via disable_tls option, ssl config ignored\n";
2200             } else {
2201 55 100       322 if (my $cert = $ssl->{cert_file}) {
2202 53 100       2205 die "SSL certificate file not found: $cert\n" unless -e $cert;
2203 52 50       645 die "SSL certificate file not readable: $cert\n" unless -r $cert;
2204             }
2205 54 100       339 if (my $key = $ssl->{key_file}) {
2206 53 100       677 die "SSL key file not found: $key\n" unless -e $key;
2207 52 50       480 die "SSL key file not readable: $key\n" unless -r $key;
2208             }
2209 53 100       264 if (my $ca = $ssl->{ca_file}) {
2210 9 100       157 die "SSL CA file not found: $ca\n" unless -e $ca;
2211 8 50       92 die "SSL CA file not readable: $ca\n" unless -r $ca;
2212             }
2213             }
2214             }
2215              
2216             # Normalize all listener forms to $self->{listeners}
2217 378 100       2198 if ($listen) {
    100          
2218             # Explicit listen array
2219 26 100       272 die "Cannot specify both 'listen' and 'host' options\n" if defined $host;
2220 24 50       250 die "Cannot specify both 'listen' and 'port' options\n" if defined $port;
2221 24 50       197 die "Cannot specify both 'listen' and 'socket' options\n" if defined $socket;
2222 24 50       228 die "Cannot specify both 'listen' and 'socket_mode' options\n" if defined $socket_mode;
2223 24 100 66     1004 die "'listen' must be a non-empty arrayref\n"
2224             unless ref $listen eq 'ARRAY' && @$listen;
2225              
2226 22         285 $self->{listeners} = [];
2227 22         386 for my $spec (@$listen) {
2228 29 50       333 die "Each listen spec must be a hashref\n" unless ref $spec eq 'HASH';
2229 29 100       220 if ($spec->{socket}) {
2230 9 100       32 die "Cannot specify both 'socket' and 'host' in a listen spec\n" if $spec->{host};
2231 7 50       15 die "Cannot specify both 'socket' and 'port' in a listen spec\n" if $spec->{port};
2232 7         38 push @{$self->{listeners}}, {
2233             type => 'unix',
2234             path => $spec->{socket},
2235             socket_mode => $spec->{socket_mode},
2236 7         9 };
2237             } else {
2238             die "TCP listen spec requires both 'host' and 'port'\n"
2239 20 100 66     407 unless defined $spec->{host} && defined $spec->{port};
2240 18         495 push @{$self->{listeners}}, {
2241             type => 'tcp',
2242             host => $spec->{host},
2243             port => $spec->{port},
2244 18         107 };
2245             }
2246             }
2247 18         742 $self->{host} = undef;
2248 18         102 $self->{port} = undef;
2249             } elsif (defined $socket) {
2250             # Socket sugar
2251 19 100       43 die "Cannot specify both 'socket' and 'host' options\n" if defined $host;
2252 17 100       52 die "Cannot specify both 'socket' and 'port' options\n" if defined $port;
2253             $self->{listeners} = [{
2254 15         66 type => 'unix',
2255             path => $socket,
2256             socket_mode => $socket_mode,
2257             }];
2258 15         29 $self->{host} = undef;
2259 15         62 $self->{port} = undef;
2260             } else {
2261             # Host/port sugar (backward compatible default)
2262 333   100     1323 $host //= '127.0.0.1';
2263 333   100     950 $port //= 5000;
2264             $self->{listeners} = [{
2265 333         2824 type => 'tcp',
2266             host => $host,
2267             port => $port,
2268             }];
2269 333         950 $self->{host} = $host;
2270 333         1205 $self->{port} = $port;
2271             }
2272              
2273             # Apply server-wide SSL to all TCP listeners
2274 366 100       1533 if ($self->{ssl}) {
2275 54         104 for my $listener (@{$self->{listeners}}) {
  54         248  
2276 54 50       261 if ($listener->{type} eq 'tcp') {
2277 54         151 $listener->{ssl} = $self->{ssl};
2278             }
2279             }
2280             }
2281              
2282 366   100     3236 $self->{extensions} = delete $params->{extensions} // {};
2283 366 100       1921 $self->{access_log} = exists $params->{access_log} ? delete $params->{access_log} : \*STDERR;
2284 366   100     3947 $self->{access_log_format} = delete $params->{access_log_format} // 'clf';
2285             $self->{_access_log_formatter} = $self->_compile_access_log_format(
2286             $self->{access_log_format}
2287 366         2678 );
2288 366   100     1767 $self->{quiet} = delete $params->{quiet} // 0;
2289 366   100     2216 $self->{log_level} = delete $params->{log_level} // 'info';
2290             # Validate log level
2291 366         3381 my %valid_levels = (debug => 1, info => 2, warn => 3, error => 4);
2292             die "Invalid log_level '$self->{log_level}' - must be one of: debug, info, warn, error\n"
2293 366 50       1238 unless $valid_levels{$self->{log_level}};
2294 366         1451 $self->{_log_level_num} = $valid_levels{$self->{log_level}};
2295 366   100     1998 $self->{timeout} = delete $params->{timeout} // 60; # Connection idle timeout (seconds)
2296 366   100     1637 $self->{max_header_size} = delete $params->{max_header_size} // 8192; # Max header size in bytes
2297 366   100     1729 $self->{max_header_count} = delete $params->{max_header_count} // 100; # Max number of headers
2298 366   100     1760 $self->{max_body_size} = delete $params->{max_body_size} // 10_000_000; # Max body size in bytes (10MB default, 0 = unlimited)
2299 366   100     1677 $self->{workers} = delete $params->{workers} // 0; # Number of worker processes (0 = single process)
2300 366   100     1610 $self->{max_requests} = delete $params->{max_requests} // 0; # 0 = unlimited
2301 366   50     1765 $self->{listener_backlog} = delete $params->{listener_backlog} // 2048; # Listener queue size
2302 366   100     1748 $self->{shutdown_timeout} = delete $params->{shutdown_timeout} // 30; # Graceful shutdown timeout (seconds)
2303 366   50     1972 $self->{reuseport} = delete $params->{reuseport} // 0; # SO_REUSEPORT mode for multi-worker
2304 366   100     1752 $self->{max_receive_queue} = delete $params->{max_receive_queue} // 1000; # Max WebSocket receive queue size (messages)
2305 366   100     1685 $self->{max_ws_frame_size} = delete $params->{max_ws_frame_size} // 65536; # Max WebSocket frame size in bytes (64KB default)
2306 366   100     2101 $self->{max_connections} = delete $params->{max_connections} // 0; # 0 = use default (1000)
2307 366   100     1600 $self->{sync_file_threshold} = delete $params->{sync_file_threshold} // 65536; # Threshold for sync file reads (0=always async)
2308 366   100     1723 $self->{request_timeout} = delete $params->{request_timeout} // 0; # Request stall timeout in seconds (0 = disabled, default for performance)
2309 366   100     1783 $self->{ws_idle_timeout} = delete $params->{ws_idle_timeout} // 0; # WebSocket idle timeout (0 = disabled)
2310 366   100     1540 $self->{sse_idle_timeout} = delete $params->{sse_idle_timeout} // 0; # SSE idle timeout (0 = disabled)
2311 366   50     1697 $self->{heartbeat_timeout} = delete $params->{heartbeat_timeout} // 50; # Worker heartbeat timeout (0 = disabled)
2312 366   100     2043 $self->{lifespan_startup_timeout} = delete $params->{lifespan_startup_timeout} // 30; # Max wait for lifespan startup signal (0 = disabled)
2313 366   100     2004 $self->{lifespan_mode} = delete $params->{lifespan_mode} // 'auto'; # auto (default) | on (decline is fatal) | off (skip lifespan)
2314             die "Invalid lifespan_mode '$self->{lifespan_mode}' - must be one of: auto, on, off\n"
2315 366 100       3846 unless $self->{lifespan_mode} =~ /\A(?:auto|on|off)\z/;
2316 365   100     1675 $self->{write_high_watermark} = delete $params->{write_high_watermark} // 65536; # 64KB - pause sending above this
2317 365   100     1488 $self->{write_low_watermark} = delete $params->{write_low_watermark} // 16384; # 16KB - resume sending below this
2318 365         975 $self->{loop_type} = delete $params->{loop_type}; # Optional loop backend (EPoll, EV, Poll, etc.)
2319 365 100       1174 if (my $lt = $self->{loop_type}) {
2320 2 50       51 die "Invalid loop_type '$lt': must contain only letters, digits, and ::\n"
2321             unless $lt =~ /\A[A-Za-z][A-Za-z0-9_]*(?:::[A-Za-z][A-Za-z0-9_]*)*\z/;
2322             }
2323             # Dev-mode event validation: explicit flag, or auto-enable in development mode
2324             $self->{validate_events} = delete $params->{validate_events}
2325 363 50 100     3263 // (($ENV{PAGI_ENV} // '') eq 'development' ? 1 : 0);
      33        
2326              
2327             # HTTP/2 support (opt-in, experimental)
2328 363   100     1465 $self->{http2} = delete $params->{http2} // 0;
2329              
2330             # HTTP/2 protocol settings (only used when http2 is enabled)
2331 363   100     1815 my $h2_max_concurrent_streams = delete $params->{h2_max_concurrent_streams} // 100;
2332 363   100     1379 my $h2_initial_window_size = delete $params->{h2_initial_window_size} // 65535;
2333 363   100     1538 my $h2_max_frame_size = delete $params->{h2_max_frame_size} // 16384;
2334 363   100     1522 my $h2_enable_push = delete $params->{h2_enable_push} // 0;
2335 363   100     1612 my $h2_enable_connect_protocol = delete $params->{h2_enable_connect_protocol} // 1;
2336 363   100     1344 my $h2_max_header_list_size = delete $params->{h2_max_header_list_size} // 65536;
2337 363   50     2362 my $h2_rst_rate_limit = delete $params->{h2_rst_rate_limit} // { burst => 1000, rate => 33 };
2338              
2339 363         960 $self->{running} = 0;
2340 363         888 $self->{bound_port} = undef;
2341 363         876 $self->{listener} = undef;
2342 363         992 $self->{connections} = {}; # Hash keyed by refaddr for O(1) add/remove
2343             $self->{protocol} = PAGI::Server::Protocol::HTTP1->new(
2344             max_header_size => $self->{max_header_size},
2345             max_header_count => $self->{max_header_count},
2346 363         6065 );
2347 363         13030 $self->{state} = {}; # Shared state from lifespan
2348 363         1724 $self->{worker_pids} = {}; # Track worker PIDs in multi-worker mode
2349 363         1003 $self->{is_worker} = 0; # True if this is a worker process
2350              
2351             # Initialize HTTP/2 protocol handler if enabled and available
2352 363 100       1013 if ($self->{http2}) {
2353 90 50       281 if ($HTTP2_AVAILABLE) {
2354 90         934 $self->{http2_protocol} = PAGI::Server::Protocol::HTTP2->new(
2355             max_concurrent_streams => $h2_max_concurrent_streams,
2356             initial_window_size => $h2_initial_window_size,
2357             max_frame_size => $h2_max_frame_size,
2358             enable_push => $h2_enable_push,
2359             enable_connect_protocol => $h2_enable_connect_protocol,
2360             max_header_list_size => $h2_max_header_list_size,
2361             h2_rst_rate_limit => $h2_rst_rate_limit,
2362             );
2363 90         172 $self->{http2_enabled} = 1;
2364              
2365             # h2c mode: HTTP/2 over cleartext (no TLS)
2366 90 100       217 if (!$self->{ssl}) {
2367 83         180 $self->{h2c_enabled} = 1;
2368             }
2369             } else {
2370 0         0 die <<"END_HTTP2_ERROR";
2371             HTTP/2 support requested but Net::HTTP2::nghttp2 is not installed.
2372              
2373             To install:
2374             cpanm Net::HTTP2::nghttp2
2375              
2376             Or disable HTTP/2:
2377             http2 => 0
2378             END_HTTP2_ERROR
2379             }
2380             }
2381              
2382 363         2498 $self->SUPER::_init($params);
2383             }
2384              
2385             sub configure {
2386 364     364 1 4492 my ($self, %params) = @_;
2387              
2388 364 50       1012 if (exists $params{app}) {
2389 0         0 $self->{app} = delete $params{app};
2390             }
2391 364 50       1044 if (exists $params{host}) {
2392 0         0 $self->{host} = delete $params{host};
2393             }
2394 364 50       979 if (exists $params{port}) {
2395 0         0 $self->{port} = delete $params{port};
2396             }
2397 364 50       902 if (exists $params{socket}) {
2398 0         0 delete $params{socket};
2399             }
2400 364 50       773 if (exists $params{socket_mode}) {
2401 0         0 delete $params{socket_mode};
2402             }
2403 364 50       952 if (exists $params{listen}) {
2404 0         0 delete $params{listen};
2405             }
2406 364 50       918 if (exists $params{ssl}) {
2407 0         0 $self->{ssl} = delete $params{ssl};
2408             }
2409 364 50       820 if (exists $params{extensions}) {
2410 0         0 $self->{extensions} = delete $params{extensions};
2411             }
2412 364 50       838 if (exists $params{access_log}) {
2413 0         0 $self->{access_log} = delete $params{access_log};
2414             }
2415 364 100       847 if (exists $params{access_log_format}) {
2416 1         3 $self->{access_log_format} = delete $params{access_log_format};
2417             $self->{_access_log_formatter} = $self->_compile_access_log_format(
2418             $self->{access_log_format}
2419 1         3 );
2420             }
2421 364 50       1244 if (exists $params{quiet}) {
2422 0         0 $self->{quiet} = delete $params{quiet};
2423             }
2424 364 50       870 if (exists $params{log_level}) {
2425 0         0 my $level = delete $params{log_level};
2426 0         0 my %valid_levels = (debug => 1, info => 2, warn => 3, error => 4);
2427             die "Invalid log_level '$level' - must be one of: debug, info, warn, error\n"
2428 0 0       0 unless $valid_levels{$level};
2429 0         0 $self->{log_level} = $level;
2430 0         0 $self->{_log_level_num} = $valid_levels{$level};
2431             }
2432 364 50       857 if (exists $params{timeout}) {
2433 0         0 $self->{timeout} = delete $params{timeout};
2434             }
2435 364 50       784 if (exists $params{max_header_size}) {
2436 0         0 $self->{max_header_size} = delete $params{max_header_size};
2437             }
2438 364 50       855 if (exists $params{max_header_count}) {
2439 0         0 $self->{max_header_count} = delete $params{max_header_count};
2440             }
2441 364 50       782 if (exists $params{max_body_size}) {
2442 0         0 $self->{max_body_size} = delete $params{max_body_size};
2443             }
2444 364 50       961 if (exists $params{workers}) {
2445 0         0 $self->{workers} = delete $params{workers};
2446             }
2447 364 50       824 if (exists $params{max_requests}) {
2448 0         0 $self->{max_requests} = delete $params{max_requests};
2449             }
2450 364 50       847 if (exists $params{listener_backlog}) {
2451 0         0 $self->{listener_backlog} = delete $params{listener_backlog};
2452             }
2453 364 50       802 if (exists $params{shutdown_timeout}) {
2454 0         0 $self->{shutdown_timeout} = delete $params{shutdown_timeout};
2455             }
2456 364 50       829 if (exists $params{lifespan_startup_timeout}) {
2457 0         0 $self->{lifespan_startup_timeout} = delete $params{lifespan_startup_timeout};
2458             }
2459 364 50       785 if (exists $params{lifespan_mode}) {
2460 0         0 my $mode = delete $params{lifespan_mode};
2461 0 0       0 die "Invalid lifespan_mode '$mode' - must be one of: auto, on, off\n"
2462             unless $mode =~ /\A(?:auto|on|off)\z/;
2463 0         0 $self->{lifespan_mode} = $mode;
2464             }
2465 364 50       863 if (exists $params{max_receive_queue}) {
2466 0         0 $self->{max_receive_queue} = delete $params{max_receive_queue};
2467             }
2468 364 50       888 if (exists $params{max_ws_frame_size}) {
2469 0         0 $self->{max_ws_frame_size} = delete $params{max_ws_frame_size};
2470             }
2471 364 50       957 if (exists $params{max_connections}) {
2472 0         0 $self->{max_connections} = delete $params{max_connections};
2473             }
2474 364 50       826 if (exists $params{request_timeout}) {
2475 0         0 $self->{request_timeout} = delete $params{request_timeout};
2476             }
2477 364 50       797 if (exists $params{ws_idle_timeout}) {
2478 0         0 $self->{ws_idle_timeout} = delete $params{ws_idle_timeout};
2479             }
2480 364 50       835 if (exists $params{sse_idle_timeout}) {
2481 0         0 $self->{sse_idle_timeout} = delete $params{sse_idle_timeout};
2482             }
2483 364 50       795 if (exists $params{http2}) {
2484 0         0 $self->{http2} = delete $params{http2};
2485             }
2486              
2487 364         1762 $self->SUPER::configure(%params);
2488             }
2489              
2490             # Log levels: debug=1, info=2, warn=3, error=4
2491             my %_LOG_LEVELS = (debug => 1, info => 2, warn => 3, error => 4);
2492              
2493             sub _log {
2494 475     475   1370 my ($self, $level, $msg) = @_;
2495              
2496 475   50     1514 my $level_num = $_LOG_LEVELS{$level} // 2;
2497 475 100       1457 return if $level_num < $self->{_log_level_num};
2498 278 100 100     1786 return if $self->{quiet} && $level ne 'error';
2499 23         1240 warn "$msg\n";
2500             }
2501              
2502             # Returns a human-readable TLS status string for the startup banner
2503             sub _tls_status_string {
2504 210     210   434 my ($self) = @_;
2505              
2506 210 50       730 if ($self->{disable_tls}) {
2507 0 0       0 return $TLS_AVAILABLE ? 'disabled' : 'n/a (disabled)';
2508             }
2509 210 100       591 if ($self->{tls_enabled}) {
2510 41         126 return 'on';
2511             }
2512 169 50       794 return $TLS_AVAILABLE ? 'available' : 'not installed';
2513             }
2514              
2515             # Returns a human-readable HTTP/2 status string for the startup banner
2516             sub _http2_status_string {
2517 210     210   369 my ($self) = @_;
2518              
2519 210 100       592 if ($self->{http2_enabled}) {
2520 3 50       27 return $self->{h2c_enabled} ? 'on (h2c)' : 'on';
2521             }
2522 207 50       931 return $HTTP2_AVAILABLE ? 'available' : 'not installed';
2523             }
2524              
2525             # Returns a human-readable Future::XS status string for the startup banner
2526             sub _future_xs_status_string {
2527 210 50   210   675 return 'on' if $FUTURE_XS_ENABLED;
2528 210 50       567 return 'available' if $FUTURE_XS_AVAILABLE;
2529 210         606 return 'not installed';
2530             }
2531              
2532             # Check if TLS modules are available
2533             sub _check_tls_available {
2534 51     51   122 my ($self) = @_;
2535              
2536             # Allow forcing TLS off for testing — return false to skip TLS setup
2537 51 50       190 if ($self->{disable_tls}) {
2538 0         0 return 0;
2539             }
2540              
2541 51 50       294 return 1 if $TLS_AVAILABLE;
2542              
2543 0         0 die <<"END_TLS_ERROR";
2544             TLS support requested but required modules not installed.
2545              
2546             To enable HTTPS/TLS support, install:
2547              
2548             cpanm IO::Async::SSL IO::Socket::SSL
2549              
2550             Or on Debian/Ubuntu:
2551              
2552             apt-get install libio-socket-ssl-perl
2553              
2554             Then restart your application.
2555             END_TLS_ERROR
2556             }
2557              
2558             # Build SSL configuration parameters for use by both single-worker and multi-worker modes.
2559             # Returns a hashref of SSL params (including SSL_reuse_ctx) or undef if no SSL configured.
2560             sub _build_ssl_config {
2561 203     203   1933 my ($self) = @_;
2562 203 100       976 my $ssl = $self->{ssl} or return;
2563              
2564 45 50       218 return unless $self->_check_tls_available;
2565              
2566 45         78 my %ssl_params;
2567 45         168 $ssl_params{SSL_server} = 1;
2568 45 50       211 $ssl_params{SSL_cert_file} = $ssl->{cert_file} if $ssl->{cert_file};
2569 45 50       191 $ssl_params{SSL_key_file} = $ssl->{key_file} if $ssl->{key_file};
2570             # SSL_version as a true floor: negotiate the highest version the client also
2571             # supports at or above min_version, so TLS 1.3 is offered. IO::Socket::SSL
2572             # pins to an exact version for a bare 'TLSv1_2' (the trailing ':' does NOT
2573             # mean "or higher"), so instead start from auto-negotiation ('SSLv23') and
2574             # exclude every protocol below the floor. A min_version outside the
2575             # documented TLSv1_2/TLSv1_3 set is passed through unchanged.
2576 45         264 my @tls_order = qw(SSLv2 SSLv3 TLSv1 TLSv1_1 TLSv1_2 TLSv1_3);
2577 45   100     330 my $min_version = $ssl->{min_version} // 'TLSv1_2';
2578 45 50       125 if (grep { $_ eq $min_version } @tls_order) {
  270         390  
2579 45         105 my @below_floor;
2580 45         138 for my $v (@tls_order) {
2581 226 100       351 last if $v eq $min_version;
2582 181         310 push @below_floor, "!$v";
2583             }
2584 45         349 $ssl_params{SSL_version} = join(':', 'SSLv23', @below_floor);
2585             }
2586             else {
2587 0         0 $ssl_params{SSL_version} = $min_version;
2588             }
2589             $ssl_params{SSL_cipher_list} = $ssl->{cipher_list}
2590 45   100     240 // 'ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
2591              
2592 45 100       130 if ($ssl->{verify_client}) {
2593             # SSL_VERIFY_PEER (0x01) | SSL_VERIFY_FAIL_IF_NO_PEER_CERT (0x02)
2594 8         64 $ssl_params{SSL_verify_mode} = 0x03;
2595 8 50       48 $ssl_params{SSL_ca_file} = $ssl->{ca_file} if $ssl->{ca_file};
2596             } else {
2597 37         124 $ssl_params{SSL_verify_mode} = 0x00; # SSL_VERIFY_NONE
2598             }
2599              
2600             # ALPN negotiation for HTTP/2 support
2601 45 50 66     145 if ($self->{http2} && $HTTP2_AVAILABLE) {
2602 3         67 $ssl_params{SSL_alpn_protocols} = ['h2', 'http/1.1'];
2603 3         8 $self->{http2_enabled} = 1;
2604 3         8 $self->{h2c_enabled} = 0; # TLS mode, not cleartext
2605             }
2606              
2607             # Pre-create shared SSL context to avoid per-connection CA bundle parsing
2608 45         685 my $ssl_ctx = IO::Socket::SSL::SSL_Context->new(\%ssl_params);
2609 45         2290645 $self->{_ssl_ctx} = $ssl_ctx;
2610 45         225 $ssl_params{SSL_reuse_ctx} = $ssl_ctx;
2611              
2612             # Mark TLS enabled and auto-add tls extension
2613 45         144 $self->{tls_enabled} = 1;
2614 45 50       457 $self->{extensions}{tls} = {} unless exists $self->{extensions}{tls};
2615              
2616 45         375 return \%ssl_params;
2617             }
2618              
2619             =head2 run
2620              
2621             $server->run;
2622              
2623             Standalone entry point that creates an event loop, starts the server,
2624             and runs until shutdown. This is the simplest way to run a PAGI server:
2625              
2626             my $server = PAGI::Server->new(
2627             app => $app,
2628             port => 8080,
2629             );
2630             $server->run;
2631              
2632             For embedding in an existing IO::Async application, use the traditional
2633             pattern instead:
2634              
2635             my $loop = IO::Async::Loop->new;
2636             $loop->add($server);
2637             $server->listen->get;
2638             $loop->run;
2639              
2640             The C method handles:
2641              
2642             =over 4
2643              
2644             =item * Creating the event loop (respecting C if set)
2645              
2646             =item * Adding the server to the loop
2647              
2648             =item * Starting the listener
2649              
2650             =item * Setting up signal handlers for graceful shutdown
2651              
2652             =item * Running the event loop until shutdown
2653              
2654             =back
2655              
2656             =cut
2657              
2658             sub run {
2659 4     4 1 7 my ($self) = @_;
2660              
2661 4         9 my $loop = $self->_create_loop;
2662 4         17927 $loop->add($self);
2663              
2664             # Start listening with error handling
2665 4         291 eval { $self->listen->get };
  4         12  
2666 2 50       276 if ($@) {
2667 0         0 my $error = $@;
2668 0         0 my $port = $self->{port};
2669 0 0       0 if ($error =~ /Address already in use/i) {
    0          
2670 0         0 die "Error: Port $port is already in use\n";
2671             }
2672             elsif ($error =~ /Permission denied/i) {
2673 0         0 die "Error: Permission denied to bind to port $port\n";
2674             }
2675 0         0 die "Error starting server: $error\n";
2676             }
2677              
2678             # Run the event loop (signal handlers were set up by listen())
2679 2         46 $loop->run;
2680              
2681             # If the loop stopped because every worker failed to start (multi-worker),
2682             # exit non-zero so a process supervisor restarts us rather than seeing a
2683             # clean exit from a server that never served anything.
2684 2 100       292 if ($self->{_workers_failed}) {
2685 1         205 die "All workers failed to start; no workers remain\n";
2686             }
2687             }
2688              
2689             # Create an event loop, respecting loop_type config
2690             sub _create_loop {
2691 4     4   17 my ($self) = @_;
2692              
2693 4 50       61 if (my $loop_type = $self->{loop_type}) {
2694 0 0       0 die "Invalid loop_type '$loop_type': must contain only letters, digits, and ::\n"
2695             unless $loop_type =~ /\A[A-Za-z][A-Za-z0-9_]*(?:::[A-Za-z][A-Za-z0-9_]*)*\z/;
2696 0         0 my $loop_class = "IO::Async::Loop::$loop_type";
2697 0         0 (my $loop_file = "$loop_class.pm") =~ s{::}{/}g;
2698 0 0       0 eval { require $loop_file }
  0         0  
2699             or die "Cannot load loop backend '$loop_type': $@\n" .
2700             "Install it with: cpanm $loop_class\n";
2701 0         0 return $loop_class->new;
2702             }
2703              
2704 4         36 require IO::Async::Loop;
2705 4         27 return IO::Async::Loop->new;
2706             }
2707              
2708 214     214 1 29595 async sub listen {
2709 214         519 my ($self) = @_;
2710              
2711 214 50       760 return if $self->{running};
2712              
2713             # Multi-worker mode uses a completely different code path
2714 214 100 66     1279 if ($self->{workers} && $self->{workers} > 0) {
2715 18         581 return $self->_listen_multiworker;
2716             }
2717              
2718 196         700 return await $self->_listen_singleworker;
2719             }
2720              
2721             # Single-worker mode - uses IO::Async normally
2722 196     196   285 async sub _listen_singleworker {
2723 196         378 my ($self) = @_;
2724              
2725 196         345 weaken(my $weak_self = $self);
2726              
2727             # Run lifespan startup before accepting connections
2728 196         715 my $startup_result = await $self->_run_lifespan_startup;
2729              
2730 196 100       6374 if (!$startup_result->{success}) {
2731 4   50     49 my $message = $startup_result->{message} // 'Lifespan startup failed';
2732 4         85 $self->_log(error => "PAGI Server startup failed: $message");
2733 4         106 die "Lifespan startup failed: $message\n";
2734             }
2735              
2736             # Collect any inherited fds (from PAGI_REUSE or LISTEN_FDS)
2737 192         865 my $inherited = $self->_collect_inherited_fds;
2738              
2739             # Iterate over listeners array, creating one IO::Async::Listener per spec
2740 192         325 my @listen_entries;
2741 192         314 for my $spec (@{$self->{listeners}}) {
  192         529  
2742              
2743             # Check for inherited fd matching this spec
2744 195 100       1228 my $match_key = $spec->{type} eq 'unix'
2745             ? "unix:$spec->{path}"
2746             : "$spec->{host}:$spec->{port}";
2747              
2748 195 100       725 if (my $inh = delete $inherited->{$match_key}) {
2749             # Reuse inherited fd — skip bind/listen entirely
2750 3         8 $spec->{_inherited} = 1;
2751              
2752 3         4 my $handle = $inh->{handle};
2753 3 100       7 if (!$handle) {
2754 2 100       4 my $class = $inh->{type} eq 'unix'
2755             ? 'IO::Socket::UNIX' : 'IO::Socket::INET';
2756 2 100       12 require IO::Socket::UNIX if $inh->{type} eq 'unix';
2757 2 50       21 $handle = $class->new_from_fd($inh->{fd}, 'r')
2758             or die "Cannot open inherited fd $inh->{fd}: $!\n";
2759             }
2760              
2761 3 100 100     165 if ($inh->{type} eq 'tcp' && $handle->can('sockport')) {
2762 1         3 $spec->{port} = $handle->sockport;
2763 1   33     29 $self->{bound_port} //= $spec->{port};
2764             }
2765              
2766 3         5 my $spec_ref = $spec;
2767 3         7 weaken(my $weak_inner = $self);
2768             my $listener = IO::Async::Listener->new(
2769             handle => $handle,
2770             on_stream => sub {
2771 2     2   4253 my ($l, $stream) = @_;
2772 2 50       21 return unless $weak_inner;
2773 2         12 $weak_inner->_on_connection($stream, $spec_ref);
2774             },
2775 3         36 );
2776 3         559 $self->add_child($listener);
2777              
2778 3         678 $self->_log(info => "Reusing inherited fd $inh->{fd} for $match_key"
2779             . " (source: $inh->{source})");
2780              
2781 3         8 push @listen_entries, { listener => $listener, spec => $spec };
2782 3         13 next; # Skip normal bind/listen
2783             }
2784              
2785 192         276 my $spec_copy = $spec; # capture for closure
2786             my $listener = IO::Async::Listener->new(
2787             on_stream => sub {
2788 198     198   19580354 my ($listener, $stream) = @_;
2789 198 50       924 return unless $weak_self;
2790 198         1725 $weak_self->_on_connection($stream, $spec_copy);
2791             },
2792 192         2368 );
2793              
2794 192         14024 $self->add_child($listener);
2795              
2796             # Build listener options
2797             my %listen_opts = (
2798             queuesize => $self->{listener_backlog},
2799 192         23514 );
2800              
2801 192 100       590 if ($spec->{type} eq 'unix') {
2802             # Remove stale socket file if it exists
2803 10 100       693 unlink $spec->{path} if -e $spec->{path};
2804              
2805             $listen_opts{addr} = {
2806             family => 'unix',
2807             socktype => 'stream',
2808             path => $spec->{path},
2809 10         81 };
2810              
2811 10 50       34 if ($self->{tls_enabled}) {
2812 0         0 $self->_log(info => "Note: TLS is configured but does not apply to Unix socket $spec->{path}");
2813             }
2814             } else {
2815             # TCP listener
2816             $listen_opts{addr} = {
2817             family => 'inet',
2818             socktype => 'stream',
2819             ip => $spec->{host},
2820             port => $spec->{port},
2821 182         1365 };
2822              
2823             # Add SSL options if configured (TCP only)
2824 182 100       756 if (my $ssl_params = $self->_build_ssl_config) {
2825 35         146 $listen_opts{extensions} = ['SSL'];
2826 35         443 %listen_opts = (%listen_opts, %$ssl_params);
2827              
2828             $listen_opts{on_ssl_error} = sub {
2829 4 50   4   282636 return unless $weak_self;
2830 4         36 $weak_self->_log(debug => "SSL handshake failed: $_[0]");
2831 35         364 };
2832             }
2833             }
2834              
2835             # Set restrictive umask for Unix socket bind to prevent brief
2836             # permission window (CVE-2023-45145 pattern in Redis)
2837 192         360 my $old_umask;
2838 192 100       659 if ($spec->{type} eq 'unix') {
2839 10         35 $old_umask = umask(0177); # Owner-only until chmod
2840             }
2841              
2842             # Start listening ($^F raised so fd survives exec for hot restart)
2843             {
2844 192         247 local $^F = 1023;
  192         898  
2845 192         1107 await $listener->listen(%listen_opts);
2846             }
2847              
2848             # Restore umask after bind
2849 192 100       158792 umask($old_umask) if defined $old_umask;
2850              
2851             # Configure accept error handler after listen() to avoid SSL extension conflicts
2852 192         382 eval {
2853             $listener->configure(
2854             on_accept_error => sub {
2855 0     0   0 my ($listener, $error) = @_;
2856 0 0       0 return unless $weak_self;
2857 0         0 $weak_self->_on_accept_error($error);
2858             },
2859 192         1153 );
2860             };
2861 192 50       44568 if ($@) {
2862 192         1061 $self->_log(debug => "Could not configure on_accept_error (likely SSL listener): $@");
2863             }
2864              
2865             # Post-listen setup
2866 192 100       601 if ($spec->{type} eq 'unix') {
2867             # Apply socket permissions if configured
2868 10 100       59 if (defined $spec->{socket_mode}) {
2869 1         37 chmod $spec->{socket_mode}, $spec->{path};
2870             }
2871             } else {
2872             # Store the actual bound port from the listener's read handle
2873 182         779 my $socket = $listener->read_handle;
2874 182 50 33     2006 if ($socket && $socket->can('sockport')) {
2875 182         641 my $bound = $socket->sockport;
2876 182         8852 $spec->{port} = $bound; # update spec with actual port
2877 182   33     839 $self->{bound_port} //= $bound; # first TCP port wins
2878             }
2879             }
2880              
2881             # Register in PAGI_REUSE for hot restart fd inheritance
2882 192         555 my $rh = $listener->read_handle;
2883 192 50       942 if ($rh) {
2884 192         420 my $fd = fileno($rh);
2885 192 50       411 if (defined $fd) {
2886 192 100       920 my $reuse_key = $spec->{type} eq 'unix'
2887             ? "unix:$spec->{path}:$fd"
2888             : "$spec->{host}:$spec->{port}:$fd";
2889 192         522 $spec->{_reuse_key} = $reuse_key;
2890 192 100 100     2538 $ENV{PAGI_REUSE} = length($ENV{PAGI_REUSE} // '')
2891             ? "$ENV{PAGI_REUSE},$reuse_key"
2892             : $reuse_key;
2893             }
2894             }
2895              
2896 192         1664 push @listen_entries, { listener => $listener, spec => $spec };
2897             }
2898              
2899             # Warn about unmatched inherited fds
2900 192         653 for my $key (sort keys %$inherited) {
2901 0         0 my $inh = $inherited->{$key};
2902 0         0 $self->_log(warn => "Inherited fd $inh->{fd} ($key) does not match "
2903             . "any listener spec — closing");
2904 0 0       0 if ($inh->{handle}) {
2905 0         0 close($inh->{handle});
2906             } else {
2907 0         0 POSIX::close($inh->{fd});
2908             }
2909             }
2910              
2911 192         563 $self->{_listen_entries} = \@listen_entries;
2912             # Backward compat: keep $self->{listener} pointing to first entry
2913 192 50       666 $self->{listener} = $listen_entries[0]{listener} if @listen_entries;
2914 192         383 $self->{running} = 1;
2915              
2916             # Set up signal handlers for graceful shutdown (single-worker mode)
2917             # Note: Windows doesn't support Unix signals, so this is skipped there
2918 192         288 unless (WIN32) {
2919 192         291 my $shutdown_triggered = 0;
2920             my $shutdown_handler = sub {
2921 1 50   1   232352 return if $shutdown_triggered;
2922 1         3 $shutdown_triggered = 1;
2923             $self->adopt_future(
2924             $self->shutdown->on_done(sub {
2925 1         58 $self->loop->stop;
2926             })->on_fail(sub {
2927 0         0 my ($error) = @_;
2928 0         0 $self->_log(error => "Shutdown error: $error");
2929 0         0 $self->loop->stop; # Still stop even on error
2930             })
2931 1         9 );
2932 192         971 };
2933 192         712 $self->loop->watch_signal(TERM => $shutdown_handler);
2934 192         42724 $self->loop->watch_signal(INT => $shutdown_handler);
2935              
2936             # HUP in single-worker mode just warns (graceful restart requires multi-worker)
2937 192         6879 my $weak_self = $self;
2938 192         351 weaken($weak_self);
2939             $self->loop->watch_signal(HUP => sub {
2940             $weak_self->_log(warn => "Received HUP signal (graceful restart only works in multi-worker mode)")
2941 0 0 0 0   0 if $weak_self && !$weak_self->{quiet};
2942 192         429 });
2943              
2944             $self->loop->watch_signal(USR2 => sub {
2945             $weak_self->_log(warn => "Received USR2 signal (hot restart only works in multi-worker mode)")
2946 0 0 0 0   0 if $weak_self && !$weak_self->{quiet};
2947 192         6013 });
2948             }
2949              
2950 192         5837 my $loop_class = ref($self->loop);
2951 192         1302 $loop_class =~ s/^IO::Async::Loop:://; # Shorten for display
2952 192         722 my $max_conn = $self->effective_max_connections;
2953 192         670 my $tls_status = $self->_tls_status_string;
2954 192         540 my $http2_status = $self->_http2_status_string;
2955 192         538 my $future_xs_status = $self->_future_xs_status_string;
2956              
2957             # Warn if access_log is a terminal (slow for benchmarks)
2958 192 50 66     1659 if ($self->{access_log} && -t $self->{access_log}) {
2959 0         0 $self->_log(warn =>
2960             "access_log is a terminal; this may impact performance. " .
2961             "Consider redirecting to a file or setting access_log => undef for benchmarks."
2962             );
2963             }
2964              
2965             # Log listening banner
2966 192 100       625 my $scheme = $self->{tls_enabled} ? 'https' : 'http';
2967 192 100       488 if (@listen_entries == 1) {
2968 189         392 my $spec = $listen_entries[0]{spec};
2969 189 100       543 if ($spec->{type} eq 'unix') {
2970 8         36 $self->_log(info => "PAGI Server listening on unix:$spec->{path} (loop: $loop_class, max_conn: $max_conn, http2: $http2_status, tls: $tls_status, future_xs: $future_xs_status)");
2971             } else {
2972 181         1040 $self->_log(info => "PAGI Server listening on $scheme://$spec->{host}:$spec->{port}/ (loop: $loop_class, max_conn: $max_conn, http2: $http2_status, tls: $tls_status, future_xs: $future_xs_status)");
2973             }
2974             } else {
2975 3         6 my @addrs;
2976 3         18 for my $entry (@listen_entries) {
2977 6         6 my $s = $entry->{spec};
2978 6 100       15 if ($s->{type} eq 'unix') {
2979 3         6 push @addrs, "unix:$s->{path}";
2980             } else {
2981 3         12 push @addrs, "$scheme://$s->{host}:$s->{port}/";
2982             }
2983             }
2984 3         21 $self->_log(info => "PAGI Server listening on: " . join(', ', @addrs) . " (loop: $loop_class, max_conn: $max_conn, http2: $http2_status, tls: $tls_status, future_xs: $future_xs_status)");
2985             }
2986              
2987             # Warn in production if using default max_connections
2988 192 100 100     1134 if (($ENV{PAGI_ENV} // '') eq 'production' && !$self->{max_connections}) {
      66        
2989 1         4 $self->_log(warn =>
2990             "Using default max_connections (1000). For production, consider tuning this value " .
2991             "based on your workload. See 'perldoc PAGI::Server' for guidance."
2992             );
2993             }
2994              
2995 192         2002 return $self;
2996             }
2997              
2998             # Multi-worker mode - forks workers, each with their own event loop
2999             sub _listen_multiworker {
3000 18     18   234 my ($self) = @_;
3001              
3002 18         262 my $workers = $self->{workers};
3003 18         242 my $reuseport = $self->{reuseport};
3004              
3005             # Create all listening sockets before forking workers
3006 18         100 my @listen_entries;
3007              
3008             # Collect any inherited fds (from PAGI_REUSE or LISTEN_FDS)
3009 18         677 my $inherited = $self->_collect_inherited_fds;
3010              
3011 18         117 for my $spec (@{$self->{listeners}}) {
  18         223  
3012 18         113 my $socket;
3013              
3014             # Check for inherited fd matching this spec
3015 18 50       518 my $match_key = $spec->{type} eq 'unix'
3016             ? "unix:$spec->{path}"
3017             : "$spec->{host}:$spec->{port}";
3018              
3019 18 50       793 if (my $inh = delete $inherited->{$match_key}) {
    50          
    50          
3020 0         0 $spec->{_inherited} = 1;
3021              
3022 0 0       0 if ($inh->{handle}) {
3023 0         0 $socket = $inh->{handle};
3024             } else {
3025 0 0       0 my $class = $inh->{type} eq 'unix'
3026             ? 'IO::Socket::UNIX' : 'IO::Socket::INET';
3027 0 0       0 require IO::Socket::UNIX if $inh->{type} eq 'unix';
3028 0 0       0 $socket = $class->new_from_fd($inh->{fd}, 'r')
3029             or die "Cannot open inherited fd $inh->{fd}: $!\n";
3030             }
3031              
3032 0 0 0     0 if ($inh->{type} eq 'tcp' && $socket->can('sockport')) {
3033 0         0 $spec->{bound_port} = $socket->sockport;
3034 0   0     0 $self->{bound_port} //= $spec->{bound_port};
3035             }
3036              
3037 0         0 $self->_log(info => "Reusing inherited fd $inh->{fd} for $match_key"
3038             . " (source: $inh->{source})");
3039             }
3040             elsif ($spec->{type} eq 'unix') {
3041             # Unix socket: parent creates, workers inherit
3042 0 0       0 unlink $spec->{path} if -e $spec->{path};
3043              
3044             # Set restrictive umask for bind (CVE-2023-45145 mitigation)
3045 0         0 my $old_umask = umask(0177);
3046              
3047 0         0 require IO::Socket::UNIX;
3048             {
3049 0         0 local $^F = 1023;
  0         0  
3050             $socket = IO::Socket::UNIX->new(
3051             Local => $spec->{path},
3052             Type => Socket::SOCK_STREAM(),
3053             Listen => $self->{listener_backlog},
3054 0 0       0 ) or die "Cannot create Unix socket $spec->{path}: $!";
3055             }
3056              
3057 0         0 umask($old_umask);
3058              
3059 0 0       0 if (defined $spec->{socket_mode}) {
3060             chmod($spec->{socket_mode}, $spec->{path})
3061 0 0       0 or die "Cannot chmod $spec->{path}: $!\n";
3062             }
3063              
3064             # Register in PAGI_REUSE for hot restart fd inheritance
3065 0 0 0     0 if ($socket && !$spec->{_inherited}) {
3066 0         0 my $fd = fileno($socket);
3067 0         0 my $reuse_key = "unix:$spec->{path}:$fd";
3068 0         0 $spec->{_reuse_key} = $reuse_key;
3069 0 0 0     0 $ENV{PAGI_REUSE} = length($ENV{PAGI_REUSE} // '')
3070             ? "$ENV{PAGI_REUSE},$reuse_key"
3071             : $reuse_key;
3072             }
3073             } elsif ($reuseport) {
3074             # reuseport TCP: probe to get port, workers create their own
3075             # Note: reuseport sockets are not registered in PAGI_REUSE because
3076             # each worker creates its own socket. fd inheritance for reuseport
3077             # mode is not currently supported — use shared-socket mode for
3078             # hot restart / systemd socket activation.
3079             my $probe_socket = IO::Socket::INET->new(
3080             LocalAddr => $spec->{host},
3081             LocalPort => $spec->{port},
3082 0 0       0 Proto => 'tcp',
3083             Listen => 1,
3084             ReuseAddr => 1,
3085             ReusePort => 1,
3086             ) or die "Cannot bind to $spec->{host}:$spec->{port}: $!";
3087 0         0 $spec->{bound_port} = $probe_socket->sockport;
3088 0   0     0 $self->{bound_port} //= $spec->{bound_port};
3089 0         0 close($probe_socket);
3090             } else {
3091             # Shared-socket TCP: parent creates, workers inherit
3092             {
3093 18         114 local $^F = 1023;
  18         561  
3094             $socket = IO::Socket::INET->new(
3095             LocalAddr => $spec->{host},
3096             LocalPort => $spec->{port},
3097             Proto => 'tcp',
3098             Listen => $self->{listener_backlog},
3099 18 50       1187 ReuseAddr => 1,
3100             Blocking => 0,
3101             ) or die "Cannot create listening socket on $spec->{host}:$spec->{port}: $!";
3102             }
3103 18         21995 $spec->{bound_port} = $socket->sockport;
3104 18   33     1818 $self->{bound_port} //= $spec->{bound_port};
3105              
3106             # Register in PAGI_REUSE for hot restart fd inheritance
3107 18 50 33     294 if ($socket && !$spec->{_inherited}) {
3108 18         105 my $fd = fileno($socket);
3109 18         296 my $reuse_key = "$spec->{host}:" . $socket->sockport . ":$fd";
3110 18         699 $spec->{_reuse_key} = $reuse_key;
3111 18 50 100     1370 $ENV{PAGI_REUSE} = length($ENV{PAGI_REUSE} // '')
3112             ? "$ENV{PAGI_REUSE},$reuse_key"
3113             : $reuse_key;
3114             }
3115             }
3116              
3117 18         302 push @listen_entries, { socket => $socket, spec => $spec };
3118             }
3119              
3120             # Warn about unmatched inherited fds
3121 18         352 for my $key (sort keys %$inherited) {
3122 0         0 my $inh = $inherited->{$key};
3123 0         0 $self->_log(warn => "Inherited fd $inh->{fd} ($key) does not match "
3124             . "any listener spec — closing");
3125 0 0       0 if ($inh->{handle}) {
3126 0         0 close($inh->{handle});
3127             } else {
3128 0         0 POSIX::close($inh->{fd});
3129             }
3130             }
3131              
3132 18         114 $self->{_listen_entries} = \@listen_entries;
3133             # Backward compat: keep listen_socket pointing to first entry's socket
3134 18 50 33     414 $self->{listen_socket} = $listen_entries[0]{socket} if @listen_entries && $listen_entries[0]{socket};
3135              
3136 18         72 $self->{running} = 1;
3137              
3138             # Validate TLS modules and set tls_enabled before forking workers
3139 18 100       128 if ($self->{ssl}) {
3140 6         102 $self->_check_tls_available;
3141 6         18 $self->{tls_enabled} = 1;
3142             }
3143              
3144 18 100       405 my $scheme = $self->{ssl} ? 'https' : 'http';
3145 18         203 my $loop_class = ref($self->loop);
3146 18         701 $loop_class =~ s/^IO::Async::Loop:://; # Shorten for display
3147 18 50       204 my $mode = $reuseport ? 'reuseport' : 'shared-socket';
3148 18         470 my $max_conn = $self->effective_max_connections;
3149 18         364 my $tls_status = $self->_tls_status_string;
3150 18         210 my $http2_status = $self->_http2_status_string;
3151 18         107 my $future_xs_status = $self->_future_xs_status_string;
3152              
3153             # Warn if access_log is a terminal (slow for benchmarks)
3154 18 50 66     398 if ($self->{access_log} && -t $self->{access_log}) {
3155 0         0 $self->_log(warn =>
3156             "access_log is a terminal; this may impact performance. " .
3157             "Consider redirecting to a file or setting access_log => undef for benchmarks."
3158             );
3159             }
3160              
3161             # Log listening banner for all listeners
3162 18         137 my @addrs;
3163 18         185 for my $entry (@listen_entries) {
3164 18         430 my $s = $entry->{spec};
3165 18 50       77 if ($s->{type} eq 'unix') {
3166 0         0 push @addrs, "unix:$s->{path}";
3167             } else {
3168 18   33     225 my $port = $s->{bound_port} // $s->{port};
3169 18         206 push @addrs, "$scheme://$s->{host}:$port/";
3170             }
3171             }
3172 18         233 my $addr_str = join(', ', @addrs);
3173 18         397 $self->_log(info => "PAGI Server (multi-worker, $mode) listening on $addr_str with $workers workers (loop: $loop_class, max_conn: $max_conn/worker, http2: $http2_status, tls: $tls_status, future_xs: $future_xs_status)");
3174              
3175             # Warn in production if using default max_connections
3176 18 100 100     453 if (($ENV{PAGI_ENV} // '') eq 'production' && !$self->{max_connections}) {
      66        
3177 3         9 $self->_log(warn =>
3178             "Using default max_connections (1000). For production, consider tuning this value " .
3179             "based on your workload. See 'perldoc PAGI::Server' for guidance."
3180             );
3181             }
3182              
3183 18         101 my $loop = $self->loop;
3184              
3185             # Fork the workers FIRST, before setting up signal handlers.
3186             # This prevents children from inheriting the parent's sigpipe setup,
3187             # which can cause issues with Ctrl-C signal delivery on macOS.
3188 18         262 for my $i (1 .. $workers) {
3189 29         697 $self->_spawn_worker(\@listen_entries, $i);
3190             }
3191              
3192             # Set up signal handlers for parent process AFTER forking
3193             # Note: Windows doesn't support Unix signals, so this is skipped there
3194 7         326 unless (WIN32) {
3195 7     3   918 $loop->watch_signal(TERM => sub { $self->_initiate_multiworker_shutdown });
  3         4159127  
3196 7     1   1907 $loop->watch_signal(INT => sub { $self->_initiate_multiworker_shutdown });
  1         2017671  
3197 7     0   1467 $loop->watch_signal(HUP => sub { $self->_graceful_restart });
  0         0  
3198 7     0   627 $loop->watch_signal(TTIN => sub { $self->_increase_workers });
  0         0  
3199 7     0   660 $loop->watch_signal(TTOU => sub { $self->_decrease_workers });
  0         0  
3200 7     0   546 $loop->watch_signal(USR2 => sub { $self->_hot_restart });
  0         0  
3201             }
3202              
3203             # Start heartbeat monitor if enabled
3204 7 50 33     947 if ($self->{heartbeat_timeout} && $self->{heartbeat_timeout} > 0) {
3205 7         129 my $hb_timeout = $self->{heartbeat_timeout};
3206 7         183 my $check_interval = $hb_timeout / 2;
3207 7         100 weaken(my $weak_self = $self);
3208              
3209             my $hb_check_timer = IO::Async::Timer::Periodic->new(
3210             interval => $check_interval,
3211             on_tick => sub {
3212 0 0   0   0 return unless $weak_self;
3213 0 0       0 return if $weak_self->{shutting_down};
3214              
3215 0         0 my $now = time();
3216 0         0 for my $pid (keys %{$weak_self->{worker_pids}}) {
  0         0  
3217 0         0 my $info = $weak_self->{worker_pids}{$pid};
3218 0 0       0 next unless $info->{heartbeat_rd};
3219              
3220             # Drain all available heartbeat bytes
3221 0         0 while (sysread($info->{heartbeat_rd}, my $buf, 64)) {
3222 0         0 $info->{last_heartbeat} = $now;
3223             }
3224              
3225             # Kill if heartbeat expired
3226 0 0       0 if ($now - $info->{last_heartbeat} > $hb_timeout) {
3227 0         0 $weak_self->_log(warn =>
3228             "Worker $pid (worker $info->{worker_num}) heartbeat " .
3229             "timeout after ${hb_timeout}s, sending SIGKILL");
3230 0         0 kill 'KILL', $pid;
3231             }
3232             }
3233             },
3234 7         1256 );
3235              
3236 7         3369 $self->add_child($hb_check_timer);
3237 7         2238 $hb_check_timer->start;
3238 7         39060 $self->{_heartbeat_check_timer} = $hb_check_timer;
3239             }
3240              
3241             # Hot restart handoff: if we were spawned by USR2, signal the old master
3242 7 50       401 if (my $old_master_pid = delete $ENV{PAGI_MASTER_PID}) {
3243 0         0 $old_master_pid = int($old_master_pid);
3244              
3245             # Wait for workers to be healthy before retiring old master
3246             # Delay = half the heartbeat timeout + 1 second buffer
3247 0   0     0 my $handoff_delay = ($self->{heartbeat_timeout} || 10) / 2 + 1;
3248 0         0 weaken(my $weak_self_handoff = $self);
3249              
3250             $self->loop->watch_time(
3251             after => $handoff_delay,
3252             code => sub {
3253 0 0   0   0 return unless $weak_self_handoff;
3254              
3255 0         0 my $worker_count = scalar keys %{$weak_self_handoff->{worker_pids}};
  0         0  
3256 0 0       0 if ($worker_count == 0) {
3257 0         0 $weak_self_handoff->_log(error =>
3258             "Hot restart: no workers running, not retiring old master $old_master_pid");
3259 0         0 return;
3260             }
3261              
3262 0 0       0 if (kill(0, $old_master_pid)) {
3263 0         0 $weak_self_handoff->_log(info =>
3264             "Hot restart: $worker_count workers healthy, "
3265             . "sending SIGTERM to old master $old_master_pid");
3266 0         0 kill('TERM', $old_master_pid);
3267             } else {
3268 0         0 $weak_self_handoff->_log(warn =>
3269             "Hot restart: old master $old_master_pid is no longer running");
3270             }
3271             },
3272 0         0 );
3273             }
3274              
3275             # Return immediately - caller (Runner) will call $loop->run()
3276             # This is consistent with single-worker mode behavior
3277 7         1454 return $self;
3278             }
3279              
3280             # Collect inherited file descriptors from PAGI_REUSE and LISTEN_FDS.
3281             # Returns a hashref keyed by "host:port" or "unix:path", values are
3282             # { fd, type, host/port/path, handle?, source }
3283             sub _collect_inherited_fds {
3284 218     218   729 my ($self) = @_;
3285 218         521 my %inherited;
3286              
3287             # Source 1: PAGI_REUSE (format: addr:port:fd,unix:path:fd,...)
3288 218 100       1180 if (my $reuse = $ENV{PAGI_REUSE}) {
3289 7         21 for my $entry (split /,/, $reuse) {
3290 10 100       77 if ($entry =~ /^unix:(.+):(\d+)$/) {
    100          
    100          
3291 3         10 my ($path, $fd) = ($1, int($2));
3292 3         20 $inherited{"unix:$path"} = {
3293             fd => $fd, type => 'unix', path => $path,
3294             source => 'pagi_reuse',
3295             };
3296             } elsif ($entry =~ /^(\[.+?\]):(\d+):(\d+)$/) {
3297 1         6 my ($host, $port, $fd) = ($1, int($2), int($3));
3298 1         7 $inherited{"$host:$port"} = {
3299             fd => $fd, type => 'tcp', host => $host, port => $port,
3300             source => 'pagi_reuse',
3301             };
3302             } elsif ($entry =~ /^(.+):(\d+):(\d+)$/) {
3303 4         20 my ($host, $port, $fd) = ($1, int($2), int($3));
3304 4         26 $inherited{"$host:$port"} = {
3305             fd => $fd, type => 'tcp', host => $host, port => $port,
3306             source => 'pagi_reuse',
3307             };
3308             }
3309             # Malformed entries silently skipped
3310             }
3311             }
3312              
3313             # Source 2: LISTEN_FDS (systemd socket activation)
3314 218         628 my $listen_fds = $ENV{LISTEN_FDS};
3315 218 50 66     1023 if (defined $listen_fds && $listen_fds =~ /^\d+$/ && $listen_fds > 0) {
      66        
3316 3 100 66     29 if (defined $ENV{LISTEN_PID} && $ENV{LISTEN_PID} == $$) {
3317 1         1 my $n = int($listen_fds);
3318 1         3 for my $i (0 .. $n - 1) {
3319 1         2 my $fd = 3 + $i; # SD_LISTEN_FDS_START
3320              
3321 1         2 my $fh;
3322 1 50       23 unless (open($fh, '+<&=', $fd)) {
3323 0         0 $self->_log(warn => "Cannot fdopen inherited fd $fd: $!");
3324 0         0 next;
3325             }
3326              
3327 1         7 my $addr = getsockname($fh);
3328 1 50       2 unless ($addr) {
3329 0         0 $self->_log(warn => "Cannot getsockname on inherited fd $fd: $!");
3330 0         0 next;
3331             }
3332              
3333 1         3 my $family = sockaddr_family($addr);
3334              
3335 1 50       4 if ($family == AF_UNIX) {
    50          
3336 0         0 my $path = unpack_sockaddr_un($addr);
3337 0         0 my $key = "unix:$path";
3338 0   0     0 $inherited{$key} //= {
3339             fd => $fd, type => 'unix', path => $path,
3340             handle => $fh, source => 'systemd',
3341             };
3342             } elsif ($family == AF_INET) {
3343 1         4 my ($port, $host_packed) = unpack_sockaddr_in($addr);
3344 1         4 my $host = Socket::inet_ntoa($host_packed);
3345 1         2 my $key = "$host:$port";
3346 1   50     11 $inherited{$key} //= {
3347             fd => $fd, type => 'tcp', host => $host, port => $port,
3348             handle => $fh, source => 'systemd',
3349             };
3350             } else {
3351 0         0 $self->_log(warn =>
3352             "Inherited fd $fd has unsupported address family $family");
3353             }
3354             }
3355             }
3356              
3357             # Always clean up systemd env vars (per sd_listen_fds spec)
3358 3         32 delete @ENV{qw(LISTEN_FDS LISTEN_PID LISTEN_FDNAMES)};
3359             }
3360              
3361 218         674 return \%inherited;
3362             }
3363              
3364             # Initiate graceful shutdown in multi-worker mode
3365             sub _initiate_multiworker_shutdown {
3366 5     5   2098013 my ($self) = @_;
3367              
3368 5 50       61 return if $self->{shutting_down};
3369 5         72 $self->{shutting_down} = 1;
3370 5         48 $self->{running} = 0;
3371              
3372             # Stop heartbeat monitoring — shutdown escalation timer handles stuck workers
3373 5 50       47 if ($self->{_heartbeat_check_timer}) {
3374 5         75 $self->{_heartbeat_check_timer}->stop;
3375 5         626 $self->remove_child($self->{_heartbeat_check_timer});
3376 5         1040 delete $self->{_heartbeat_check_timer};
3377             }
3378              
3379             # Close all listen sockets to stop accepting new connections
3380             # (skip during hot restart — new master is using these fds)
3381 5 50       47 if (!$self->{_hot_restart_in_progress}) {
3382 5   50     13 for my $entry (@{$self->{_listen_entries} // []}) {
  5         154  
3383 5 50       43 if ($entry->{socket}) {
3384 5         92 close($entry->{socket});
3385             }
3386             }
3387 5 50       34 if ($self->{listen_socket}) {
3388 5         88 delete $self->{listen_socket};
3389             }
3390             }
3391              
3392             # Clean up PAGI_REUSE entries (skip during hot restart)
3393 5 50       47 if (!$self->{_hot_restart_in_progress}) {
3394 5   50     32 for my $entry (@{$self->{_listen_entries} // []}) {
  5         115  
3395 5         40 my $key = $entry->{spec}{_reuse_key};
3396 5 50 33     121 if ($key && defined $ENV{PAGI_REUSE}) {
3397 5         642 $ENV{PAGI_REUSE} =~ s/(?:^|,)\Q$key\E//;
3398 5 50       73 $ENV{PAGI_REUSE} =~ s/^,// if defined $ENV{PAGI_REUSE};
3399             }
3400             }
3401             }
3402              
3403             # Clean up Unix socket files (skip inherited, skip during hot restart)
3404 5 50       29 if (!$self->{_hot_restart_in_progress}) {
3405 5   50     11 for my $entry (@{$self->{_listen_entries} // []}) {
  5         48  
3406 5 0 33     48 if ($entry->{spec}{type} eq 'unix'
      33        
3407             && !$entry->{spec}{_inherited}
3408             && -e $entry->{spec}{path}) {
3409 0         0 unlink $entry->{spec}{path};
3410             }
3411             }
3412             }
3413              
3414             # Signal all workers to shutdown
3415 5         13 for my $pid (keys %{$self->{worker_pids}}) {
  5         561  
3416 9         324 kill 'TERM', $pid;
3417             }
3418              
3419             # If no workers, stop the loop immediately
3420 5 50       31 if (!keys %{$self->{worker_pids}}) {
  5         44  
3421 0         0 $self->loop->stop;
3422 0         0 return;
3423             }
3424              
3425             # Escalate to SIGKILL after shutdown_timeout for workers that ignore SIGTERM
3426 5   50     38 my $timeout = $self->{shutdown_timeout} // 30;
3427 5         27 weaken(my $weak_self = $self);
3428             $self->{_shutdown_kill_timer} = $self->loop->watch_time(
3429             after => $timeout,
3430             code => sub {
3431 0 0   0   0 return unless $weak_self;
3432 0         0 for my $pid (keys %{$weak_self->{worker_pids}}) {
  0         0  
3433 0         0 $weak_self->_log(warn =>
3434             "Worker $pid did not exit after ${timeout}s, sending SIGKILL");
3435 0         0 kill 'KILL', $pid;
3436             }
3437             },
3438 5         46 );
3439             }
3440              
3441             # Graceful restart: replace all workers one by one
3442             sub _graceful_restart {
3443 0     0   0 my ($self) = @_;
3444              
3445 0 0       0 return if $self->{shutting_down};
3446              
3447 0         0 $self->_log(info => "Received HUP, performing graceful restart");
3448              
3449             # Signal all current workers to shutdown
3450             # watch_process callbacks will respawn them
3451 0   0     0 my $timeout = $self->{shutdown_timeout} // 30;
3452 0         0 for my $pid (keys %{$self->{worker_pids}}) {
  0         0  
3453 0         0 kill 'TERM', $pid;
3454              
3455             # Escalate to SIGKILL if worker doesn't exit within shutdown_timeout
3456 0         0 weaken(my $weak_self = $self);
3457             $self->{_restart_kill_timers}{$pid} = $self->loop->watch_time(
3458             after => $timeout,
3459             code => sub {
3460 0 0   0   0 return unless $weak_self;
3461 0 0       0 if (exists $weak_self->{worker_pids}{$pid}) {
3462 0         0 $weak_self->_log(warn =>
3463             "Worker $pid did not exit after ${timeout}s during restart, sending SIGKILL");
3464 0         0 kill 'KILL', $pid;
3465             }
3466             },
3467 0         0 );
3468             }
3469             }
3470              
3471             # Hot restart: fork+exec a new master that inherits listen sockets via PAGI_REUSE
3472             sub _hot_restart {
3473 0     0   0 my ($self) = @_;
3474              
3475 0 0       0 if ($self->{_hot_restart_in_progress}) {
3476 0         0 $self->_log(warn => "Hot restart already in progress, ignoring USR2");
3477 0         0 return;
3478             }
3479              
3480 0 0       0 if ($self->{shutting_down}) {
3481 0         0 $self->_log(warn => "Server is shutting down, ignoring USR2");
3482 0         0 return;
3483             }
3484              
3485 0         0 $self->{_hot_restart_in_progress} = 1;
3486 0         0 $self->_log(info => "Received USR2, starting hot restart");
3487              
3488             # Store our PID so the new master can signal us when ready
3489 0         0 $ENV{PAGI_MASTER_PID} = $$;
3490              
3491             # Fork and exec a new master process
3492 0         0 my $pid = fork();
3493              
3494 0 0       0 if (!defined $pid) {
3495 0         0 $self->_log(error => "Hot restart fork failed: $!");
3496 0         0 $self->{_hot_restart_in_progress} = 0;
3497 0         0 delete $ENV{PAGI_MASTER_PID};
3498 0         0 return;
3499             }
3500              
3501 0 0       0 if ($pid == 0) {
3502             # Child: exec new master
3503             my @args = defined $ENV{PAGI_ARGV}
3504             ? split(/\0/, $ENV{PAGI_ARGV})
3505 0 0       0 : ();
3506             exec($^X, $0, @args)
3507 0 0       0 or do {
3508 0         0 warn "Hot restart exec failed: $!\n";
3509 0         0 POSIX::_exit(1);
3510             };
3511             }
3512              
3513             # Parent: log and continue running
3514 0         0 $self->_log(info => "Hot restart: new master spawned as PID $pid");
3515             }
3516              
3517             # Increase worker pool by 1
3518             sub _increase_workers {
3519 0     0   0 my ($self) = @_;
3520              
3521 0 0       0 return if $self->{shutting_down};
3522              
3523 0         0 my $current = scalar keys %{$self->{worker_pids}};
  0         0  
3524 0         0 my $new_worker_num = $current + 1;
3525              
3526 0         0 $self->_log(info => "Received TTIN, spawning worker $new_worker_num (total: $new_worker_num)");
3527 0         0 $self->_spawn_worker($self->{_listen_entries}, $new_worker_num);
3528             }
3529              
3530             # Decrease worker pool by 1
3531             sub _decrease_workers {
3532 0     0   0 my ($self) = @_;
3533              
3534 0 0       0 return if $self->{shutting_down};
3535              
3536 0         0 my @pids = keys %{$self->{worker_pids}};
  0         0  
3537 0 0       0 return unless @pids > 1; # Keep at least 1 worker
3538              
3539 0         0 my $victim_pid = $pids[-1]; # Kill most recent
3540 0         0 my $remaining = scalar(@pids) - 1;
3541              
3542 0         0 $self->_log(info => "Received TTOU, killing worker (remaining: $remaining)");
3543              
3544             # Mark as "don't respawn" by setting a flag before killing
3545 0         0 $self->{_dont_respawn}{$victim_pid} = 1;
3546 0         0 kill 'TERM', $victim_pid;
3547             }
3548              
3549             sub _spawn_worker {
3550 29     29   208 my ($self, $listen_entries, $worker_num) = @_;
3551              
3552 29         150 my $loop = $self->loop;
3553 29         209 weaken(my $weak_self = $self);
3554              
3555             # Create heartbeat pipe if enabled
3556 29         63 my ($hb_rd, $hb_wr);
3557 29 50 33     550 if ($self->{heartbeat_timeout} && $self->{heartbeat_timeout} > 0) {
3558 29 50       1608 pipe($hb_rd, $hb_wr) or die "Cannot create heartbeat pipe: $!";
3559             }
3560              
3561             # Set IGNORE before fork - child inherits it. IO::Async only resets
3562             # CODE refs, so 'IGNORE' (a string) survives. Child must NOT call
3563             # watch_signal(INT) or it will overwrite the IGNORE.
3564 29         300 my $old_sigint = $SIG{INT};
3565 29         461 $SIG{INT} = 'IGNORE' unless WIN32;
3566              
3567             my $pid = $loop->fork(
3568             code => sub {
3569 11 50   11   39841 close($hb_rd) if $hb_rd;
3570 11         834 $self->_run_as_worker($listen_entries, $worker_num, $hb_wr);
3571 0         0 return 0;
3572             },
3573 29         1504 );
3574              
3575             # Restore parent's SIGINT handler
3576 18         55594 $SIG{INT} = $old_sigint unless WIN32;
3577              
3578 18 50       726 die "Fork failed" unless defined $pid;
3579              
3580             # Parent — close write end, set read end non-blocking
3581 18 50       484 if ($hb_wr) {
3582 18         1132 close($hb_wr);
3583 18         1367 $hb_rd->blocking(0);
3584             }
3585              
3586             # Parent - track the worker
3587 18         2060 $self->{worker_pids}{$pid} = {
3588             worker_num => $worker_num,
3589             started => time(),
3590             heartbeat_rd => $hb_rd,
3591             last_heartbeat => time(),
3592             };
3593              
3594             # Use watch_process to handle worker exit (replaces manual SIGCHLD handling)
3595             $loop->watch_process($pid => sub {
3596 9     9   14245024 my ($exit_pid, $exitcode) = @_;
3597 9 50       72 return unless $weak_self;
3598              
3599             # Close heartbeat pipe read end
3600 9 50       118 if (my $info = $weak_self->{worker_pids}{$exit_pid}) {
3601 9 50       284 close($info->{heartbeat_rd}) if $info->{heartbeat_rd};
3602             }
3603              
3604             # Remove from tracking
3605 9         37 delete $weak_self->{worker_pids}{$exit_pid};
3606              
3607             # Cancel per-worker restart kill timer if one exists
3608 9 50       199 if (my $timer_id = delete $weak_self->{_restart_kill_timers}{$exit_pid}) {
3609 0         0 $loop->unwatch_time($timer_id);
3610             }
3611              
3612             # Check exit code: exit(2) = startup failure, don't respawn
3613 9         54 my $exit_code = $exitcode >> 8;
3614 9 100 33     91 if ($exit_code == 2) {
    50          
3615 2         220 $weak_self->_log(warn => "Worker $worker_num startup failed, not respawning");
3616             # Don't respawn - startup failure would just repeat
3617             }
3618             # Respawn if still running and not shutting down
3619             elsif ($weak_self->{running} && !$weak_self->{shutting_down}) {
3620             # Don't respawn if this was a TTOU reduction
3621 0 0       0 unless (delete $weak_self->{_dont_respawn}{$exit_pid}) {
3622 0         0 $weak_self->_spawn_worker($listen_entries, $worker_num);
3623             }
3624             }
3625              
3626             # Check if all workers have exited.
3627 9 100       28 if (!keys %{$weak_self->{worker_pids}}) {
  9         115  
3628 5 100       40 if ($weak_self->{shutting_down}) {
3629             # Normal shutdown: stop once the last worker is gone.
3630 4 50       41 if ($weak_self->{_shutdown_kill_timer}) {
3631 4         49 $loop->unwatch_time($weak_self->{_shutdown_kill_timer});
3632 4         171 delete $weak_self->{_shutdown_kill_timer};
3633             }
3634 4         88 $loop->stop;
3635             }
3636             else {
3637             # Not shutting down, yet no workers remain and none were
3638             # respawned -- e.g. every worker failed lifespan startup (exit 2).
3639             # The master would otherwise sit holding the listening socket with
3640             # zero workers serving and never exit. Like uvicorn (#1115/#1177),
3641             # stop and exit non-zero so a supervisor detects the failure and
3642             # restarts us.
3643 1         7 $weak_self->_log(error =>
3644             "All workers have exited and none were respawned; shutting down");
3645 1         4 $weak_self->{_workers_failed} = 1;
3646 1         14 $loop->stop;
3647             }
3648             }
3649 18         2888 });
3650              
3651 18         46307 return $pid;
3652             }
3653              
3654             sub _run_as_worker {
3655 11     11   458 my ($self, $listen_entries, $worker_num, $heartbeat_wr) = @_;
3656              
3657             # Note: $ONE_TRUE_LOOP already cleared by $loop->fork(), so this creates a fresh loop
3658             # Note: $SIG{INT} = 'IGNORE' inherited from parent - do NOT call watch_signal(INT)
3659             # or it will overwrite the IGNORE with a CODE ref!
3660 11         853 my $loop = IO::Async::Loop->new;
3661              
3662             # In reuseport mode, each worker creates its own TCP listening socket
3663 11         15951 my $reuseport = $self->{reuseport};
3664 11         331 for my $entry (@$listen_entries) {
3665 11         270 my $spec = $entry->{spec};
3666 11 0 33     441 if (!$entry->{socket} && $reuseport && $spec->{type} eq 'tcp') {
      33        
3667             $entry->{socket} = IO::Socket::INET->new(
3668             LocalAddr => $spec->{host},
3669             LocalPort => $spec->{bound_port},
3670             Proto => 'tcp',
3671             Listen => $self->{listener_backlog},
3672 0 0       0 ReuseAddr => 1,
3673             ReusePort => 1,
3674             Blocking => 0,
3675             ) or die "Worker $worker_num: Cannot create listening socket: $!";
3676             }
3677             }
3678              
3679             # Build listener specs for the worker server constructor
3680 11         134 my @listen_specs;
3681 11         258 for my $entry (@$listen_entries) {
3682 11         168 my $s = $entry->{spec};
3683 11 50       316 if ($s->{type} eq 'unix') {
3684 0 0       0 push @listen_specs, { socket => $s->{path}, (defined $s->{socket_mode} ? (socket_mode => $s->{socket_mode}) : ()) };
3685             } else {
3686 11 100       648 push @listen_specs, { host => $s->{host}, port => $s->{port}, ($s->{ssl} ? (ssl => $s->{ssl}) : ()) };
3687             }
3688             }
3689              
3690             # Create a fresh server instance for this worker (single-worker mode)
3691             my $worker_server = PAGI::Server->new(
3692             app => $self->{app},
3693             listen => \@listen_specs,
3694             ssl => $self->{ssl},
3695             http2 => $self->{http2},
3696             extensions => $self->{extensions},
3697             access_log => $self->{access_log},
3698             log_level => $self->{log_level},
3699             quiet => 1, # Workers should be quiet
3700             timeout => $self->{timeout},
3701             max_header_size => $self->{max_header_size},
3702             max_header_count => $self->{max_header_count},
3703             max_body_size => $self->{max_body_size},
3704             max_requests => $self->{max_requests},
3705             shutdown_timeout => $self->{shutdown_timeout},
3706             request_timeout => $self->{request_timeout},
3707             ws_idle_timeout => $self->{ws_idle_timeout},
3708             sse_idle_timeout => $self->{sse_idle_timeout},
3709             sync_file_threshold => $self->{sync_file_threshold},
3710             max_receive_queue => $self->{max_receive_queue},
3711             max_ws_frame_size => $self->{max_ws_frame_size},
3712             write_high_watermark => $self->{write_high_watermark},
3713             write_low_watermark => $self->{write_low_watermark},
3714 11         2462 workers => 0, # Single-worker mode in worker process
3715             );
3716 11         360 $worker_server->{is_worker} = 1;
3717 11         133 $worker_server->{worker_num} = $worker_num; # Store for lifespan scope
3718 11         214 $worker_server->{_request_count} = 0; # Track requests handled
3719              
3720             # Set bound_port from first TCP listener's socket
3721 11         159 for my $entry (@$listen_entries) {
3722 11 50 33     1954 if ($entry->{spec}{type} eq 'tcp' && $entry->{socket} && $entry->{socket}->can('sockport')) {
      33        
3723 11         426 $worker_server->{bound_port} = $entry->{socket}->sockport;
3724 11         1359 last;
3725             }
3726             }
3727              
3728 11         679 $loop->add($worker_server);
3729              
3730             # Build SSL config for this worker (each worker gets its own SSL context post-fork)
3731 11         1460 my $ssl_params = $worker_server->_build_ssl_config;
3732              
3733             # Set up graceful shutdown on SIGTERM using IO::Async's signal watching
3734             # (raw $SIG handlers don't work reliably when the loop is running)
3735             # Note: Windows doesn't support Unix signals, so this is skipped there
3736             # Note: We do NOT set up watch_signal(INT) here - workers inherit $SIG{INT}='IGNORE'
3737             # from parent, so they ignore SIGINT (including Ctrl-C). Parent sends SIGTERM.
3738 11         75 unless (WIN32) {
3739 11         121 my $shutdown_triggered = 0;
3740             $loop->watch_signal(TERM => sub {
3741 9 50   9   9116807 return if $shutdown_triggered;
3742 9         37 $shutdown_triggered = 1;
3743             $worker_server->adopt_future(
3744             $worker_server->shutdown->on_done(sub {
3745 9         773 $loop->stop;
3746             })->on_fail(sub {
3747 0         0 my ($error) = @_;
3748 0         0 $worker_server->_log(error => "Worker shutdown error: $error");
3749 0         0 $loop->stop; # Still stop even on error
3750             })
3751 9         214 );
3752 11         656 });
3753             }
3754              
3755             # Run lifespan startup using a proper async wrapper
3756 11         16979 my $startup_done = 0;
3757 11         59 my $startup_error;
3758              
3759 11     11   60 my $startup_future = (async sub {
3760 11         61 eval {
3761 11         217 my $startup_result = await $worker_server->_run_lifespan_startup;
3762 11 100       1030 if (!$startup_result->{success}) {
3763 2   50     27 $startup_error = $startup_result->{message} // 'Lifespan startup failed';
3764             }
3765             };
3766 11 50       105 if ($@) {
3767 0         0 $startup_error = $@;
3768             }
3769 11         22 $startup_done = 1;
3770 11 100       157 $loop->stop if $startup_error; # Stop loop on error
3771 11         374 })->();
3772              
3773             # Use adopt_future instead of retain
3774 11         361 $worker_server->adopt_future($startup_future);
3775              
3776             # Run the loop briefly to let async startup complete
3777 11         836 $loop->loop_once while !$startup_done;
3778              
3779 11 100       91 if ($startup_error) {
3780 2         86 $self->_log(error => "Worker $worker_num ($$): startup failed: $startup_error");
3781 2         9 for my $entry (@$listen_entries) {
3782 2 50       39 close($entry->{socket}) if $entry->{socket};
3783             }
3784 2         822 exit(2); # Exit code 2 = startup failure (don't respawn)
3785             }
3786              
3787             # Create IO::Async::Listener for each inherited socket
3788 9         725 weaken(my $weak_server = $worker_server);
3789              
3790 9         86 for my $entry (@$listen_entries) {
3791 9 50       49 next unless $entry->{socket};
3792 9         75 my $spec = $entry->{spec};
3793              
3794             # Build SSL config for TCP listeners if needed
3795 9   66     141 my $use_ssl = ($ssl_params && $spec->{type} eq 'tcp');
3796              
3797             my $listener = IO::Async::Listener->new(
3798             handle => $entry->{socket},
3799             on_stream => sub {
3800 5     5   4405938 my ($listener, $stream) = @_;
3801 5 50       377 return unless $weak_server;
3802              
3803 5 100       26 if ($use_ssl) {
3804             $loop->SSL_upgrade(
3805             handle => $stream,
3806             SSL_server => 1,
3807             SSL_reuse_ctx => $worker_server->{_ssl_ctx},
3808             )->on_done(sub {
3809 3 50       152357 $weak_server->_on_connection($stream, $spec) if $weak_server;
3810             })->on_fail(sub {
3811 0         0 my ($failure) = @_;
3812 0 0       0 $weak_server->_log(debug => "SSL handshake failed: $failure")
3813             if $weak_server;
3814 3         109 });
3815             } else {
3816 2         46 $weak_server->_on_connection($stream, $spec);
3817             }
3818             },
3819 9         500 );
3820              
3821 9         3188 $worker_server->add_child($listener);
3822              
3823             # Configure accept error handler - try but ignore if it fails
3824 9         2473 eval {
3825             $listener->configure(
3826             on_accept_error => sub {
3827 0     0   0 my ($listener, $error) = @_;
3828 0 0       0 return unless $weak_server;
3829 0         0 $weak_server->_on_accept_error($error);
3830             },
3831 9         231 );
3832             };
3833             # Silently ignore configuration errors in workers
3834             }
3835              
3836             # Set up heartbeat writer: periodically signal liveness to parent
3837 9 50       6196 if ($heartbeat_wr) {
3838 9   50     121 my $interval = ($self->{heartbeat_timeout} || 50) / 5;
3839             my $hb_timer = IO::Async::Timer::Periodic->new(
3840             interval => $interval,
3841             on_tick => sub {
3842 0     0   0 syswrite($heartbeat_wr, "\x00", 1);
3843             },
3844 9         748 );
3845 9         1622 $worker_server->add_child($hb_timer);
3846 9         1068 $hb_timer->start;
3847             }
3848              
3849 9         1633 $worker_server->{running} = 1;
3850              
3851             # Run the event loop
3852 9         194 $loop->run;
3853              
3854             # Clean up FDs before exit
3855 9 50       1886 close($heartbeat_wr) if $heartbeat_wr;
3856 9         102 for my $entry (@$listen_entries) {
3857 9 50       319 close($entry->{socket}) if $entry->{socket};
3858             }
3859 9         2096 exit(0);
3860             }
3861              
3862             sub _on_connection {
3863 205     205   650 my ($self, $stream, $listener_spec) = @_;
3864              
3865 205         586 weaken(my $weak_self = $self);
3866              
3867             # Check if we're at capacity
3868 205         1016 my $max = $self->effective_max_connections;
3869 205 100       744 if ($self->connection_count >= $max) {
3870             # Over capacity - send 503 and close
3871 2         7 $self->_send_503_and_close($stream);
3872 2         13 return;
3873             }
3874              
3875             # Detect ALPN-negotiated protocol from TLS handle
3876 203         389 my $alpn_protocol;
3877 203 100 100     924 if ($self->{tls_enabled} && $self->{http2_enabled}) {
3878 1   33     14 my $handle = $stream->write_handle // $stream->read_handle;
3879 1 50 33     48 if ($handle && $handle->can('alpn_selected')) {
3880 1         4 $alpn_protocol = eval { $handle->alpn_selected };
  1         13  
3881             }
3882             }
3883              
3884             my $conn = PAGI::Server::Connection->new(
3885             stream => $stream,
3886             app => $self->{app},
3887             protocol => $self->{protocol},
3888             server => $self,
3889             extensions => $self->{extensions},
3890             state => $self->{state},
3891             tls_enabled => $self->{tls_enabled} // 0,
3892             timeout => $self->{timeout},
3893             request_timeout => $self->{request_timeout},
3894             ws_idle_timeout => $self->{ws_idle_timeout},
3895             sse_idle_timeout => $self->{sse_idle_timeout},
3896             max_body_size => $self->{max_body_size},
3897             access_log => $self->{access_log},
3898             _access_log_formatter => $self->{_access_log_formatter},
3899             max_receive_queue => $self->{max_receive_queue},
3900             max_ws_frame_size => $self->{max_ws_frame_size},
3901             sync_file_threshold => $self->{sync_file_threshold},
3902             validate_events => $self->{validate_events},
3903             write_high_watermark => $self->{write_high_watermark},
3904             write_low_watermark => $self->{write_low_watermark},
3905             transport_type => ($listener_spec && $listener_spec->{type}) // 'tcp',
3906             transport_path => ($listener_spec ? $listener_spec->{path} : undef),
3907             ($self->{http2_enabled} ? (
3908             h2_protocol => $self->{http2_protocol},
3909             alpn_protocol => $alpn_protocol,
3910 203 50 100     7311 h2c_enabled => $self->{h2c_enabled} // 0,
    100 33        
      50        
      50        
3911             ) : ()),
3912             );
3913              
3914             # Track the connection (O(1) hash insert)
3915 203         1115 $self->{connections}{refaddr($conn)} = $conn;
3916              
3917             # Configure stream with callbacks BEFORE adding to loop
3918 203         1138 $conn->start;
3919              
3920             # Add stream to the loop so it can read/write
3921 203         13704 $self->add_child($stream);
3922             }
3923              
3924             sub _send_503_and_close {
3925 2     2   5 my ($self, $stream) = @_;
3926              
3927 2         3 my $body = "503 Service Unavailable - Server at capacity\r\n";
3928 2         9 my $response = join("\r\n",
3929             "HTTP/1.1 503 Service Unavailable",
3930             "Content-Type: text/plain",
3931             "Content-Length: " . length($body),
3932             "Connection: close",
3933             "Retry-After: 5",
3934             "",
3935             $body
3936             );
3937              
3938             # Configure stream with minimal on_read handler (required by IO::Async)
3939             $stream->configure(
3940 1     1   445 on_read => sub { 0 }, # Ignore any incoming data
3941 2         8 );
3942              
3943             # Add stream to loop so it can write
3944 2         98 $self->add_child($stream);
3945              
3946             # Write response and close
3947 2         325 $stream->write($response);
3948 2         480 $stream->close_when_empty;
3949              
3950 2         19 $self->_log(warn => "Connection rejected: at capacity (" . $self->connection_count . "/" . $self->effective_max_connections . ")");
3951             }
3952              
3953             sub _on_accept_error {
3954 0     0   0 my ($self, $error) = @_;
3955              
3956             # EMFILE = "Too many open files" - we're out of file descriptors
3957             # ENFILE = System-wide FD limit reached
3958 0 0       0 if ($error =~ /Too many open files|EMFILE|ENFILE/i) {
3959             # Only log the first EMFILE in a burst (when we're not already paused)
3960 0 0       0 unless ($self->{_accept_paused}) {
3961 0         0 $self->_log(warn => "Accept error (FD exhaustion): $error - pausing accept for 100ms");
3962             }
3963              
3964             # Pause accepting for a short time to let connections drain
3965 0         0 $self->_pause_accepting(0.1);
3966             }
3967             else {
3968             # Log other accept errors but don't crash
3969 0         0 $self->_log(error => "Accept error: $error");
3970             }
3971             }
3972              
3973             sub _pause_accepting {
3974 0     0   0 my ($self, $duration) = @_;
3975              
3976 0 0       0 return if $self->{_accept_paused};
3977 0         0 $self->{_accept_paused} = 1;
3978              
3979             # Cancel any existing timer before creating new one
3980 0 0       0 if ($self->{_accept_pause_timer}) {
3981 0         0 $self->loop->unwatch_time($self->{_accept_pause_timer});
3982 0         0 delete $self->{_accept_pause_timer};
3983             }
3984              
3985             # Temporarily disable all listeners
3986 0   0     0 for my $entry (@{$self->{_listen_entries} // []}) {
  0         0  
3987 0         0 my $listener = $entry->{listener};
3988 0 0 0     0 if ($listener && $listener->read_handle) {
3989 0         0 $listener->want_readready(0);
3990             }
3991             }
3992             # Backward compat: also pause $self->{listener} if not in entries
3993 0 0 0     0 if ($self->{listener} && !$self->{_listen_entries}) {
3994 0 0       0 $self->{listener}->want_readready(0) if $self->{listener}->read_handle;
3995             }
3996              
3997             # Re-enable after duration
3998 0         0 weaken(my $weak_self = $self);
3999             my $timer_id = $self->loop->watch_time(after => $duration, code => sub {
4000 0 0 0 0   0 return unless $weak_self && $weak_self->{running};
4001 0         0 $weak_self->{_accept_paused} = 0;
4002 0         0 delete $weak_self->{_accept_pause_timer};
4003              
4004             # Resume all listeners
4005 0   0     0 for my $entry (@{$weak_self->{_listen_entries} // []}) {
  0         0  
4006 0         0 my $listener = $entry->{listener};
4007 0 0 0     0 if ($listener && $listener->read_handle) {
4008 0         0 $listener->want_readready(1);
4009             }
4010             }
4011             # Backward compat
4012 0 0 0     0 if ($weak_self->{listener} && !$weak_self->{_listen_entries}) {
4013 0 0       0 $weak_self->{listener}->want_readready(1) if $weak_self->{listener}->read_handle;
4014             }
4015              
4016 0         0 $weak_self->_log(debug => "Accept resumed after FD exhaustion pause");
4017 0         0 });
4018              
4019 0         0 $self->{_accept_pause_timer} = $timer_id;
4020             }
4021              
4022             # Called when a request completes (for max_requests tracking)
4023             sub _on_request_complete {
4024 237     237   1004 my ($self) = @_;
4025              
4026 237 100       1016 return unless $self->{is_worker};
4027 7 100 66     67 return unless $self->{max_requests} && $self->{max_requests} > 0;
4028              
4029 2         4 $self->{_request_count}++;
4030              
4031 2 50       6 if ($self->{_request_count} >= $self->{max_requests}) {
4032 0 0       0 return if $self->{_max_requests_shutdown_triggered}; # Prevent duplicate shutdowns
4033 0         0 $self->{_max_requests_shutdown_triggered} = 1;
4034 0         0 $self->_log(info => "Worker $$: reached max_requests ($self->{max_requests}), shutting down");
4035             # Initiate graceful shutdown (finish current connections, then exit)
4036             $self->adopt_future(
4037             $self->shutdown->on_done(sub {
4038 0     0   0 $self->loop->stop;
4039             })->on_fail(sub {
4040 0     0   0 my ($error) = @_;
4041 0         0 $self->_log(error => "Worker $$: max_requests shutdown error: $error");
4042 0         0 $self->loop->stop; # Still stop even on error
4043             })
4044 0         0 );
4045             }
4046             }
4047              
4048             # Lifespan Protocol Implementation
4049              
4050 207     207   391 async sub _run_lifespan_startup {
4051 207         521 my ($self) = @_;
4052              
4053             # lifespan_mode 'off': skip the protocol entirely -- never invoke the app
4054             # with a lifespan scope.
4055             return { success => 1, lifespan_supported => 0 }
4056 207 100 50     1337 if ($self->{lifespan_mode} // 'auto') eq 'off';
4057              
4058             # Create lifespan scope
4059             my $scope = {
4060             type => 'lifespan',
4061             pagi => {
4062             version => '0.3',
4063             spec_version => '0.3',
4064             is_worker => $self->{is_worker} // 0,
4065             worker_num => $self->{worker_num}, # undef for single-worker, 1-N for multi-worker
4066             },
4067             state => $self->{state}, # App can populate this
4068 206   50     2198 };
4069              
4070             # Create receive/send for lifespan protocol
4071 206         412 my @send_queue;
4072             my $receive_pending;
4073 206         2249 my $startup_complete = Future->new;
4074              
4075             # $receive for the app - returns events from the server
4076             my $receive = sub {
4077 309 100   309   5732 if (@send_queue) {
4078 158         1156 return Future->done(shift @send_queue);
4079             }
4080 151         386 $receive_pending = Future->new;
4081 151         776 return $receive_pending;
4082 206         2721 };
4083              
4084             # $send for the app - handles app responses
4085 302     302   21559 my $send = async sub {
4086 302         522 my ($event) = @_;
4087 302   50     835 my $type = $event->{type} // '';
4088              
4089 302 100       1237 if ($type eq 'lifespan.startup.complete') {
    100          
    50          
    0          
4090 152         599 $startup_complete->done({ success => 1 });
4091             }
4092             elsif ($type eq 'lifespan.startup.failed') {
4093 4   50     56 my $message = $event->{message} // '';
4094 4         155 $startup_complete->done({ success => 0, message => $message });
4095             }
4096             elsif ($type eq 'lifespan.shutdown.complete') {
4097             # Store for shutdown handling
4098 146         504 $self->{shutdown_complete} = 1;
4099 146 50       412 if ($self->{shutdown_pending}) {
4100 146         1057 $self->{shutdown_pending}->done({ success => 1 });
4101             }
4102             }
4103             elsif ($type eq 'lifespan.shutdown.failed') {
4104 0   0     0 my $message = $event->{message} // '';
4105 0         0 $self->{shutdown_complete} = 1;
4106 0 0       0 if ($self->{shutdown_pending}) {
4107 0         0 $self->{shutdown_pending}->done({ success => 0, message => $message });
4108             }
4109             }
4110              
4111 302         10514 return;
4112 206         1395 };
4113              
4114             # Queue the startup event
4115 206         785 push @send_queue, { type => 'lifespan.startup' };
4116 206 50 33     710 if ($receive_pending && !$receive_pending->is_ready) {
4117 0         0 my $f = $receive_pending;
4118 0         0 $receive_pending = undef;
4119 0         0 $f->done(shift @send_queue);
4120             }
4121              
4122             # Store lifespan handlers for shutdown
4123 206         753 $self->{lifespan_receive} = $receive;
4124 206         515 $self->{lifespan_send} = $send;
4125 206         484 $self->{lifespan_send_queue} = \@send_queue;
4126 206         582 $self->{lifespan_receive_pending} = \$receive_pending;
4127              
4128             # Start the lifespan app handler
4129             # We run it in the background and wait for startup.complete
4130 206     206   346 my $app_future = (async sub {
4131 206         365 my $ok = eval {
4132 206         1013 await $self->{app}->($scope, $receive, $send);
4133 151         13633 1;
4134             };
4135 201 100       51145 my $err = $ok ? undef : $@;
4136              
4137 201 100       581 if (!$startup_complete->is_ready) {
    100          
4138             # The app finished -- by returning cleanly OR by throwing -- without
4139             # ever signalling startup.
4140 49         200 my $detail;
4141 49 100       113 if (defined $err) { ($detail = $err) =~ s/\s+\z//; }
  48         374  
4142              
4143 49 100 50     206 if (($self->{lifespan_mode} // 'auto') eq 'on') {
4144             # 'on' requires lifespan: a decline (a raise, or a clean return
4145             # that never signalled startup) is a fatal startup failure rather
4146             # than "continue without it".
4147 1 50       4 my $why = defined $detail
4148             ? "the application raised: $detail"
4149             : "the application returned without signalling startup";
4150 1         7 $startup_complete->done({ success => 0,
4151             message => "lifespan_mode is 'on' but $why" });
4152             }
4153             else {
4154             # auto: continue without lifespan. This matches Uvicorn/Hypercorn
4155             # "auto"; an app may decline by returning on the lifespan scope or
4156             # by dying on it. Include the raised message so a genuine startup
4157             # failure masquerading as a decline is still visible.
4158 48         75 my $msg = "Lifespan not supported, continuing without it";
4159 48 100       141 $msg .= " (application raised: $detail)" if defined $detail;
4160 48         177 $self->_log(info => $msg);
4161 48         228 $startup_complete->done({ success => 1, lifespan_supported => 0 });
4162             }
4163             }
4164             elsif (defined $err) {
4165             # The app's long-lived lifespan task failed AFTER startup completed.
4166             # Surface it at error level rather than swallowing it silently, so a
4167             # crashed background task is visible to operators.
4168 2         54 $self->_log(error => "Lifespan app failed after startup: $err");
4169             }
4170             # else: the app returned cleanly after startup completed -- the handler
4171             # simply exited early; nothing to do.
4172 206         2579 })->();
4173              
4174             # Keep the app future so we can trigger shutdown later
4175 206         22292 $self->{lifespan_app_future} = $app_future;
4176             # Use adopt_future instead of retain for proper error handling
4177 206         1178 $self->adopt_future($app_future);
4178              
4179             # Wait for startup complete, bounded by lifespan_startup_timeout so a hung
4180             # lifespan handler (one that neither signals startup nor returns) cannot
4181             # block the server forever. 0 disables the bound. without_cancel keeps
4182             # $startup_complete pending on timeout, so a late signal cannot croak.
4183 206   50     12710 my $startup_timeout = $self->{lifespan_startup_timeout} // 30;
4184 206         362 my $result;
4185 206 50       557 if ($startup_timeout > 0) {
4186             my $timeout_f = $self->loop->delay_future(after => $startup_timeout)
4187 206     1   639 ->then(sub { Future->done(undef) });
  1         301100  
4188 206         262261 $result = await Future->wait_any($startup_complete->without_cancel, $timeout_f);
4189 206 100       72671 if (!defined $result) {
4190 1         12 my $message = "Lifespan startup timed out after ${startup_timeout}s";
4191 1         6 $self->_log(error => $message);
4192 1         14 return { success => 0, message => $message };
4193             }
4194             }
4195             else {
4196 0         0 $result = await $startup_complete;
4197             }
4198              
4199             # Track if lifespan is supported
4200 205   100     1180 $self->{lifespan_supported} = $result->{lifespan_supported} // 1;
4201              
4202 205         1114 return $result;
4203             }
4204              
4205 196     196   361 async sub _run_lifespan_shutdown {
4206 196         444 my ($self) = @_;
4207              
4208             # If lifespan is not supported or no lifespan was started, just return success
4209 196 100       773 return { success => 1 } unless $self->{lifespan_supported};
4210 147 50       574 return { success => 1 } unless $self->{lifespan_send_queue};
4211              
4212 147         459 $self->{shutdown_pending} = $self->loop->new_future;
4213              
4214             # Queue the shutdown event
4215 147         5371 my $send_queue = $self->{lifespan_send_queue};
4216 147         348 my $receive_pending_ref = $self->{lifespan_receive_pending};
4217              
4218 147         561 push @$send_queue, { type => 'lifespan.shutdown' };
4219              
4220             # Trigger pending receive if waiting
4221 147 50 33     784 if ($$receive_pending_ref && !$$receive_pending_ref->is_ready) {
4222 147         1228 my $f = $$receive_pending_ref;
4223 147         261 $$receive_pending_ref = undef;
4224 147         388 $f->done(shift @$send_queue);
4225             }
4226              
4227             # Wait for shutdown complete (with timeout to prevent hanging)
4228 147   50     14489 my $timeout = $self->{shutdown_timeout} // 30;
4229 147         453 my $timeout_f = $self->loop->delay_future(after => $timeout);
4230              
4231 147         16501 my $result = await Future->wait_any($self->{shutdown_pending}, $timeout_f);
4232              
4233             # If timeout won, return failure
4234 147 50 33     2029650 if ($timeout_f->is_ready && !$self->{shutdown_pending}->is_ready) {
4235 0         0 return { success => 0, message => "Lifespan shutdown timed out after ${timeout}s" };
4236             }
4237              
4238 147   100     2886 return $result // { success => 1 };
4239             }
4240              
4241 197     197 1 12047289 async sub shutdown {
4242 197         601 my ($self) = @_;
4243              
4244 197 50       971 return unless $self->{running};
4245 197         481 $self->{running} = 0;
4246 197         610 $self->{shutting_down} = 1;
4247              
4248             # Cancel accept pause timer if active
4249 197 50       721 if ($self->{_accept_pause_timer}) {
4250 0         0 $self->loop->unwatch_time($self->{_accept_pause_timer});
4251 0         0 delete $self->{_accept_pause_timer};
4252 0         0 $self->{_accept_paused} = 0;
4253             }
4254              
4255             # Stop accepting new connections on all listeners
4256 197   100     336 for my $entry (@{$self->{_listen_entries} // []}) {
  197         1170  
4257 189         594 eval { $self->remove_child($entry->{listener}) };
  189         1210  
4258             }
4259 197         29607 $self->{listener} = undef;
4260              
4261             # Clean up PAGI_REUSE entries for sockets we created (not inherited)
4262 197   100     346 for my $entry (@{$self->{_listen_entries} // []}) {
  197         1179  
4263 189         519 my $key = $entry->{spec}{_reuse_key};
4264 189 50 66     1652 if ($key && !$self->{_hot_restart_in_progress} && defined $ENV{PAGI_REUSE}) {
      66        
4265 186         8215 $ENV{PAGI_REUSE} =~ s/(?:^|,)\Q$key\E//;
4266 186 50       1072 $ENV{PAGI_REUSE} =~ s/^,// if defined $ENV{PAGI_REUSE};
4267             }
4268             }
4269              
4270             # Clean up Unix socket files (only those we created, not inherited)
4271 197   100     366 for my $entry (@{$self->{_listen_entries} // []}) {
  197         866  
4272 189 100 100     1044 if ($entry->{spec}{type} eq 'unix'
      66        
4273             && !$entry->{spec}{_inherited}
4274             && -e $entry->{spec}{path}) {
4275 7         338 unlink $entry->{spec}{path};
4276             }
4277             }
4278 197         9417 $self->{_listen_entries} = [];
4279              
4280             # Wait for active connections to drain (graceful shutdown)
4281 197         1322 await $self->_drain_connections;
4282              
4283             # Run lifespan shutdown
4284 196         8728 my $shutdown_result = await $self->_run_lifespan_shutdown;
4285              
4286 196 50       5623 if (!$shutdown_result->{success}) {
4287 0   0     0 my $message = $shutdown_result->{message} // 'Lifespan shutdown failed';
4288 0         0 $self->_log(warn => "PAGI Server shutdown warning: $message");
4289             }
4290              
4291 196         563 return $self;
4292             }
4293              
4294             # Wait for active connections to complete, with timeout
4295             # Uses event-driven approach: Connection._close() signals when last one closes
4296 197     197   395 async sub _drain_connections {
4297 197         475 my ($self) = @_;
4298              
4299 197   50     787 my $timeout = $self->{shutdown_timeout} // 30;
4300 197         729 my $loop = $self->loop;
4301              
4302             # First, close all idle connections immediately (not processing a request)
4303             # Keep-alive connections waiting for next request should be closed
4304 197         912 my @idle = grep { !$_->{handling_request} } values %{$self->{connections}};
  137         555  
  197         703  
4305 197         488 for my $conn (@idle) {
4306 127         1980 $conn->_handle_disconnect_and_close('server_shutdown');
4307             }
4308              
4309             # Also close long-lived connections (SSE, WebSocket) immediately
4310             # These never become "idle" so would wait for full timeout otherwise
4311 197 100       13285 my @longlived = grep { $_->{sse_mode} || $_->{websocket_mode} } values %{$self->{connections}};
  10         57  
  197         595  
4312 197         553 for my $conn (@longlived) {
4313 9         76 $conn->_handle_disconnect_and_close('server_shutdown');
4314             }
4315              
4316             # If all connections are now closed, we're done
4317 197 100       300 return if keys %{$self->{connections}} == 0;
  197         7016  
4318              
4319             # Create a Future that Connection._close() will resolve when last one closes
4320 1         4 $self->{drain_complete} = $loop->new_future;
4321              
4322             # Wait for either: all connections close OR timeout
4323 1         90 my $timeout_f = $loop->delay_future(after => $timeout);
4324              
4325 1         134 await Future->wait_any($self->{drain_complete}, $timeout_f);
4326              
4327             # Brief pause to let any final socket writes flush
4328             # (stream->write is async; data may still be in kernel buffer)
4329 1 50       312 await $loop->delay_future(after => 0.05) if keys %{$self->{connections}} == 0;
  1         17  
4330              
4331             # If timeout won (connections still remain), force close them
4332 0 0       0 if (keys %{$self->{connections}} > 0) {
  0         0  
4333 0         0 my $remaining = scalar keys %{$self->{connections}};
  0         0  
4334 0         0 $self->_log(warn => "Shutdown timeout: force-closing $remaining active connections");
4335              
4336 0         0 for my $conn (values %{$self->{connections}}) {
  0         0  
4337 0 0 0     0 $conn->_close if $conn && $conn->can('_close');
4338             }
4339             }
4340              
4341 0         0 delete $self->{drain_complete};
4342 0         0 return;
4343             }
4344              
4345             # --- Access log format compiler ---
4346              
4347             my %ACCESS_LOG_PRESETS = (
4348             clf => '%h - - [%t] "%m %U%q" %s %ds',
4349             combined => '%h - - [%t] "%r" %s %b "%{Referer}i" "%{User-Agent}i"',
4350             common => '%h - - [%t] "%r" %s %b',
4351             tiny => '%m %U%q %s %Dms',
4352             );
4353              
4354             sub _compile_access_log_format {
4355 401     401   18530 my ($class_or_self, $format) = @_;
4356              
4357             # Resolve preset names
4358 401 100       1734 if (exists $ACCESS_LOG_PRESETS{$format}) {
4359 367         1377 $format = $ACCESS_LOG_PRESETS{$format};
4360             }
4361              
4362             # Parse format string into a list of fragments (closures or literal strings)
4363 401         954 my @fragments;
4364 401         875 my $pos = 0;
4365 401         1117 my $len = length($format);
4366              
4367 401         1321 while ($pos < $len) {
4368 4811         6700 my $ch = substr($format, $pos, 1);
4369              
4370 4811 100       6841 if ($ch eq '%') {
4371 2602         3003 $pos++;
4372 2602 50       5380 last if $pos >= $len;
4373              
4374 2602         4249 my $next = substr($format, $pos, 1);
4375              
4376 2602 100       5628 if ($next eq '%') {
    100          
4377             # Literal percent
4378 1         2 push @fragments, '%';
4379 1         3 $pos++;
4380             }
4381             elsif ($next eq '{') {
4382             # Header extraction: %{Name}i
4383 13         23 my $end = index($format, '}', $pos);
4384 13 50       23 die "Unterminated %{...} in access log format\n" if $end < 0;
4385 13         25 my $header_name = substr($format, $pos + 1, $end - $pos - 1);
4386 13         16 $pos = $end + 1;
4387              
4388             # Must be followed by 'i' (request header)
4389 13 50 33     65 die "Expected 'i' after %{$header_name} in access log format\n"
4390             if $pos >= $len || substr($format, $pos, 1) ne 'i';
4391 13         14 $pos++;
4392              
4393 13         25 my $lc_name = lc($header_name);
4394             push @fragments, sub {
4395 11     11   16 my ($info) = @_;
4396 11         12 for my $h (@{$info->{request_headers}}) {
  11         22  
4397 24 100       96 return $h->[1] if lc($h->[0]) eq $lc_name;
4398             }
4399 2         15 return '-';
4400 13         56 };
4401             }
4402             else {
4403             # Simple atom
4404 2588         4960 my $atom = $next;
4405 2588         11008 $pos++;
4406              
4407 2588         4517 my $frag = _access_log_atom($atom);
4408 2587         5465 push @fragments, $frag;
4409             }
4410             }
4411             else {
4412             # Literal text: collect until next %
4413 2209         3303 my $next_pct = index($format, '%', $pos);
4414 2209 100       3167 if ($next_pct < 0) {
4415 367         1070 push @fragments, substr($format, $pos);
4416 367         860 $pos = $len;
4417             }
4418             else {
4419 1842         3944 push @fragments, substr($format, $pos, $next_pct - $pos);
4420 1842         2855 $pos = $next_pct;
4421             }
4422             }
4423             }
4424              
4425             # Build a single closure from fragments
4426             return sub {
4427 232     232   729 my ($info) = @_;
4428 232 100       526 return join('', map { ref $_ ? $_->($info) : $_ } @fragments);
  2619         20884  
4429 400         2296 };
4430             }
4431              
4432             sub _access_log_atom {
4433 2588     2588   4290 my ($atom) = @_;
4434              
4435             my %atoms = (
4436 200   50 200   833 h => sub { $_[0]->{client_ip} // '-' },
4437 1     1   9 l => sub { '-' },
4438 1     1   9 u => sub { '-' },
4439 198   50 198   637 t => sub { $_[0]->{timestamp} // '-' },
4440             r => sub {
4441 5     5   7 my $i = $_[0];
4442 5   50     11 my $uri = $i->{path} // '/';
4443 5         7 my $qs = $i->{query};
4444 5 100 66     21 $uri .= "?$qs" if defined $qs && length $qs;
4445 5   50     45 sprintf('%s %s HTTP/%s', $i->{method} // '-', $uri, $i->{http_version} // '1.1');
      50        
4446             },
4447 195   50 195   621 m => sub { $_[0]->{method} // '-' },
4448 195   50 195   659 U => sub { $_[0]->{path} // '/' },
4449             q => sub {
4450 197     197   390 my $qs = $_[0]->{query};
4451 197 100 100     1185 (defined $qs && length $qs) ? "?$qs" : '';
4452             },
4453 1   50 1   18 H => sub { 'HTTP/' . ($_[0]->{http_version} // '1.1') },
4454 205   50 205   604 s => sub { $_[0]->{status} // '-' },
4455             b => sub {
4456 10   50 10   26 my $size = $_[0]->{size} // 0;
4457 10 100       79 $size ? $size : '-';
4458             },
4459 2   50 2   30 B => sub { $_[0]->{size} // 0 },
4460 194   50 194   2077 d => sub { sprintf('%.3f', $_[0]->{duration} // 0) },
4461 4   50 4   23 D => sub { int(($_[0]->{duration} // 0) * 1_000_000) },
4462 2   50 2   36 T => sub { int($_[0]->{duration} // 0) },
4463 2588         45285 );
4464              
4465 2588 100       5151 if (my $frag = $atoms{$atom}) {
4466 2587         27853 return $frag;
4467             }
4468              
4469 1         27 die "Unknown access log format atom '%$atom'\n";
4470             }
4471              
4472             sub port {
4473 179     179 1 518921 my ($self) = @_;
4474              
4475 179   66     979 return $self->{bound_port} // $self->{port};
4476             }
4477              
4478             sub socket_path {
4479 4     4 1 84 my ($self) = @_;
4480 4   50     6 for my $listener (@{$self->{listeners} // []}) {
  4         12  
4481 4 100       18 return $listener->{path} if $listener->{type} eq 'unix';
4482             }
4483 2         10 return undef;
4484             }
4485              
4486             sub listeners {
4487 5     5 1 256 my ($self) = @_;
4488 5   50     50 return $self->{listeners} // [];
4489             }
4490              
4491             sub is_running {
4492 21     21 1 108618 my ($self) = @_;
4493              
4494 21 100       157 return $self->{running} ? 1 : 0;
4495             }
4496              
4497             sub connection_count {
4498 214     214 1 198593 my ($self) = @_;
4499              
4500 214         335 return scalar keys %{$self->{connections}};
  214         1001  
4501             }
4502              
4503             sub effective_max_connections {
4504 419     419 1 5376 my ($self) = @_;
4505              
4506             # If explicitly set, use that; otherwise default to 1000
4507             # (Same default as Mojolicious - simple, predictable, no platform-specific hacks)
4508             return $self->{max_connections} && $self->{max_connections} > 0
4509             ? $self->{max_connections}
4510 419 100 66     2087 : 1000;
4511             }
4512              
4513             1;
4514              
4515             __END__