Drain deferred disposeBrowserContext on next createBrowserContext (#2472 follow-up)#2476
Closed
staylor wants to merge 1 commit into
Closed
Drain deferred disposeBrowserContext on next createBrowserContext (#2472 follow-up)#2476staylor wants to merge 1 commit into
staylor wants to merge 1 commit into
Conversation
`CDP.disposeBrowserContext` defers cleanup when a script eval is on the
stack (a CDP message drained inside `HttpClient.syncRequest` arrives
mid-evaluate; tearing down the bc here would free Session/V8/inspector
state the unwinding script frame is about to dereference). The
existing comment said cleanup was "deferred to CDP.deinit at connection
close, by which time eval has unwound."
That assumption holds for short-lived CDP connections that close after
each browser session. It breaks for long-lived ones (Playwright's
`chromium.connectOverCDP` keeps the websocket open across many
contexts): once a dispose hits the deferred branch, `browser_context`
stays non-null, and the next `createBrowserContext` rejects with
`error.AlreadyExists` -> `Cannot have more than one browser context
at a time` for the rest of the connection.
Fix: mark the bc with a new `pending_dispose` flag in the deferred
branch instead of just returning, and drain that pending dispose on
the next `createBrowserContext` once eval has unwound. The drain runs
the same `bc.deinit` + `closeSession` + `browser_context = null` the
non-deferred path would have. If eval is still on the stack at the
next create attempt (very tight cycle), the create still rejects with
AlreadyExists -- waiting for eval to finish is the client's problem,
but recovery now happens automatically once it does.
Also updates `isValidSessionId`: a pending-dispose bc is logically
gone from the client's POV, so commands on its old session_id reject
like any other unknown session instead of silently routing into a
half-alive context.
Reproduction (`playwright-core@1.58.2`) -- a slow `<script src>` load
forces `disposeBrowserContext` into the deferred branch, then a
later `newContext` (after the script has finished) recovers cleanly:
for (let i = 1; i <= 3; i++) {
const ctx = await browser.newContext();
const page = await ctx.newPage();
page.goto(SLOW_PAGE_URL).catch(() => {});
await new Promise((r) => setTimeout(r, 500)); // close mid-fetch
await ctx.close();
await new Promise((r) => setTimeout(r, 4000)); // let eval unwind
}
Before: cycle 2's `newContext()` throws
`Cannot have more than one browser context at a time`.
After: 3/3 cycles complete cleanly.
Tests: 653 / 653 pass. Added regression coverage in
`cdp.target: createBrowserContext flushes deferred dispose once eval
unwinds` -- forces `is_evaluating = true`, asserts dispose defers and
flips `pending_dispose`, asserts `createBrowserContext` rejects while
eval is still on the stack, then drops `is_evaluating` and asserts
the next `createBrowserContext` flushes the pending dispose and
returns a distinct bc.
Notes:
- `Session.removePage` has the same deferred pattern but isn't in the
client-visible recovery path (Playwright never calls page-level
dispose, only context-level), so this PR doesn't touch it.
- Independent of lightpanda-io#2472 (frame-ID generator scope) and lightpanda-io#2399 (Playwright
STARTUP-session promotion). The three together close the path for
Playwright `connectOverCDP` to recycle BrowserContexts safely.
Collaborator
|
Closing. Fixed (or at least significantly improved) by #2495 |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Follow-up to #2472 / #2474. The frame-ID PR fixed the cross-BrowserContext target-ID collision. This one fixes a second
Cannot have more than one browser context at a timefailure that the same Playwright workload hits when the CDP connection is long-lived.Problem
CDP.disposeBrowserContext(src/cdp/CDP.zig:378) defers cleanup when a script eval is on the stack. The existing comment is right about why -- a CDP message drained insideHttpClient.syncRequestarrives mid-evaluate, and tearing down the bc here would free Session / V8 / inspector state the unwinding script frame is about to dereference. The comment also said cleanup was "deferred toCDP.deinitat connection close, by which time eval has unwound."That assumption holds for short-lived CDP connections that close after each browser session. It breaks for long-lived ones. Playwright's
chromium.connectOverCDPkeeps the websocket open across many BrowserContexts: once a dispose hits the deferred branch,browser_contextstays non-null, and the nextcreateBrowserContextrejects witherror.AlreadyExists->Cannot have more than one browser context at a timefor the rest of the connection. There is no path to recovery short of disconnecting and reconnecting, which defeats the point of pooling.Reproduction
Minimal Node + Playwright + a slow
<script src>server:Before this PR (against
main):After:
Fix
Three small changes in
src/cdp/CDP.zig:New
pending_dispose: boolflag onBrowserContext. Set in the deferred branch ofdisposeBrowserContextinstead of just returning. Marks the bc as logically gone but physically still alive while the eval frame above us unwinds.New private
flushPendingDisposehelper. Re-checksanyScriptEvaluating()and runs the samebc.deinit+closeSession+browser_context = nullthe non-deferred path would have if eval is now off the stack. Returnsfalseif eval is still on the stack so the caller can surface that to the client.createBrowserContextcallsflushPendingDisposebefore checkingAlreadyExists. If the previous deferred dispose can be drained, the new context proceeds normally. If eval is still on the stack at the next create attempt (very tight cycle), the create still rejects withAlreadyExists-- waiting for eval to finish is the client's problem, but recovery now happens automatically once it does.isValidSessionIdtreatspending_dispose == trueas no bc. Commands on the old session_id now reject withUnknown sessionIdinstead of silently routing into a half-alive context.Session.removePagehas the same deferred pattern but isn't in the client-visible recovery path (Playwright never calls page-level dispose, only context-level), so this PR doesn't touch it. Filing separately if it turns out to bite something.Tests
zig build test: 653 / 653 pass (652 existing + 1 new).Added
cdp.target: createBrowserContext flushes deferred dispose once eval unwinds:is_evaluating = trueon the active page's frame.disposeBrowserContextreturns success and flipspending_dispose.createBrowserContextrejects withAlreadyExistswhile eval is still on the stack.is_evaluating = false(eval has unwound).createBrowserContextflushes the pending dispose and returns a distinct bc.Verified the test catches the bug: reverting the CDP.zig changes (keeping only the test) produces a single-test failure, the rest of the suite still passes.
Relationship to other PRs
connectOverCDPSTARTUP-session promotion. Lets Playwright clients connect at all.Duplicate target FID-....Independent of both -- can land in any order. With all three, Playwright
connectOverCDPbecomes safe to use as a long-lived pooled connection.