feat(io_pool, warmup): add shared 4-thread pool + WarmupManager
Phase 2 Tasks T2.1-T2.4 of the startup_speedup_20260606 track.
NEW: src/io_pool.py
make_io_pool() factory: 4-worker ThreadPoolExecutor with
thread_name_prefix='controller-io'. The sanctioned way for any
background work. Replaces ad-hoc threading.Thread() calls per
the 'no new threads' rule.
NEW: src/warmup.py
WarmupManager: manages a list of modules to import on the shared
pool. Public API:
.submit(modules) - start warmup (call once)
.status() - {pending, completed, failed}
.is_done() - bool
.wait(timeout) - block until done
.on_complete(callback) - register completion callback
.reset() - clear state
Thread-safe (lock-guarded). 10 tests cover all paths.
NEW: tests/test_io_pool.py (4 tests):
- ThreadPoolExecutor returned
- 4 workers
- Threads named 'controller-io-*'
- Jobs run in parallel (barrier test)
NEW: tests/test_warmup.py (10 tests):
- One job per module submitted
- Initial pending list correct
- Failed imports tracked
- Done event set after all complete
- wait() blocks until done
- on_complete callback fires (and immediately if already done)
- Modules actually end up in sys.modules
- reset() clears state
- Jobs run concurrently (not serially)
All 14 tests pass. AppController integration is the next commit.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""Tests for src/io_pool.py (the shared 4-thread job pool on AppController)."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.io_pool import make_io_pool, IO_POOL_MAX_WORKERS # noqa: E402
|
||||
|
||||
|
||||
def test_make_io_pool_returns_thread_pool_executor() -> None:
|
||||
pool = make_io_pool()
|
||||
assert isinstance(pool, ThreadPoolExecutor)
|
||||
pool.shutdown(wait=False)
|
||||
|
||||
|
||||
def test_make_io_pool_has_four_workers() -> None:
|
||||
pool = make_io_pool()
|
||||
assert pool._max_workers == IO_POOL_MAX_WORKERS == 4
|
||||
pool.shutdown(wait=False)
|
||||
|
||||
|
||||
def test_make_io_pool_workers_named_controller_io() -> None:
|
||||
pool = make_io_pool()
|
||||
|
||||
def capture() -> str:
|
||||
return threading.current_thread().name
|
||||
|
||||
fut = pool.submit(capture)
|
||||
name = fut.result(timeout=5)
|
||||
assert name.startswith("controller-io"), f"got {name!r}"
|
||||
pool.shutdown(wait=False)
|
||||
|
||||
|
||||
def test_make_io_pool_runs_jobs_in_parallel() -> None:
|
||||
pool = make_io_pool()
|
||||
barrier = threading.Barrier(4)
|
||||
results: list[float] = []
|
||||
|
||||
def wait_at_barrier() -> float:
|
||||
t0 = time.perf_counter()
|
||||
barrier.wait(timeout=5)
|
||||
return time.perf_counter() - t0
|
||||
|
||||
futs = [pool.submit(wait_at_barrier) for _ in range(4)]
|
||||
durations = [f.result(timeout=5) for f in futs]
|
||||
assert all(d < 0.5 for d in durations), f"jobs did not run in parallel: {durations}"
|
||||
pool.shutdown(wait=False)
|
||||
Reference in New Issue
Block a user