mirror of
https://mirror.skon.top/github.com/cft0808/edict
synced 2026-04-23 02:12:35 +08:00
- EventBus: Redis Streams pub/sub for decoupled service communication - State machine: strict lifecycle transitions with audit logging - Dispatch worker: parallel execution, retry with backoff, resource locking - Orchestrator: DAG-based task decomposition and dependency resolution - Outbox relay: transactional outbox pattern for reliable event delivery - Auth: dashboard authentication module - Agent groups: sansheng/liubu agent configuration - CI/CD: Docker publish workflow, systemd service, start script - Frontend: dashboard build assets - Tests: state machine consistency tests
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Refresh Watcher — 常驻进程,监控信号文件,debounce 后执行 refresh_live_data.py。
|
||
|
||
替代 kanban_update.py 中每次操作都 fork 子进程的方式。
|
||
多 Agent 并发时,200 次 touch → 合并为 1 次 refresh。
|
||
|
||
运行方式:
|
||
python3 scripts/refresh_watcher.py
|
||
|
||
部署方式:
|
||
- systemd: 参见 edict.service
|
||
- docker-compose: 参见 edict/docker-compose.yml
|
||
- 手动前台: python3 scripts/refresh_watcher.py
|
||
"""
|
||
import logging
|
||
import os
|
||
import pathlib
|
||
import signal
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
_BASE = pathlib.Path(os.environ.get('EDICT_HOME', '')).resolve() if os.environ.get('EDICT_HOME') else pathlib.Path(__file__).resolve().parent.parent
|
||
SIGNAL_FILE = _BASE / 'data' / '.refresh_pending'
|
||
PID_FILE = _BASE / 'data' / '.refresh_watcher_pid'
|
||
REFRESH_SCRIPT = _BASE / 'scripts' / 'refresh_live_data.py'
|
||
DEBOUNCE_SEC = 2 # 信号文件稳定 2 秒后才执行
|
||
POLL_INTERVAL = 0.5 # 检查间隔
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s [refresh_watcher] %(message)s',
|
||
datefmt='%H:%M:%S',
|
||
)
|
||
log = logging.getLogger('refresh_watcher')
|
||
|
||
_running = True
|
||
|
||
|
||
def _shutdown(signum, frame):
|
||
global _running
|
||
_running = False
|
||
log.info(f'收到信号 {signum},准备退出')
|
||
|
||
|
||
def main():
|
||
# 写 PID 文件,让 kanban_update.py 知道 watcher 在运行
|
||
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||
PID_FILE.write_text(str(os.getpid()))
|
||
log.info(f'Refresh watcher started (pid={os.getpid()}, debounce={DEBOUNCE_SEC}s)')
|
||
|
||
signal.signal(signal.SIGTERM, _shutdown)
|
||
signal.signal(signal.SIGINT, _shutdown)
|
||
|
||
last_seen_mtime = 0.0
|
||
refresh_count = 0
|
||
|
||
try:
|
||
while _running:
|
||
try:
|
||
if SIGNAL_FILE.exists():
|
||
mtime = SIGNAL_FILE.stat().st_mtime
|
||
now = time.time()
|
||
# 信号文件存在且已稳定 DEBOUNCE_SEC 秒
|
||
if mtime > last_seen_mtime and (now - mtime) >= DEBOUNCE_SEC:
|
||
last_seen_mtime = mtime
|
||
# 删除信号文件(在执行前删,避免执行期间的新 touch 被吞)
|
||
try:
|
||
SIGNAL_FILE.unlink()
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
refresh_count += 1
|
||
log.info(f'🔄 执行 refresh #{refresh_count}')
|
||
try:
|
||
subprocess.run(
|
||
[sys.executable, str(REFRESH_SCRIPT)],
|
||
capture_output=True,
|
||
timeout=30,
|
||
)
|
||
except subprocess.TimeoutExpired:
|
||
log.warning('refresh_live_data.py 超时 (30s)')
|
||
except Exception as e:
|
||
log.error(f'refresh 执行失败: {e}')
|
||
except Exception as e:
|
||
log.error(f'Watcher loop error: {e}')
|
||
|
||
time.sleep(POLL_INTERVAL)
|
||
finally:
|
||
# 清理 PID 文件
|
||
try:
|
||
PID_FILE.unlink()
|
||
except Exception:
|
||
pass
|
||
log.info(f'Refresh watcher stopped (total refreshes: {refresh_count})')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|