Files

147 lines
4.5 KiB
Python
Raw Permalink Normal View History

#!/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()