Skip to content

Fixed bug GH-22981 (unix socket connect fails with EAGAIN when listen backlog is full) - #23669

Open
shoutoutuoadi325 wants to merge 2 commits into
php:masterfrom
shoutoutuoadi325:fix/GH-22981
Open

shoutoutuoadi325 wants to merge 2 commits into
php:masterfrom
shoutoutuoadi325:fix/GH-22981

Conversation

@shoutoutuoadi325

@shoutoutuoadi325 shoutoutuoadi325 commented Sep 12, 2026 •

Copy link
Copy Markdown

Fixes GH-22981.

php_network_connect_socket() runs the connect with the socket in
non-blocking mode so it can honour the timeout. The flip side is that
connect() to a unix domain socket returns EAGAIN as soon as the listen
backlog fills up, while a blocking connect would wait for a slot. Short
bursts of connections (PDO over a unix socket in the report) then fail
with "Resource temporarily unavailable".

Retry the connect while it keeps returning EAGAIN, bounded by the
connect timeout if one was given, matching what a blocking connect
does. Everything else (other errors, async connects, TCP) is unchanged.

gh22981.phpt runs a server with backlog=1 that starts accepting late:
connect #3 fails before the patch, all 20 succeed after. It also runs a
server that never accepts and expects the client to fail once its
connect timeout expires.

…ten backlog is full)

php_network_connect_socket() runs the connect in non-blocking mode so the
timeout can be honoured. On a unix domain socket, connect() then returns
EAGAIN as soon as the listen backlog is full, instead of waiting for a
slot like a blocking connect would. Retry while that is the case, bounded
by the connect timeout.
Copilot AI lite review requested due to automatic review settings September 12, 2026 07:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved issues remain in transient-error handling, timeout-bounded retries, and timeout regression coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes Unix-domain socket connection failures when the listen backlog is full by retrying transient connection errors.

Changes:

  • Adds bounded retry handling for Unix socket connections.
  • Adds GH-22981 regression coverage.
  • Documents the fix in NEWS.
File summaries
File Description
NEWS Documents the bug fix.
main/network.c Implements Unix socket connection retries.
ext/standard/tests/streams/gh22981.phpt Tests delayed backlog recovery.
Review details

Suppressed comments (2)

ext/standard/tests/streams/gh22981.phpt:64

  • Please add coverage for a backlog that remains full until the client timeout expires. This test only exercises eventual success—the server starts accepting after 200 ms and every client has a 5-second timeout—so it would not catch a retry loop that ignores or exceeds the supplied timeout.
for ($i = 0; $i < 20; $i++) {
    $conn = stream_socket_client('unix://' . $socketPath, $errno, $errstr, 5);

main/network.c:359

  • On platforms where EWOULDBLOCK differs from EAGAIN, a non-blocking UNIX-domain connect() can report EWOULDBLOCK for this transient full-backlog condition. This guard then falls through to the hard-error path, so the fix remains platform-dependent; use the existing PHP_IS_TRANSIENT_ERROR(error) macro, which covers both values.
		if (!asynchronous && error == EAGAIN && addr->sa_family == AF_UNIX) {
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread main/network.c
The pause between attempts is capped to the time the caller asked for,
so a very small (or zero) timeout is no longer overstretched by the
10ms slice. EWOULDBLOCK is now retried as well for the systems where
it differs from EAGAIN, and the test also covers a backlog that never
drains: the client has to fail once its connect timeout expires.
@anun333

anun333 commented Oct 29, 2026

Copy link
Copy Markdown

Quoted below: written with Claude, an LLM, as CONTRIBUTING.md asks us to mark. The builds and test runs behind it are on my machine; the commands to repeat them are under the fold.

This fixes the issue's case. With a listener on listen(1), debug builds of today's master (125ffe30167) with and without this PR, stream_socket_client("unix://…") one connect after another:

master this PR
server accepts one connection every 300 ms, 6 connects, timeout 3 s 2 ok, then 4 × Resource temporarily unavailable at once 6 ok, each after ≤ 0.3 s
backlog full, never accepted, timeout 2 s Resource temporarily unavailable at once Connection timed out after 2.0 s
the same through mysqli (MYSQLI_OPT_CONNECT_TIMEOUT 2) 2002 Resource temporarily unavailable at once 2002 Connection timed out after 2.0 s
CPU time while waiting 2 s – 2.00 s

The wait spins. php_pollfd_for(sockfd, 0, &slice) returns at once, because a socket whose connect never started reports POLLHUP; under strace, a run with one 1 s wait made about 6,850 connect() calls, each poll() returning revents=POLLHUP. So with a few workers waiting on a busy server, each holds a core for the whole timeout.

Sleeping the slice instead of polling keeps every result in the table and the waits at 0.01 s of CPU (patch under the fold; usleep() is also provided on Windows by win32/time.c). ext/standard/tests/streams and tests/network pass on all three builds (245 on master, 246 with the new test), and gh22981.phpt fails on master without the fix.

Since it's a bug fix: it applies cleanly to today's PHP-8.4 (303bb88f1c5), where the bug and the fix (and the spinning) are the same, so per CONTRIBUTING.md it could target PHP-8.4.

How this was tested

Linux x86_64 (kernel 7.0), gcc 13.3, debug builds of origin/master, of it with this PR's commits cherry-picked (NEWS left as master's), of that with the change below, and of origin/PHP-8.4 with and without this PR:

./buildconf --force
./configure --enable-debug --disable-cgi --disable-phpdbg --disable-fileinfo --enable-pcntl --enable-zend-test --enable-fpm --with-curl --enable-mbstring --disable-mbregex --with-mysqli --with-pdo-mysql
make -j4

The listener is Python: s.bind(path); s.listen(1), then either never accept() or accept() one connection every 300 ms and keep it. The client opens connections in a loop with stream_socket_client("unix://$path", $errno, $errstr, $timeout) (or mysqli::real_connect('localhost', …, $socket) with -d mysqlnd.net_read_timeout=2, since the queued connections wait for a greeting), keeps each, and prints the wall time and the CPU time from getrusage() around each connect. The strace count: strace -f -e trace=connect,poll, two connects to fill the backlog, then one with a 1 s timeout.

The change tried instead of the poll:

-				/* nothing to poll for here, the connection never started */
-				php_pollfd_for(sockfd, 0, &slice);
+				/* sleep: polling a socket that never connected returns POLLHUP at once */
+				usleep((unsigned int) (slice.tv_sec * 1000000 + slice.tv_usec));

Suites: TEST_PHP_EXECUTABLE=$PWD/sapi/cli/php sapi/cli/php run-tests.php -n -q -j4 ext/standard/tests/streams ext/standard/tests/network on each build.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Connecting to PDO / MySQL database over unix-socket throws "a retry" error

3 participants