Skip to content

Persist the port of the server that was actually started - #246

Open
yeapea wants to merge 1 commit into
pestphp:5.xfrom
yeapea:fix/persist-only-the-server-that-was-started
Open

Persist the port of the server that was actually started#246
yeapea wants to merge 1 commit into
pestphp:5.xfrom
yeapea:fix/persist-only-the-server-that-was-started

Conversation

@yeapea

@yeapea yeapea commented Aug 6, 2026

Copy link
Copy Markdown

The defect

ServerManager::playwright() creates the Playwright server once — ??= sees to that — but the two lines around the assignment run on every call:

$port = Port::find();                               // ← every call
$host = Playwright::host() ?? self::DEFAULT_HOST;

$this->playwright ??= PlaywrightNpmServer::create(   // ← first call only
    ..., $host, $port, 'Listening on',
);

AlreadyStartedPlaywrightServer::persist($host, $port);   // ← every call

The port lives on the server instance as a readonly property, so the server keeps the port it was created with. But the second call runs Port::find() again — which by definition returns a port nothing is listening on, since the running server's port is now taken — and persists that as the description of the server.

The server is fine. The record of where to reach it is not, and that record is what AlreadyStartedPlaywrightServer::fromPersisted() hands to every parallel worker.

The measurement

Two calls to playwright() in one process, start()ed in between so the server is genuinely listening, reading .temp/playwright-server.json after each:

Before

call 1  returned url: 127.0.0.1:56398
call 1  persisted:    {"host":"127.0.0.1","port":56398}
call 2  returned url: 127.0.0.1:56398
call 2  persisted:    {"host":"127.0.0.1","port":56399}

same server object:   yes
persisted unchanged:  NO
persisted port 56399 listening: no (Connection refused)

After

call 1  returned url: 127.0.0.1:56427
call 1  persisted:    {"host":"127.0.0.1","port":56427}
call 2  returned url: 127.0.0.1:56427
call 2  persisted:    {"host":"127.0.0.1","port":56427}

same server object:   yes
persisted unchanged:  yes
persisted port 56427 listening: yes

The same server object is returned either way — only the persisted description moves, onto a port that refuses connections.

The fix

Move the port allocation and the persist() inside the guard, so they stay with the creation they describe:

if (! $this->playwright instanceof PlaywrightServer) {
    $port = Port::find();
    $host = Playwright::host() ?? self::DEFAULT_HOST;

    $this->playwright = PlaywrightNpmServer::create(/* ... */);

    AlreadyStartedPlaywrightServer::persist($host, $port);
}

return $this->playwright;

Nothing changes on the first call. On every later call the method now returns the same server and leaves its description alone. composer lint (rector + pint) is clean on the changed file.

A related finding, deliberately not in this PR

While tracking this down I ran into a second issue in the same area: the persisted state file has no run identity, so two concurrent runs on one machine share it — the second overwrites the first's description, and whichever finishes first removes the file out from under the other via markAsStopped().

I left it out because fixing it properly means deciding how a run is identified and what happens to a stale file, and those are design calls that belong to you rather than to a drive-by PR. Happy to open it separately with a measurement if the shape you'd want is clear — or to just leave it here as a report.

`??=` already makes the Playwright server a one-off, but the two lines around
it ran on every call to `playwright()`. So a second call allocated a fresh port
that nothing was listening on, and then persisted that port as the description
of the server that IS running.

Every later `visit()` reads that description, so workers connect to a dead port.

Moving the port allocation and the persist inside the guard keeps them with the
creation they describe.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant