File size: 3,352 Bytes
78738de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
"""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())