feat(1.2): prefork supervisor primitive + tests (C, CoW gate passed)
CoW measurement on CPython 3.14: forked idle child keeps ~71MB shared, dirties ~1MB private; working child ~26MB. PEP 683 immortal objects keep code/module pages clean so gc.freeze() is unnecessary (freeze==nofreeze). prefork.run_fleet: master imports the base floor once, forks one child per worker (own process/GIL, CoW-shared floor), reaps + restarts with backoff, graceful SIGTERM->SIGKILL shutdown. Not yet wired to a command (that lands when 1.2 picks the target worker set).
This commit is contained in:
55
tests/prefork_driver.py
Normal file
55
tests/prefork_driver.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Standalone driver for the prefork supervisor — runnable directly OR via
|
||||
tests/test_prefork.py (which execs it in a subprocess so no fork happens inside
|
||||
the pytest/xdist worker).
|
||||
|
||||
python tests/prefork_driver.py <out_dir>
|
||||
|
||||
Forks two fake workers under decnet.prefork.run_fleet:
|
||||
* "tick" — append a line every 0.2s forever (proves a worker runs & stays up)
|
||||
* "crasher" — write a marker then exit(1) (proves restart-on-crash)
|
||||
Runs for ~2s via stop_after, then shuts the fleet down. Writes results into
|
||||
<out_dir>; the caller asserts on them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Running this file as a script puts its own dir (tests/) on sys.path[0], which
|
||||
# shadows the stdlib `logging` via tests/logging/. Drop it before importing
|
||||
# decnet (still importable — it's installed in the venv).
|
||||
if sys.path and os.path.basename(sys.path[0]) == "tests":
|
||||
sys.path.pop(0)
|
||||
|
||||
from decnet.prefork import run_fleet # noqa: E402
|
||||
|
||||
|
||||
def main(out: str) -> None:
|
||||
tick_log = os.path.join(out, "tick.log")
|
||||
crash_log = os.path.join(out, "crash.log")
|
||||
|
||||
def tick() -> None:
|
||||
while True:
|
||||
with open(tick_log, "a") as f:
|
||||
f.write("t\n")
|
||||
time.sleep(0.2)
|
||||
|
||||
def crasher() -> None:
|
||||
with open(crash_log, "a") as f:
|
||||
f.write("c\n")
|
||||
time.sleep(0.15)
|
||||
os._exit(1)
|
||||
|
||||
# Fast backoff so we observe multiple restarts inside the short window.
|
||||
run_fleet(
|
||||
{"tick": tick, "crasher": crasher},
|
||||
max_backoff=0.2,
|
||||
poll_interval=0.05,
|
||||
stop_after=2.0,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1] if len(sys.argv) > 1 else ".")
|
||||
40
tests/test_prefork.py
Normal file
40
tests/test_prefork.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Prefork supervisor behaviour, exercised via a subprocess driver so no fork
|
||||
happens inside the pytest/xdist worker (which would be unsafe).
|
||||
|
||||
Proves: workers fork and run, a crashing worker is restarted with backoff, and
|
||||
the fleet shuts down cleanly (stop_after returns, no orphaned children).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_prefork_runs_and_restarts(tmp_path: pathlib.Path):
|
||||
driver = pathlib.Path(__file__).parent / "prefork_driver.py"
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(driver), str(tmp_path)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
assert proc.returncode == 0, f"driver failed:\n{proc.stderr}"
|
||||
|
||||
tick = (tmp_path / "tick.log").read_text().splitlines()
|
||||
crash = (tmp_path / "crash.log").read_text().splitlines()
|
||||
|
||||
# tick ran continuously for ~2s at 0.2s cadence → several lines.
|
||||
assert len(tick) >= 5, f"tick worker did not stay up: {len(tick)} lines"
|
||||
# crasher died fast and was restarted repeatedly → many markers.
|
||||
assert len(crash) >= 3, f"crasher was not restarted: {len(crash)} markers"
|
||||
|
||||
|
||||
def test_empty_fleet_returns(tmp_path: pathlib.Path):
|
||||
# run_fleet([]) must be a no-op, not hang.
|
||||
code = (
|
||||
"from decnet.prefork import run_fleet; run_fleet({}, stop_after=5)"
|
||||
)
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", code], capture_output=True, text=True, timeout=15
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
Reference in New Issue
Block a user