The single ThreadingHTTPServer ran pure-Python v5 score / block_scan / report reference under one GIL, so the gate's two slots shared a core. start_server now starts a spawn pool (workers = heavy concurrency, PYTHONHASHSEED=0, engine pre-imported); those three routes call the same compute method in a worker. Over HTTP: score alone 4.74 s, two at once 5.01 / 4.58 s (1.06x; in-process 1.56x). Results equal except per-process fingerprints; report markdown equal. ~165 MB extra. A crashed pool answers inline and restarts; off with JYOTISH_HEAVY_COMPUTE_PROCESSES=0; tests calling compute methods unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
"""Issue #133: heavy routes run in worker processes when the server starts the pool."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from scripts.api_process_pool import pool
|
|
from scripts.jyotish_api_server import REPO_ROOT, SCRIPTS_DIR, BadRequest, JyotishComputeService
|
|
from tests.test_rectification_v5_vedastro_validation import request as v5_request
|
|
|
|
NOISE = ("fingerprint", "feature_hash", "_hash", "elapsed", "duration", "_ms", "timing")
|
|
|
|
|
|
def _body() -> dict:
|
|
body = v5_request()
|
|
body.pop("candidate_times")
|
|
return body
|
|
|
|
|
|
def _strip(value):
|
|
if isinstance(value, dict):
|
|
return {k: _strip(v) for k, v in value.items() if not any(n in k for n in NOISE)}
|
|
if isinstance(value, list):
|
|
return [_strip(v) for v in value]
|
|
return value
|
|
|
|
|
|
@pytest.fixture
|
|
def started_pool():
|
|
assert pool.start(2, [REPO_ROOT, SCRIPTS_DIR])
|
|
yield
|
|
pool.stop()
|
|
|
|
|
|
def test_without_the_pool_routes_run_in_process():
|
|
assert not pool.active()
|
|
calls = []
|
|
assert pool.run("_compute_rectification_v5_score", {"x": 1}, lambda body: calls.append(body) or "inline") == "inline"
|
|
assert calls == [{"x": 1}]
|
|
|
|
|
|
def test_a_pooled_score_equals_the_in_process_one(started_pool):
|
|
inline = JyotishComputeService()._compute_rectification_v5_score
|
|
pooled = pool.run("_compute_rectification_v5_score", _body(), lambda body: pytest.fail("ran inline"))
|
|
assert pooled["success"] is True
|
|
# Fingerprints already vary per process (ERR-111); everything else is identical.
|
|
assert _strip(pooled) == _strip(inline(_body()))
|
|
|
|
|
|
def test_bad_input_is_still_a_bad_request(started_pool):
|
|
body = _body()
|
|
body["start_time"] = "not-a-time"
|
|
with pytest.raises((BadRequest, ValueError, KeyError)):
|
|
pool.run("_compute_rectification_v5_score", body, lambda _body: pytest.fail("ran inline"))
|
|
|
|
|
|
def test_routes_outside_the_list_never_leave_the_process(started_pool):
|
|
assert pool.run("_compute_chart", {}, lambda body: "inline") == "inline"
|
|
|
|
|
|
def test_a_crashed_pool_answers_in_process_and_comes_back(started_pool):
|
|
victim = next(iter(pool._executor._processes.values()))
|
|
os.kill(victim.pid, 9)
|
|
time.sleep(0.5)
|
|
calls = []
|
|
result = pool.run("_compute_rectification_v5_score", _body(), lambda body: calls.append(1) or "inline")
|
|
assert result == "inline" and calls == [1]
|
|
for _ in range(120):
|
|
if pool.active():
|
|
break
|
|
time.sleep(0.5)
|
|
assert pool.active(), "the pool restarts in the background"
|
|
|
|
|
|
def test_the_switch_turns_the_pool_off(monkeypatch):
|
|
monkeypatch.setenv(pool.ENV_PROCESSES, "0")
|
|
assert pool.start(2, [REPO_ROOT, SCRIPTS_DIR]) is False
|
|
assert not pool.active()
|