Repository navigation
fix: keep the Playwright connection when a forked child process exits - #297
Open
Vasiliy-Bondarenko wants to merge 1 commit into
Open
Vasiliy-Bondarenko wants to merge 1 commit into
Vasiliy-Bondarenko wants to merge 1 commit into
Conversation
A process forked from the test process (pcntl_fork) inherits the Playwright WebSocket socket. When the child exits, PHP destroys its copy of the connection: amphp's UpgradedSocket::__destruct() closes the socket with stream_socket_shutdown(), which acts on the socket shared by both processes, so the Playwright server drops the connection the test process still uses. The client now remembers the process that opened the connection and, in any other process, closes only that process's copy of the socket (fclose) in a shutdown function, before PHP runs the destructors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A process forked from the test process (
pcntl_fork) that ends with a plainexit()kills the test process's Playwright connection. The next browser call fails:So does every later browser test in that process. With 5.0.x the test process could also spin at 100% CPU in
Client::fetch()(fixed separately in 5.2.0). Code that forks shows this in real suites: worker pools, a job killed bypcntl_alarm()+exit(), race-condition tests that fork children.Why
The child inherits the WebSocket socket. On
exit()PHP destroys the child's copy of the connection. amphp'sUpgradedSocket::__destruct()callsclose()→ResourceSocket::close()→stream_socket_shutdown(). Unlikefclose(),shutdown()acts on the socket itself and not on the child's descriptor, so the Playwright server receives FIN and drops the connection that the parent still uses.It depends on the order PHP destroys objects at shutdown. In a fresh process the socket's read and write streams go first and null their resource, so the later
close()does nothing. PHP reuses freed object handles (last freed, first reused). In a process that has already created and freed many objects, which describes any long test run, the order flips. That is why it shows up in full suites and not when a single test runs.Fix
Clientcreates the connection through a smallConnectionFactorythat hands it the underlying socket. The client remembers which process opened the connection and registers a shutdown function once. In any other process, that functionfclose()s the process's own copy of the socket before PHP runs the destructors, so the shared socket is never shut down.Test
tests/Browser/Visit/ForkedChildTest.phpreconnects after recycling object handles, which makes the destruction order deterministic. It then forks a child that callsexit(0)and visits a page again. The test fails onmainwith the error above and passes with the fix.composer testpasses: lint, profanity, 100% type coverage, phpstan and all 432 tests.Standalone reproduction: a clean Laravel 13 + Pest 5.3 + plugin 5.2.0 project with one failing test and one SIGKILL control. It can be shared if useful.