Skip to content

feat(engine): opt-in prewarm Chromium spare with smoke matrix and hardening - #50

Merged
gildesmarais merged 19 commits into
mainfrom
feat/prewarm-browser
Aug 28, 2026
Merged

feat(engine): opt-in prewarm Chromium spare with smoke matrix and hardening#50
gildesmarais merged 19 commits into
mainfrom
feat/prewarm-browser

Conversation

@gildesmarais

@gildesmarais gildesmarais commented Aug 27, 2026

Copy link
Copy Markdown
Member

What changed

  • Opt-in SCRAPE_PREWARM single-slot WarmDriverPool: 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).
  • Browser tier adopts warm spare on fingerprint match; cold boot unchanged on pool miss or prewarm off. Spare dies with the adopting request (never returned to the pool).
  • Production hardening: atomic prune snapshot of active request ids + live_spare_dirs() (app/engine/orchestrator.py); unified _probe_health / _abort_adopted_take so probe failures close the driver and drop adopted-dir protection; stale spare reap when _desired fingerprint changes (fixes refill stall at SCRAPE_PREWARM_IDLE_TTL_SECONDS=0).
  • Lifespan coverage: tests/api/test_prewarm_lifespan.py asserts SCRAPE_PREWARM=false leaves engine.warm_pool is None, true attaches WarmDriverPool, and real shutdown stops refill (post-exit notify_scrape_finished does not rebuild); also parse_scrape_prewarm env string matrix.
  • Smoke profiles (scripts/smoke.sh): SMOKE_PROFILE=contract-prewarm-off (full contract suite) and prewarm-on-warm-handoff (two browser scrapes; assert scrape_boot warm_hit=False then refill then warm_hit=True). Default all runs both. SMOKE_SKIP_BUILD + empty-env-safe container start.
  • CI: docker-smoke-image warms Buildx GHA cache (scope=smoke-linux-amd64); matrix legs titled Smoke (prewarm=false, cold) and Smoke (prewarm=true, warm-handoff) with fail-fast: false and SMOKE_SKIP_BUILD=1. Workflow push limited to main (PR checks via pull_request only) plus concurrency cancel-in-progress.
  • Test support: consolidated fakes in 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).
  • Ops docs: single uvicorn worker when prewarm on; idle_ttl=0 holds RAM until process exit; smoke/CI coverage recorded (README.md, AGENTS.md).

Commits vs main (newest first):

  • 5ca676f ci: run PR checks once via pull_request only
  • 2524045 fix(smoke): tolerate empty env when starting scrape container
  • 7b79c63 fix(tests): assert real warm-pool shutdown in lifespan test
  • be94cb3 docs: record prewarm smoke coverage and CI image cache
  • 636c486 ci(smoke): cached image job and named prewarm matrix
  • 15a6ef0 test(smoke): add SMOKE_PROFILE off and warm-handoff
  • de10e1a test(engine): cover create_app prewarm on and off via lifespan
  • 56f81e8 fix(tests): stabilize concurrent warm-pool take/notify test
  • c890065 refactor(tests): replace dataclass lambdas with typed factory functions
  • 355d098 fix(engine): reap stale warm spare when desired fingerprint changes
  • a5bfca3 docs(ops): state prewarm single-worker and idle TTL requirements
  • 85a0992 test(engine): lock warm-path configure and idle-spare invariants
  • 3e1ce6f fix(engine): atomic prune protection and unified adopted-take abort
  • da0aeb4 refactor(tests): consolidate warm pool driver fakes
  • 7a6f9ec fix(engine): protect adopted spare dirs from prune during handoff
  • e0ec5a2 docs(ops): document prewarm flag, memory posture, and invariant change
  • 1f1d121 feat(engine): serve browser scrapes from prewarmed driver when fingerprint matches
  • d8ba007 feat(engine): add warm driver pool with fingerprint gating

Why

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; push limited to main avoids duplicate PR check runs.

Risk

  • Memory: warm spare holds an extra Chromium (briefly two during refill); canary under --memory before production enable.
  • Deployment: prewarm is in-process — single uvicorn worker (or sticky routing) required.
  • Fingerprint: cookies/headers/block_trackers applied per request in configure_driver(), not in fingerprint hash — idle-spare + warm-path tests lock this.
  • Default off: no runtime change unless SCRAPE_PREWARM=true.
  • CI time: two smoke matrix legs; shared image cache mitigates rebuild cost.

Review map

Large PR (~2557 lines, 27 files). Tests first:

  1. tests/engine/test_warm_pool.py — pool lifecycle, TTL, stale fingerprint reap, health-probe abort, concurrent take/notify, idle refill
  2. tests/engine/test_warm_wiring.py — end-to-end adoption, prune barrier, cookies/headers on warm hit, isolation
  3. tests/api/test_prewarm_lifespan.py — create_app attach/shutdown for prewarm on/off; env parse
  4. tests/support/fakes.py — shared instrumented drivers and health-check stubs
  5. app/engine/warm_pool.py — spare build/refill/TTL/take/abort; module docstring invariants
  6. app/engine/browser_tier.py — warm hit vs cold boot wiring
  7. app/engine/orchestrator.py — atomic prune snapshot with live_spare_dirs()
  8. scripts/smoke.shSMOKE_PROFILE off / warm-handoff / all; skip-build
  9. .github/workflows/ci.yml — docker-smoke-image cache job; named matrix; push→main only
  10. README.md / AGENTS.md — ops constraints and isolation invariants

Validation

  • make check (lint, tests, pyright strict, openapi-verify, spectral, hadolint)
  • CI smoke matrix: Smoke (prewarm=false, cold) + Smoke (prewarm=true, warm-handoff) via cached image
  • Local canary: make smoke / warm handoff under memory limit; record p50 boot_ms delta cold vs warm

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.
Comment thread tests/support/fakes.py Fixed
Satisfies code-quality review on DriverConfigureState default_factory
while keeping pyright strict list typing.
@gildesmarais gildesmarais changed the title feat(engine): prewarmed Chromium hot spare (opt-in) feat(engine): opt-in prewarmed Chromium spare with production hardening Aug 28, 2026
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.
@gildesmarais gildesmarais changed the title feat(engine): opt-in prewarmed Chromium spare with production hardening feat(engine): opt-in prewarm Chromium spare with smoke matrix and hardening Aug 28, 2026
@gildesmarais
gildesmarais enabled auto-merge (squash) August 28, 2026 15:48
@gildesmarais
gildesmarais merged commit e4eb7f8 into main Aug 28, 2026
13 checks passed
@gildesmarais
gildesmarais deleted the feat/prewarm-browser branch August 28, 2026 15:50
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