14 Commits
Author SHA1 Message Date
germondai ac79af388f perf(api): accelerate cold starts 2026-08-09 23:34:47 +02:00
germondai c0769af181 fix(browser): bound memory with rolling recycling 2026-08-09 18:15:48 +02:00
germondai 2547daa41b fix(browser): clarify Gluetun startup failures 2026-08-08 22:58:10 +02:00
germondai 8b54abd4a1 fix(browser): keep persistent contexts pool-owned 2026-08-02 15:37:47 +02:00
germondai 78836d6ed7 fix(proxy): support authenticated browser proxies 2026-07-27 03:08:16 +02:00
germondai 3a4d7491b3 Merge pull request #37 from funkypenguin/fix/bound-restart-and-reclaim-stalled 2026-07-27 02:40:41 +02:00
germondai defade4324 refactor(browser): simplify optional pool state 2026-07-24 12:08:55 +02:00
germondai 3e26a7e990 feat(browser): add persistent context cache 2026-07-24 10:43:18 +02:00
David Young 7dae357103 fix(browser): bound every await in restartEntry; reclaim stalled checkouts
`BrowserPool.restartEntry` awaited `context.close()`, `browser.close()` and the
Camoufox launch with no timeout on any of them. Camoufox hangs on close when a
content process is wedged — tiers/3.ts and tiers/4.ts already guard their
*temporary* contexts against exactly this with a 5s `Promise.race` — but the
persistent context and browser the pool owns had no such guard, and launches can
hang too.

When any one of those hangs, the entry is pinned at `restarting = true` forever.
From then on the health check hits its own `if (entry.restarting) return` guard,
so every 30s tick logs "browser N disconnected, restarting" and does nothing.
The pool silently loses that slot permanently: `restartCount` never increments,
so the restart counter sits frozen while the log implies furious activity. With
enough uptime every entry ends up in this state and the pool is inert.

Changes:

  * every await in `restartEntry`, `init()` and `shutdown()` is bounded. On
    timeout the entry is left unhealthy with `restarting` cleared, so the next
    health-check tick retries it from scratch instead of wedging.
  * `runHealthCheck` reclaims checkouts past their deadline. Previously busy
    entries were skipped entirely, so an entry whose request wedged was never
    examined again.
  * a per-checkout `lease`, returned on the handle and passed back to
    `release()`, so a request that outlives its checkout cannot free — or
    recycle, via `noteTemporaryContext` — a browser the pool has since handed to
    someone else.
  * `release()` hands its in-flight page closes to `restartEntry` rather than
    racing them, since closing a context underneath in-flight `page.close()`
    calls is one way to wedge the transport in the first place.
  * abandoned launches are counted and capped. A timeout can only stop *waiting*
    for a launch, not cancel it, so retrying without a cap could pile up hung
    Firefox processes; past the cap the entry stays down and `live` reflects it.

Timeouts are configurable (`closeTimeoutMs`, `launchTimeoutMs`, `stallAfterMs`,
`healthIntervalMs`) with the API exposing them as BROWSER_*_MS env vars.

Adds regression tests for the hung close, the hung launch, stall accounting,
budget-aware stall deadlines, disconnected-but-busy entries, and stale releases.
The hung-close and hung-launch tests both fail against the unpatched pool.
2026-07-21 14:47:53 +12:00
germondai bb062c6bd8 test(browser): cover recycle-on-blocked and contentProcesses options 2026-07-06 20:10:19 +02:00
CoolDotty 68ad2e0f1a Recycle browsers after temporary contexts 2026-07-06 00:47:32 -07:00
germondai 1e6ca16dd0 test: add Cloudflare challenge bypass tests 2026-06-02 15:10:00 +02:00
germondai cc17320017 test: add camoufox stealth mode integration tests 2026-06-02 11:55:00 +02:00
germondai b235862fc2 test: add browser pool smoke tests 2026-06-02 09:20:00 +02:00