Files
shelfmark/tests/bypass/test_warm_helper.py
T
helgehelge123andhelgehelge123 7b9c416df8 perf(bypass): keep the helper subprocess alive between bypasses (#1222)
Every protected request spawns a fresh helper subprocess, paying
interpreter start and imports before any work begins. Measured inside
the container, five consecutive runs of `python -c "import
shelfmark.bypass.internal_bypasser"`:

```
3.53s  3.45s  3.55s  3.54s  3.46s
```

A single search issues several protected requests, so that is paid
several times over per search.

## What changed

The helper now serves one JSON request per line of stdin until the
parent closes the pipe, and an idle timer
(`BYPASS_BROWSER_IDLE_TIMEOUT`, default 180s) shuts it down once
searching stops.

Answers still travel by result file, but the file is now written to a
`.part` path and renamed into place — the parent treats the file's
existence as the answer, so it must never observe a half-written one.
stdout and stderr stay attached to the parent's, so helper logs keep
appearing in `docker logs` exactly as before.

Failure handling, since a warm helper is exposed to more of it than a
per-request one ever was:

| Situation | Handling |
| --- | --- |
| Helper died between requests | Detected via `poll()`, respawned |
| Pipe broken at write time (`poll()` can miss this) | One retry on a
fresh process; a fresh one failing there is a real failure |
| Helper exits without writing a result | `RuntimeError` naming the exit
code |
| Wedged past the timeout, or cancelled mid-bypass | Helper killed, then
`_cleanup_orphan_processes` because a killed helper never got to close
Chrome |
| Idle reaper racing an arriving request | Re-checks the deadline under
the lock and re-arms instead of killing a helper that just did work |

The DNS config now travels with every request rather than only at spawn:
a warm helper outlives changes the parent makes to its provider.

## `BYPASS_REUSE_BROWSER`, off by default

This parks the CDP driver between bypasses. A driver's websockets are
bound to the loop that opened them and cannot outlive their process, so
the persistent helper is what makes this possible at all — and the warm
path runs on `_CDP_WORKER`'s long-lived loop rather than `asyncio.run`
for the same reason.

The mechanism works. With it on, the browser start disappears from the
second request onward: 0.7s from `Reusing warm Chrome browser` to the
first bypass attempt, against roughly 16s cold.

**It still ships off, because a matched-pair test shows it is a net loss
against DDoS-Guard.** Each round primed with one cold bypass, waited
10s, then measured a second — identical timing in both arms, only the
browser strategy differing, order balanced (fresh, warm, warm, fresh) so
drift over the session cannot masquerade as an effect:

| Arm | Measured request |
| --- | --- |
| fresh browser | 42.8s, 40.6s |
| warm browser | 57.1s, 59.6s |

Spread within each arm is 2.2s and 2.5s, against 16.7s between them.
Reuse removes the ~15s browser start and then gives back roughly twice
that in solving: a returning browser draws a harder challenge. Where the
cold browser is through on the second bypass method, the warm one fails
the first three and only `_bypass_method_humanlike` gets it, at ~30s for
that method alone.

Worth separating from a second effect I ran into while measuring: five
back-to-back searches slow from ~32s to 51–98s with reuse **disabled**
as well, so DDoS-Guard escalates on request rate independently of any of
this. That is why the pairs above are timed identically rather than
simply run in sequence. It is the larger of the two effects, but not
something this project can patch around.

Reuse is left available rather than dropped because Cloudflare sites may
not respond the same way, and because the two concerns are independent:
the helper start is pure overhead and always worth removing, the browser
is not.

## Verification

- 2559 unit tests pass (2542 before, 17 added in
`tests/bypass/test_warm_browser.py`)
- `ruff check`, `ruff format`, `basedpyright` over backend and tests,
and `vulture` all clean
- `docs/environment-variables.md` regenerated via
`scripts/generate_env_docs.py`
- Live against Anna's Archive on a warm helper: searches return their
usual ~760KB and 667 results, the app's own search warm-up completes
with 50 results, and the container is left with no orphan
chrome/Xvfb/ffmpeg processes

Happy to drop the `BYPASS_REUSE_BROWSER` half entirely if you would
rather not carry a default-off path — the helper persistence stands on
its own.

Co-authored-by: helgehelge123 <helge.neumann@zollsoft.de>
2026-08-20 19:00:06 -04:00

434 lines
16 KiB
Python

"""Tests for keeping the bypass helper process alive between requests.
The browser is deliberately not kept: every bypass starts and closes its own Chrome. What
survives is the helper process, whose interpreter start and imports are pure overhead.
"""
import asyncio
import json
import pytest
class _FakeStdin:
def __init__(self) -> None:
self.closed = False
self.written: list[str] = []
def write(self, data: str) -> None:
if self.closed:
raise BrokenPipeError("stdin is closed")
self.written.append(data)
def flush(self) -> None:
return None
def close(self) -> None:
self.closed = True
class _FakeProc:
"""Enough of subprocess.Popen for the helper's process bookkeeping."""
_next_pid = 90001
def __init__(self) -> None:
self.stdin = _FakeStdin()
self.returncode: int | None = None
# A pid nothing may actually be signalled by: _terminate_helper_session is patched
# out in these tests, and a stray killpg on a live pid would take out the test run.
type(self)._next_pid += 1
self.pid = type(self)._next_pid
def poll(self) -> int | None:
return self.returncode
def wait(self, timeout: float | None = None) -> int:
if self.returncode is None:
self.returncode = 0
return self.returncode
def kill(self) -> None:
self.returncode = -9
def _helper_with_fake_spawn(monkeypatch, procs: list[_FakeProc], terminated=None):
"""Build a helper that hands out fake processes and never arms a real timer."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
def _spawn(_self) -> _FakeProc:
proc = _FakeProc()
procs.append(proc)
return proc
def _terminate(proc) -> None:
if terminated is not None:
terminated.append(proc)
monkeypatch.setattr(internal_bypasser._BypassHelper, "_spawn", _spawn)
monkeypatch.setattr(internal_bypasser._BypassHelper, "_idle_timeout", lambda _self: 0.0)
monkeypatch.setattr(internal_bypasser, "_terminate_helper_session", _terminate)
return internal_bypasser._BypassHelper()
def _answered_payload(tmp_path, name: str = "result.json") -> dict:
"""A request whose result file already exists, so the helper resolves immediately."""
result_path = tmp_path / name
result_path.write_text(json.dumps({"ok": True, "html": "<html/>"}), encoding="utf-8")
return {"url": "https://example.com", "retry": 1, "result_path": str(result_path)}
def test_helper_serves_consecutive_requests_from_one_process(monkeypatch, tmp_path):
"""The point of the whole thing: request two and three must not re-pay the spawn."""
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
for i in range(3):
result = helper.run(_answered_payload(tmp_path, f"r{i}.json"), timeout=5, cancel_flag=None)
assert result["ok"] is True
assert len(procs) == 1, "each request spawned its own helper"
assert len(procs[0].stdin.written) == 3
assert all(line.endswith("\n") for line in procs[0].stdin.written), (
"requests must be newline-delimited or the helper's loop cannot split them"
)
def test_helper_respawns_after_the_previous_one_died(monkeypatch, tmp_path):
"""A helper can be reaped while idle; the next request must not fail on it."""
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
helper.run(_answered_payload(tmp_path, "a.json"), timeout=5, cancel_flag=None)
procs[0].returncode = 1 # died between requests
result = helper.run(_answered_payload(tmp_path, "b.json"), timeout=5, cancel_flag=None)
assert result["ok"] is True
assert len(procs) == 2
def test_helper_retries_once_when_the_pipe_breaks_on_write(monkeypatch, tmp_path):
"""poll() can still say alive when the far end is already gone."""
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
helper.run(_answered_payload(tmp_path, "a.json"), timeout=5, cancel_flag=None)
procs[0].stdin.closed = True # pipe gone, but poll() still reports running
result = helper.run(_answered_payload(tmp_path, "b.json"), timeout=5, cancel_flag=None)
assert result["ok"] is True
assert len(procs) == 2
def test_helper_reports_a_helper_that_exits_without_answering(monkeypatch, tmp_path):
import shelfmark.bypass.internal_bypasser as internal_bypasser
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
payload = {
"url": "https://example.com",
"retry": 1,
"result_path": str(tmp_path / "never-written.json"),
}
def _die_on_write(_self, proc, _line) -> None:
proc.returncode = 3
monkeypatch.setattr(internal_bypasser._BypassHelper, "_write", _die_on_write)
with pytest.raises(RuntimeError, match="exited without a result"):
helper.run(payload, timeout=5, cancel_flag=None)
def test_helper_times_out_and_discards_the_wedged_process(monkeypatch, tmp_path):
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
payload = {
"url": "https://example.com",
"retry": 1,
"result_path": str(tmp_path / "never-written.json"),
}
with pytest.raises(TimeoutError):
helper.run(payload, timeout=0.05, cancel_flag=None)
assert helper._proc is None, "a wedged helper must not be handed to the next request"
def test_idle_reaper_rearms_when_work_arrived_while_it_waited(monkeypatch, tmp_path):
"""The timer fires on its own thread and can lose the race against a new request."""
import time
import shelfmark.bypass.internal_bypasser as internal_bypasser
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
helper.run(_answered_payload(tmp_path), timeout=5, cancel_flag=None)
rearmed: list[bool] = []
monkeypatch.setattr(internal_bypasser._BypassHelper, "_idle_timeout", lambda _self: 3600.0)
monkeypatch.setattr(
internal_bypasser._BypassHelper, "_arm_idle_timer", lambda _self: rearmed.append(True)
)
helper._last_used = time.monotonic()
helper._reap_if_idle()
assert rearmed == [True]
assert helper._proc is not None, "helper was killed despite recent work"
def test_idle_reaper_closes_a_genuinely_idle_helper(monkeypatch, tmp_path):
import time
import shelfmark.bypass.internal_bypasser as internal_bypasser
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
helper.run(_answered_payload(tmp_path), timeout=5, cancel_flag=None)
monkeypatch.setattr(internal_bypasser._BypassHelper, "_idle_timeout", lambda _self: 60.0)
helper._last_used = time.monotonic() - 120
helper._reap_if_idle()
assert helper._proc is None
assert procs[0].stdin.closed
def test_discard_tears_down_the_whole_session(monkeypatch, tmp_path):
"""Dropping the helper must reach its browser tree, not just the helper itself.
The helper is a session leader (start_new_session), so a Chrome left behind by one
killed mid-bypass would keep a process group alive that the cleanup sweep is then not
allowed to reclaim - the leak #1231 was about.
"""
procs: list[_FakeProc] = []
terminated: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs, terminated)
helper.run(_answered_payload(tmp_path), timeout=5, cancel_flag=None)
helper._discard()
assert terminated == [procs[0]]
def test_helper_asks_before_it_kills(monkeypatch, tmp_path):
"""An idle helper should get to exit on its own; the kill is the fallback."""
procs: list[_FakeProc] = []
helper = _helper_with_fake_spawn(monkeypatch, procs)
helper.run(_answered_payload(tmp_path), timeout=5, cancel_flag=None)
helper._discard()
assert procs[0].stdin.closed, "stdin must be closed to end the helper's request loop"
assert procs[0].returncode == 0, "an idle helper should have exited on its own"
def _bypass_with_recorded_driver(monkeypatch, get_impl):
"""Wire up a bypass whose browser creation and closing are observable."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
driver = object()
closed: list[object] = []
async def _create(_url):
return driver
async def _close(drv):
closed.append(drv)
monkeypatch.setattr(internal_bypasser, "_create_cdp_browser", _create)
monkeypatch.setattr(internal_bypasser, "_get", get_impl)
monkeypatch.setattr(internal_bypasser, "_close_cdp_driver", _close)
return driver, closed
def test_successful_bypass_closes_its_browser(monkeypatch):
"""A living helper must not accumulate browsers: each bypass ends with Chrome gone."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
async def _get(_url, _driver, _cancel=None):
return "<html>ok</html>"
driver, closed = _bypass_with_recorded_driver(monkeypatch, _get)
result = internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
assert result == "<html>ok</html>"
assert closed == [driver]
def test_failed_bypass_closes_its_browser(monkeypatch):
"""The same has to hold when the bypass raises on its way out."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
async def _get(_url, _driver, _cancel=None):
raise internal_bypasser.BypassCancelledError("cancelled")
driver, closed = _bypass_with_recorded_driver(monkeypatch, _get)
with pytest.raises(internal_bypasser.BypassCancelledError):
internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
assert closed == [driver]
def test_child_process_serves_every_line_it_is_given(monkeypatch, tmp_path):
"""One helper, several requests: the loop is what saves the repeated process start."""
import io
import shelfmark.bypass.internal_bypasser as internal_bypasser
urls: list[str] = []
def _fake_get(url, retry=None, cancel_flag=None):
urls.append(url)
return f"<html>{url}</html>"
requests = [
{"url": "https://example.com/one", "retry": 1, "result_path": str(tmp_path / "1.json")},
{"url": "https://example.com/two", "retry": 1, "result_path": str(tmp_path / "2.json")},
]
stdin = io.StringIO("\n".join(json.dumps(request) for request in requests) + "\n")
monkeypatch.setattr(internal_bypasser, "get", _fake_get)
monkeypatch.setattr(internal_bypasser.sys, "stdin", stdin)
assert internal_bypasser._run_child_process() == 0
assert urls == ["https://example.com/one", "https://example.com/two"]
for index, request in enumerate(requests, start=1):
result = json.loads((tmp_path / f"{index}.json").read_text(encoding="utf-8"))
assert result["ok"] is True
assert result["html"] == f"<html>{request['url']}</html>"
def test_child_process_keeps_serving_after_a_failed_request(monkeypatch, tmp_path):
"""One failing URL must not take the helper - and everything queued - down."""
import io
import shelfmark.bypass.internal_bypasser as internal_bypasser
def _fake_get(url, retry=None, cancel_flag=None):
if url.endswith("boom"):
raise RuntimeError("bypass exploded")
return "<html>ok</html>"
requests = [
{"url": "https://example.com/boom", "retry": 1, "result_path": str(tmp_path / "1.json")},
{"url": "https://example.com/fine", "retry": 1, "result_path": str(tmp_path / "2.json")},
]
stdin = io.StringIO("\n".join(json.dumps(request) for request in requests) + "\n")
monkeypatch.setattr(internal_bypasser, "get", _fake_get)
monkeypatch.setattr(internal_bypasser.sys, "stdin", stdin)
assert internal_bypasser._run_child_process() == 0
failed = json.loads((tmp_path / "1.json").read_text(encoding="utf-8"))
assert failed["ok"] is False
assert failed["error"] == "bypass exploded"
served = json.loads((tmp_path / "2.json").read_text(encoding="utf-8"))
assert served["ok"] is True
def test_result_file_becomes_visible_only_when_complete(tmp_path):
"""The parent treats the file's existence as the answer, so no partial writes."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
result_path = tmp_path / "result.json"
internal_bypasser._publish_result(result_path, {"ok": True, "html": "<html/>"})
assert json.loads(result_path.read_text(encoding="utf-8"))["ok"] is True
assert list(tmp_path.iterdir()) == [result_path], "temporary file was left behind"
def test_child_bypass_runs_on_the_long_lived_worker_loop(monkeypatch):
"""A helper serving many requests must not build and close a loop per bypass.
asyncio.run() owns the loop for one call and closes it on the way out, which is why the
child goes through the worker unconditionally: one loop for the process's lifetime.
"""
import shelfmark.bypass.internal_bypasser as internal_bypasser
monkeypatch.setenv("SHELFMARK_INTERNAL_BYPASSER_CHILD", "1")
loops: list[asyncio.AbstractEventLoop] = []
async def _record_loop(_url, _driver, _cancel=None):
loops.append(asyncio.get_running_loop())
return "<html>ok</html>"
_bypass_with_recorded_driver(monkeypatch, _record_loop)
internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
assert len(loops) == 2
assert loops[0] is loops[1], "second bypass ran on a different loop than the first"
assert not loops[0].is_closed()
def test_child_bypass_carries_its_own_deadline(monkeypatch):
"""The child bounds itself, rather than relying only on the parent's deadline."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
monkeypatch.setenv("SHELFMARK_INTERNAL_BYPASSER_CHILD", "1")
timeouts: list[float | None] = []
real_run = internal_bypasser._CDP_WORKER.run
def _record_timeout(coro, timeout=None):
timeouts.append(timeout)
return real_run(coro, timeout=timeout)
async def _get(_url, _driver, _cancel=None):
return "<html>ok</html>"
_bypass_with_recorded_driver(monkeypatch, _get)
monkeypatch.setattr(internal_bypasser._CDP_WORKER, "run", _record_timeout)
internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
assert timeouts == [internal_bypasser._CHILD_BYPASS_TIMEOUT_SECONDS]
def test_child_deadline_leaves_the_parent_room_to_hear_the_answer(monkeypatch):
"""If the parent gave up first it could only kill the helper, losing a warm process."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
assert (
internal_bypasser._CHILD_BYPASS_TIMEOUT_SECONDS
< internal_bypasser._BYPASS_SUBPROCESS_TIMEOUT_SECONDS
)
def test_in_process_bypass_keeps_the_parents_budget(monkeypatch):
"""Non-Docker installs run in-process, where there is no helper to outlive anything."""
import shelfmark.bypass.internal_bypasser as internal_bypasser
monkeypatch.delenv("SHELFMARK_INTERNAL_BYPASSER_CHILD", raising=False)
timeouts: list[float | None] = []
real_run = internal_bypasser._CDP_WORKER.run
def _record_timeout(coro, timeout=None):
timeouts.append(timeout)
return real_run(coro, timeout=timeout)
async def _get(_url, _driver, _cancel=None):
return "<html>ok</html>"
_bypass_with_recorded_driver(monkeypatch, _get)
monkeypatch.setattr(internal_bypasser._CDP_WORKER, "run", _record_timeout)
internal_bypasser._run_bypass_in_current_process("https://example.com", 1)
assert timeouts == [internal_bypasser._IN_PROCESS_BYPASS_TIMEOUT_SECONDS]