Skip to content

Add multi-worker concurrency (page-level and session-level parallelism)#22

Merged
billrichards merged 8 commits into
mainfrom
claude/eloquent-euler-a4ry0
Jun 13, 2026
Merged

Add multi-worker concurrency (page-level and session-level parallelism)#22
billrichards merged 8 commits into
mainfrom
claude/eloquent-euler-a4ry0

Conversation

@billrichards

Copy link
Copy Markdown
Owner

Summary

Adds two layers of concurrency, available identically across CLI, web, and the Python API. The codebase previously tested pages strictly sequentially within a run, and the web server spawned one unbounded thread per job.

Layer 1 — page-level parallelism (qa_agent/concurrency.py, agent.py)

  • New thread-safe Frontier (shared BFS queue with correct max_pages/max_depth budgeting under concurrency + deadlock-free termination) and PageIndexer (worker-safe screenshot indices).
  • QAAgent._run_concurrent(): N worker threads, each owning its own Playwright stack (required — the sync API is thread-bound). workers == 1 keeps the original sequential path unchanged.
  • Auth is performed once on a bootstrap context and replicated to every worker via Playwright storage_state.
  • Surfaced as TestConfig.workers (default 1, capped at 16) → CLI --workers, web workers field in POST /api/run.

Layer 2 — session pool (qa_agent/batch.py)

  • BatchRunner (bounded ThreadPoolExecutor) + BatchJob. The web server now uses it instead of unbounded thread-per-job (QA_AGENT_JOB_POOL_SIZE, default 4). CLI exposes --batch-file/--pool-size; library users get from qa_agent import BatchRunner.

Cross-cutting

  • QAAgent deep-copies the config so concurrent agents don't clobber each other's output dirs (previously __init__ mutated the caller's config).
  • A worker_thread_init hook routes worker-thread stdout to the web SSE job queue (thread-local stdout would otherwise miss sub-threads); _QueueWriter made thread-safe.
  • Full public API re-exported from qa_agent (matches the README's documented imports).

Total live browsers ≈ pool_size × workers — both knobs are capped and documented.

Test plan

  • 566 unit tests pass; coverage 78% (gate 70%). New tests: tests/test_concurrency.py (Frontier budget/termination/indexer under threads), tests/test_batch.py (bounded concurrency, ordering, per-job exception isolation), TestQAAgentParallel in tests/test_agent.py (aggregation, config isolation, stop-event, explore max_pages).
  • ruff clean; mypy clean on all changed files.
  • Added TestSmokeParallel integration tests exercising the real sync-Playwright-in-threads path.

Notes

  • Real-browser integration tests could not be run in the authoring environment (network policy blocks the Playwright browser CDN); they will run in CI.
  • One pre-existing mypy error remains in qa_agent/testers/custom.py:190 (untouched code, flagged only by newer mypy 2.x vs. the project's pinned mypy>=1.0) — not introduced by this change.

https://claude.ai/code/session_0159uLk7nyt7cbv4PX1bTe5o


Generated by Claude Code

claude and others added 8 commits June 3, 2026 01:17
…lism

Introduce two layers of concurrency, available identically across CLI, web,
and the Python API:

- Page-level: TestConfig.workers (CLI --workers, web 'workers' body field)
  tests multiple pages of one run in parallel via a thread-safe Frontier, each
  worker owning its own browser/context. Defaults to 1 (unchanged sequential
  behaviour); capped at 16. Auth is performed once and replicated to every
  worker via Playwright storage_state.
- Session-level: BatchRunner runs multiple independent sessions through a
  bounded thread pool. CLI exposes --batch-file/--pool-size; the web server now
  uses it instead of unbounded thread-per-job (QA_AGENT_JOB_POOL_SIZE).

Also stop mutating the caller's TestConfig (deep-copy in QAAgent), route
worker-thread stdout via a worker_thread_init hook so web SSE keeps working,
and re-export the full public API from qa_agent.

Adds unit tests for the Frontier/PageIndexer primitives, BatchRunner pool,
parallel agent runs, plus integration smoke tests for the real
sync-Playwright-in-threads path.
CI runs mypy 2.1.0 (stricter than local 1.x): annotate _launch_browser's
playwright param so it returns Browser not Any, cast user-supplied cookies to
satisfy add_cookies' SetCookieParam typing, and str() the recording video path.
- Add "Page Workers" number input (1–16) to the Browser Settings section
  of the run form; collectFormData and applyConfig now include workers so
  it flows through the existing POST /api/run body → _build_config path.

- Add "Server Settings" collapsible card below the run form with a Pool
  Size input (1–8); index.js fetches the current value from the new
  GET /api/server-config endpoint on page load and the Apply button
  PATCHes the new value.

- New GET /api/server-config endpoint returns {pool_size, pool_size_max,
  workers_max}; new PATCH /api/server-config recreates the BatchRunner
  singleton with the requested pool size (clamped 1–8).

- 16 new tests in TestApiServerConfig covering GET fields, PATCH
  update/clamp/validation, and _build_config workers handling.

https://claude.ai/code/session_0159uLk7nyt7cbv4PX1bTe5o
Gives Claude Code instances a quick orientation: editable-install
requirement for packaging tests, test/lint/build commands, and a summary
of the request-flow architecture and extension points.
Add HostRateLimiter, a thread-safe min-interval-per-host throttle for
page.goto() navigations, enabled by default at 3 req/s (config.rate_limit,
--rate-limit, 0 disables). One shared limiter covers all workers within a
QAAgent run; BatchRunner can hold a single shared limiter passed to every
QAAgent it constructs so concurrent batch jobs targeting the same host share
a budget. The web server applies the same default (QA_AGENT_RATE_LIMIT).

Addresses "too many connections" errors when --workers/--pool-size fan out
many concurrent browsers against a single dev/staging target.
Log/report instead of silently swallowing worker_thread_init exceptions in both the page-worker and batch-pool paths, and document the single-interpreter assumption behind the web server's global pool/rate limiter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_QueueWriter emitted a generic 'log' SSE event for every line AND a 'progress'/'finding' event for Testing:/[SEVERITY] lines, causing run.js to render those lines twice in the live log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@billrichards billrichards merged commit a5cf4cd into main Jun 13, 2026
15 checks passed
@billrichards billrichards deleted the claude/eloquent-euler-a4ry0 branch June 13, 2026 21:37
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.

2 participants