Make the headless browser on-demand: spawn on connect, reap when idle #44
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #38
What to build
The browser container stops holding a live Chrome while nothing is using it. Idle cost falls from
~186 MiB anonymous to ~3 MiB, Chrome exists only for the few minutes a day the poller actually
needs it, and the Cloudflare clearance it earned is still there on the next wake.
This is a change to the browser image only. The backend needs no code change to work with it — the
remote allocator re-runs endpoint discovery on every allocation and the fetcher builds a fresh
context per fetch, which was verified against a real Chrome restart with a changed debugger UUID.
Implementation decisions
holds the CDP port for the container's lifetime, spawns Chrome on the first inbound connection
under a lock so concurrent connections block rather than race, and a periodic reaper terminates
Chrome once there are no live connections and the last use is older than the idle threshold.
to disk on a ~31-second batch timer, so a shorter threshold discards the clearance just earned;
and the effective reap lands at threshold + 90 s because Go's default HTTP transport parks the
discovery connection for its idle timeout. 300 s also holds Chrome through a full staggered batch
plus a cover prefetch run.
recreation. Without it every
up --buildre-solves the challenge.legacy: Chromium forcibly remaps a non-loopback remote-debugging address to loopback since M113,
and Chrome ignores the remote-debugging flags entirely on a default profile since Chrome 136.
chromedp.NoModifyURLmust never be added. It is the single change that would break survival ofa Chrome restart behind a stable endpoint; the existing comment forbidding it becomes load-bearing
for a second reason.
non-UTC timezone (a UTC clock is itself the bot signal), a User-Agent whose major version is read
out of the installed binary and which carries no HeadlessChrome token, and no automation flag.
exists for seconds per wake, at the cost of untested changes to the fingerprint of the one thing
that is hard to get right.
from the caller's own deadline expiring. It gets a distinguishing wrap. With an on-demand browser
this case becomes a normal event, and misreading it as a site timeout sends someone hunting a
Cloudflare problem that does not exist.
expensive to rediscover.
Acceptance criteria
Chrome process is running.
failure.
answering on its port.
older than the commit window.
docker compose up --build(profile volume), not just a reap.as a caller timeout, and this is asserted on the classification rather than by killing a real
browser.
go test ./...passes.Blocked by
None — can start immediately.
Implemented in
b1588bb. Acceptance evidence: docker build succeeded; curl to the fresh sidecar returned a real Chrome CDP response and spawned Chrome on demand; graceful stop exited 143 and left no container or orphan Chrome; sh -n chrome/entrypoint.sh passed; go test ./... passed (7 packages, 3 no tests). ADR-0005 records the measured 31s cookie batch, 90s transport park, 300s reap floor, M113/M136 profile constraints, NoModifyURL prohibition, anti-bot constraints, and shutdown cookie flush.Additional smoke evidence: a fresh container had no Chrome process before any client; the first /json/version request returned Chrome/151.0.7922.108; after closing the helper and aging last-use, the 10-second reaper removed Chrome while socat stayed up; a second /json/version request woke Chrome again. Container cleanup completed with no remaining Docker record.