"""Worker engine recommend — mode queue (bàn giao 14, 04/08/2026). Process TÁCH KHỎI API: nạp env + JIT Numba MỘT lần lúc boot, xong hết mới bật heartbeat + nhận job (bối cảnh #3: JIT nguội ~40s/process, health không được báo alive lúc còn đang JIT). Vòng đời sau đó: BRPOP pc:jobs:recommend → app.engine.handle_job (MỘT code path với in-process — bẫy 27/07) → LPUSH pc:result:{job_id} (TTL dọn rác) Heartbeat: SET pc:worker:heartbeat TTL 15s mỗi vòng chờ — BRPOP timeout 5s nên nhịp ~5s lúc rảnh, mỗi job lúc bận (search gate < 5s/cú → không hụt TTL). Ctrl+C thoát sạch: xoá heartbeat để /api/health thấy worker chết NGAY thay vì đợi TTL; job đang dở vẫn trả reply xong mới thoát (BRPOP là điểm ngắt). Chạy tay (Redis: docker compose -f docker-compose.dev.yml up -d redis): set REDIS_URL=redis://localhost:6379/0 python scripts\\engine_worker.py Launcher: D:\\Khoa luan\\run_engine_worker.bat (ngoài repo, như mọi launcher). """ from __future__ import annotations import os import sys import time from pathlib import Path # Repo KHÔNG cài package: src/ cho poolcoach_rl, ROOT cho app.* (quy ước # scripts/ — xem CLAUDE.md; thiếu dòng này là lỗi đã tái phạm 24/07). ROOT = Path(__file__).resolve().parents[1] for _p in (ROOT / "src", ROOT): if str(_p) not in sys.path: sys.path.insert(0, str(_p)) from app import engine, jobqueue # noqa: E402 def main() -> int: if not os.environ.get("REDIS_URL"): print("engine_worker: thieu env REDIS_URL -- worker chi co nghia o " "mode queue (vi du: redis://localhost:6379/0)", flush=True) return 2 jobqueue.setup() t0 = time.time() from poolcoach_rl.envs import PositionPlayEnv from poolcoach_rl.recommend import warmup env_h = PositionPlayEnv() print(f"[worker] env san sang sau {time.time() - t0:.1f}s -- warmup JIT " f"Numba (~40s lan dau)...", flush=True) warmup_s = warmup(env_h) print(f"[worker] JIT xong sau {warmup_s:.1f}s -- bat heartbeat, cho job " f"tren '{jobqueue.JOBS_KEY}'", flush=True) import redis served = 0 try: while True: try: jobqueue.beat() t_job = time.perf_counter() if jobqueue.serve_one(lambda p: engine.handle_job(p, env_h), timeout_s=5): served += 1 print(f"[worker] job #{served} xong sau " f"{time.perf_counter() - t_job:.2f}s", flush=True) except redis.RedisError as e: # Redis chết/khởi động lại KHÔNG được giết worker đã JIT ấm # — chờ rồi thử lại; API phía kia tự trả 503 trong lúc này. print(f"[worker] mat ket noi Redis ({type(e).__name__}: {e}) " f"-- thu lai sau 2s", flush=True) time.sleep(2) except KeyboardInterrupt: print(f"\n[worker] Ctrl+C -- thoat sach sau {served} job", flush=True) finally: jobqueue.clear_heartbeat() # health thay worker chet NGAY, khong doi TTL jobqueue.teardown() return 0 if __name__ == "__main__": sys.exit(main())