From e136939a253bfaa59acfc6dbfb5ccdd85158a73e Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Mon, 9 Mar 2026 14:50:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(stats):=20=E6=B7=BB=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E9=94=81=EF=BC=88fcntl.flock=EF=BC=89=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=B9=B6=E5=8F=91=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 _acquire_lock() 和 _release_lock() 辅助函数 - _load_stats_data() 和 _atomic_write() 使用文件锁保护 - 使用独立的 .lock 文件作为锁 - 防止并发读写导致数据损坏 --- agents/haonan/workspace/dev-workflow/stats.py | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/agents/haonan/workspace/dev-workflow/stats.py b/agents/haonan/workspace/dev-workflow/stats.py index fd615b8..2f72768 100644 --- a/agents/haonan/workspace/dev-workflow/stats.py +++ b/agents/haonan/workspace/dev-workflow/stats.py @@ -8,6 +8,7 @@ Cross 模式统计报告模块 import json import os import tempfile +import fcntl from datetime import datetime from pathlib import Path from typing import Dict, List, Any, Optional @@ -26,12 +27,32 @@ def _ensure_stats_dir(): STATS_DIR.mkdir(parents=True, exist_ok=True) +# 锁文件路径 +LOCK_FILE = STATS_DIR / "stats_data.lock" + + +def _acquire_lock(lock_file_path: Path): + """获取文件锁""" + lock_file_path.parent.mkdir(parents=True, exist_ok=True) + lock_fd = open(lock_file_path, 'w') + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX) + return lock_fd + + +def _release_lock(lock_fd): + """释放文件锁""" + fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN) + lock_fd.close() + + def _load_stats_data() -> Dict[str, Any]: - """加载统计数据""" + """加载统计数据(带锁保护)""" if not STATS_FILE.exists(): return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} + lock_fd = None try: + lock_fd = _acquire_lock(LOCK_FILE) with open(STATS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except json.JSONDecodeError as e: @@ -44,25 +65,40 @@ def _load_stats_data() -> Dict[str, Any]: return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} except IOError: return {"events": [], "metadata": {"created_at": datetime.now().isoformat()}} + finally: + if lock_fd: + _release_lock(lock_fd) def _atomic_write(data: Dict[str, Any]): - """原子写入统计数据""" + """原子写入统计数据(带锁保护)""" _ensure_stats_dir() - # 写入临时文件 - fd, temp_path = tempfile.mkstemp(dir=STATS_DIR, suffix='.tmp') + lock_fd = None + temp_path = None try: - with os.fdopen(fd, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) + lock_fd = _acquire_lock(LOCK_FILE) + + # 写入临时文件 + with tempfile.NamedTemporaryFile( + mode='w', + encoding='utf-8', + dir=STATS_DIR, + suffix='.tmp', + delete=False + ) as temp_file: + json.dump(data, temp_file, ensure_ascii=False, indent=2) + temp_path = temp_file.name # 原子重命名 os.replace(temp_path, STATS_FILE) - except Exception: + temp_path = None # 防止在 finally 中再次删除 + finally: + if lock_fd: + _release_lock(lock_fd) # 清理临时文件 - if os.path.exists(temp_path): + if temp_path and os.path.exists(temp_path): os.unlink(temp_path) - raise def _filter_sensitive_data(data: Dict[str, Any]) -> Dict[str, Any]: