feat(engine): opt-in prewarm Chromium spare with smoke matrix and hardening - #50
Merged
Conversation
Introduce an opt-in single-slot WarmDriverPool and redacting DriverFingerprint so Chromium can be prebuilt off the scrape executor. Docker Xvfb spike confirmed concurrent headed drivers are safe, so the pool is not gated to headless-only.
…print matches Wire WarmDriverPool into browser_tier, session adoption, orchestrator prune protection, lifespan shutdown, and Sentry warm_hit tagging so matching requests skip cold Chromium boot without changing the wire contract.
Update AGENTS isolation/runtime rules and README env table for opt-in SCRAPE_PREWARM, and extend the bench harness with --execution-mode browser for cold vs warm boot measurement.
After take(), spare-* left live_spare_dirs so concurrent prune/ENOSPC retry could delete an in-use profile. Track adopted + in-build dirs until release_adopted/session cleanup; schedule deferred refill wakes on min-refill/memory skip; run CDP health outside the pool lock.
When idle TTL is disabled, a ready spare whose fingerprint no longer matches _desired blocked refill indefinitely. Reap on mismatch before refill.
Satisfies code-quality review on DriverConfigureState default_factory while keeping pyright strict list typing.
Notify-before-take race skipped refill without a second wake; nudge notify after the barrier window and clean up adopted spare.
Replace the misleading ScraperEngine constructor assertion with TestClient lifespan coverage so SCRAPE_PREWARM attach/shutdown is exercised without booting Chromium, and cover parse_scrape_prewarm env strings.
Profile-drive make smoke so contract-prewarm-off keeps the full suite while prewarm-on-warm-handoff only proves warm_hit handoff, with skip-build, faster health polls, overlapped Sentry checks, and --init --shm-size=1gb.
Warm a shared linux/amd64 Buildx GHA cache once, then run named matrix legs for prewarm=false cold contract and prewarm=true warm-handoff with skip-build and fail-fast disabled so Checks UI shows which scenario failed.
Document SMOKE_PROFILE off/on coverage, Checks UI titles, and the shared Buildx GHA smoke image cache so maintainers know isolation stays on the cold path only.
Drop the WarmDriverPool.shutdown mock so create_app lifespan joins the refill daemon; prove stop by showing post-exit notify does not refill.
Iterate \"\$@\" instead of a copied empty array so contract-prewarm-off works under set -u.
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 changed
SCRAPE_PREWARMsingle-slotWarmDriverPool: fingerprint-gated Chromium spare, dedicated refill daemon (never the scrape executor), one-shot adoption per request (app/engine/warm_pool.py,app/engine/browser_tier.py,app/main.py).live_spare_dirs()(app/engine/orchestrator.py); unified_probe_health/_abort_adopted_takeso probe failures close the driver and drop adopted-dir protection; stale spare reap when_desiredfingerprint changes (fixes refill stall atSCRAPE_PREWARM_IDLE_TTL_SECONDS=0).tests/api/test_prewarm_lifespan.pyassertsSCRAPE_PREWARM=falseleavesengine.warm_pool is None,trueattachesWarmDriverPool, and real shutdown stops refill (post-exitnotify_scrape_finisheddoes not rebuild); alsoparse_scrape_prewarmenv string matrix.scripts/smoke.sh):SMOKE_PROFILE=contract-prewarm-off(full contract suite) andprewarm-on-warm-handoff(two browser scrapes; assertscrape_boot warm_hit=Falsethen refill thenwarm_hit=True). Defaultallruns both.SMOKE_SKIP_BUILD+ empty-env-safe container start.docker-smoke-imagewarms Buildx GHA cache (scope=smoke-linux-amd64); matrix legs titledSmoke (prewarm=false, cold)andSmoke (prewarm=true, warm-handoff)withfail-fast: falseandSMOKE_SKIP_BUILD=1. Workflowpushlimited tomain(PR checks viapull_requestonly) plus concurrency cancel-in-progress.tests/support/fakes.py(TrackingDriver,InstrumentedDriver,HealthCheckStub); typed factory functions replace dataclass lambdas; concurrent take/notify race fixed (nudge notify after barrier + clean adopted spare).idle_ttl=0holds RAM until process exit; smoke/CI coverage recorded (README.md,AGENTS.md).Commits vs
main(newest first):5ca676fci: run PR checks once via pull_request only2524045fix(smoke): tolerate empty env when starting scrape container7b79c63fix(tests): assert real warm-pool shutdown in lifespan testbe94cb3docs: record prewarm smoke coverage and CI image cache636c486ci(smoke): cached image job and named prewarm matrix15a6ef0test(smoke): add SMOKE_PROFILE off and warm-handoffde10e1atest(engine): cover create_app prewarm on and off via lifespan56f81e8fix(tests): stabilize concurrent warm-pool take/notify testc890065refactor(tests): replace dataclass lambdas with typed factory functions355d098fix(engine): reap stale warm spare when desired fingerprint changesa5bfca3docs(ops): state prewarm single-worker and idle TTL requirements85a0992test(engine): lock warm-path configure and idle-spare invariants3e1ce6ffix(engine): atomic prune protection and unified adopted-take abortda0aeb4refactor(tests): consolidate warm pool driver fakes7a6f9ecfix(engine): protect adopted spare dirs from prune during handoffe0ec5a2docs(ops): document prewarm flag, memory posture, and invariant change1f1d121feat(engine): serve browser scrapes from prewarmed driver when fingerprint matchesd8ba007feat(engine): add warm driver pool with fingerprint gatingWhy
Cold Chromium boot dominates browser-tier latency. A single prewarmed spare cuts boot time when fingerprints match, without changing default memory posture (
SCRAPE_PREWARM=false). Hardening closes prune races, health-probe leaks, and stale-spare refill stalls. Smoke profiles + cached CI matrix prove cold contract and warm handoff without double-building the image;pushlimited tomainavoids duplicate PR check runs.Risk
--memorybefore production enable.configure_driver(), not in fingerprint hash — idle-spare + warm-path tests lock this.SCRAPE_PREWARM=true.Review map
Large PR (~2557 lines, 27 files). Tests first:
tests/engine/test_warm_pool.py— pool lifecycle, TTL, stale fingerprint reap, health-probe abort, concurrent take/notify, idle refilltests/engine/test_warm_wiring.py— end-to-end adoption, prune barrier, cookies/headers on warm hit, isolationtests/api/test_prewarm_lifespan.py— create_app attach/shutdown for prewarm on/off; env parsetests/support/fakes.py— shared instrumented drivers and health-check stubsapp/engine/warm_pool.py— spare build/refill/TTL/take/abort; module docstring invariantsapp/engine/browser_tier.py— warm hit vs cold boot wiringapp/engine/orchestrator.py— atomic prune snapshot withlive_spare_dirs()scripts/smoke.sh—SMOKE_PROFILEoff / warm-handoff / all; skip-build.github/workflows/ci.yml— docker-smoke-image cache job; named matrix; push→main onlyREADME.md/AGENTS.md— ops constraints and isolation invariantsValidation
make check(lint, tests, pyright strict, openapi-verify, spectral, hadolint)Smoke (prewarm=false, cold)+Smoke (prewarm=true, warm-handoff)via cached imagemake smoke/ warm handoff under memory limit; record p50boot_msdelta cold vs warm