fix(stats): 添加文件锁(fcntl.flock)防止并发竞态
- 添加 _acquire_lock() 和 _release_lock() 辅助函数 - _load_stats_data() 和 _atomic_write() 使用文件锁保护 - 使用独立的 .lock 文件作为锁 - 防止并发读写导致数据损坏
This commit is contained in:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user