147 lines
4.5 KiB
Python
147 lines
4.5 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
AO3 Mirror - 被动守护进程 (Daemon v2)
|
|||
|
|
不再主动 e2e 检查或重启 healthy worker。
|
|||
|
|
职责:
|
|||
|
|
1. 每 30s 检查 worker 是否活着,只有连续 3 轮都挂掉才重启
|
|||
|
|
2. 不做 e2e,不做 caddy 检查(让 systemd 管)
|
|||
|
|
3. 每 5min 写状态文件
|
|||
|
|
"""
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import subprocess
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import logging
|
|||
|
|
import signal
|
|||
|
|
|
|||
|
|
BASE_DIR = "/home/ubuntu/ao3-mirror"
|
|||
|
|
SHM_DIR = "/dev/shm/ao3"
|
|||
|
|
WORKER_PORTS = [8081, 8082]
|
|||
|
|
CHECK_INTERVAL = 30
|
|||
|
|
STATUS_FILE = f"{SHM_DIR}/status.json"
|
|||
|
|
|
|||
|
|
os.makedirs(SHM_DIR, exist_ok=True)
|
|||
|
|
|
|||
|
|
logging.basicConfig(
|
|||
|
|
level=logging.INFO,
|
|||
|
|
format="%(asctime)s [DAEMON] %(message)s",
|
|||
|
|
handlers=[logging.StreamHandler()],
|
|||
|
|
)
|
|||
|
|
logger = logging.getLogger("ao3-daemon")
|
|||
|
|
|
|||
|
|
state = {
|
|||
|
|
"started_at": time.time(),
|
|||
|
|
"consecutive_dead": {p: 0 for p in WORKER_PORTS},
|
|||
|
|
"restarts_today": 0,
|
|||
|
|
"alerts": [],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _http_get(url: str, timeout: int = 5) -> tuple[bool, str]:
|
|||
|
|
import urllib.request
|
|||
|
|
try:
|
|||
|
|
req = urllib.request.Request(url)
|
|||
|
|
resp = urllib.request.urlopen(req, timeout=timeout)
|
|||
|
|
return (resp.status == 200, resp.read().decode("utf-8", errors="replace")[:200])
|
|||
|
|
except Exception as e:
|
|||
|
|
return (False, str(e)[:200])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check_worker(port: int) -> bool:
|
|||
|
|
ok, body = _http_get(f"http://127.0.0.1:{port}/health", timeout=4)
|
|||
|
|
if ok:
|
|||
|
|
try:
|
|||
|
|
return json.loads(body).get("status") == "ok"
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def restart_worker(port: int):
|
|||
|
|
logger.warning(f"Restarting worker on port {port}")
|
|||
|
|
state["restarts_today"] += 1
|
|||
|
|
subprocess.run(["fuser", "-k", f"{port}/tcp"], capture_output=True, timeout=10)
|
|||
|
|
time.sleep(1)
|
|||
|
|
python_bin = "/home/ubuntu/.hermes/hermes-agent/venv/bin/python3"
|
|||
|
|
i = WORKER_PORTS.index(port)
|
|||
|
|
log_file = f"{BASE_DIR}/worker-{i}.log"
|
|||
|
|
proc = subprocess.Popen(
|
|||
|
|
["taskset", "-c", str(i), python_bin, "-m", "uvicorn", "app:app",
|
|||
|
|
"--host", "127.0.0.1", "--port", str(port), "--workers", "1",
|
|||
|
|
"--loop", "uvloop", "--http", "httptools",
|
|||
|
|
"--log-level", "warning", "--timeout-keep-alive", "30"],
|
|||
|
|
cwd=BASE_DIR, stdout=open(log_file, "a"), stderr=open(log_file, "a"),
|
|||
|
|
)
|
|||
|
|
pid_file = f"{SHM_DIR}/worker-{i}.pid"
|
|||
|
|
with open(pid_file, "w") as f:
|
|||
|
|
f.write(str(proc.pid))
|
|||
|
|
logger.info(f"Worker {port} restarted (PID: {proc.pid})")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check_workers():
|
|||
|
|
"""Only restart if worker has been dead for 3 consecutive checks (90s)."""
|
|||
|
|
for port in WORKER_PORTS:
|
|||
|
|
if check_worker(port):
|
|||
|
|
state["consecutive_dead"][port] = 0
|
|||
|
|
else:
|
|||
|
|
state["consecutive_dead"][port] += 1
|
|||
|
|
if state["consecutive_dead"][port] >= 3:
|
|||
|
|
restart_worker(port)
|
|||
|
|
state["consecutive_dead"][port] = 0
|
|||
|
|
dead_count = sum(1 for p in WORKER_PORTS if state["consecutive_dead"][p] > 0)
|
|||
|
|
alive = len(WORKER_PORTS) - dead_count
|
|||
|
|
if dead_count > 0:
|
|||
|
|
logger.info(f"{alive}/{len(WORKER_PORTS)} workers alive ({dead_count} degraded)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def save_status():
|
|||
|
|
worker_status = {}
|
|||
|
|
for i, port in enumerate(WORKER_PORTS):
|
|||
|
|
worker_status[f"worker_{i}"] = {
|
|||
|
|
"port": port, "healthy": check_worker(port), "uptime_s": int(time.time() - state["started_at"]),
|
|||
|
|
}
|
|||
|
|
data = {
|
|||
|
|
"timestamp": time.time(), "uptime": int(time.time() - state["started_at"]),
|
|||
|
|
"workers": worker_status, "restarts_today": state["restarts_today"],
|
|||
|
|
}
|
|||
|
|
with open(STATUS_FILE + ".tmp", "w") as f:
|
|||
|
|
json.dump(data, f)
|
|||
|
|
os.rename(STATUS_FILE + ".tmp", STATUS_FILE)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def handle_signal(signum, frame):
|
|||
|
|
logger.info(f"Signal {signum}, exiting")
|
|||
|
|
sys.exit(0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
signal.signal(signal.SIGTERM, handle_signal)
|
|||
|
|
signal.signal(signal.SIGINT, handle_signal)
|
|||
|
|
logger.info("Daemon v2 started (passive mode)")
|
|||
|
|
logger.info(f"Watching {WORKER_PORTS}, check every {CHECK_INTERVAL}s, restart after 3 missed checks")
|
|||
|
|
|
|||
|
|
# Initial startup: start all workers immediately
|
|||
|
|
for port in WORKER_PORTS:
|
|||
|
|
if not check_worker(port):
|
|||
|
|
restart_worker(port)
|
|||
|
|
time.sleep(2)
|
|||
|
|
state["consecutive_dead"][port] = 0
|
|||
|
|
|
|||
|
|
time.sleep(5) # Let them settle
|
|||
|
|
last_status = 0
|
|||
|
|
while True:
|
|||
|
|
try:
|
|||
|
|
check_workers()
|
|||
|
|
now = time.time()
|
|||
|
|
if now - last_status >= 300:
|
|||
|
|
save_status()
|
|||
|
|
last_status = now
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"Loop error: {e}")
|
|||
|
|
time.sleep(CHECK_INTERVAL)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|