fix(锁与查询组): #12 熔断器状态转换加锁;#10 问答人名匹配改 JSON_CONTAINS 精确匹配(防 LIKE 子串/特殊字符误匹配);#4 OracleDB 复合写加锁;#6 每消费者独立 VideoProcessor(消除 adapter.timeout 共享竞争);#15 done 视频文件被覆盖(mtime>processed_at)自动重置重分析

This commit is contained in:
ericwyuan
2026-08-21 14:24:12 +08:00
parent 034dca92b2
commit 4e85a98944
4 changed files with 68 additions and 34 deletions

View File

@@ -32,7 +32,6 @@ class VideoQueue:
def __init__(self, db: oracle_db.OracleDB):
self.config = load_config()
self.db = db
self.processor = VideoProcessor(db)
self.local_dir = self.config.get('gdrive_sync', {}).get(
'local_dir', '/opt/fam-edge/gdrive_videos')
self.watch_interval = self.config.get('gdrive_sync', {}).get(
@@ -105,6 +104,28 @@ class VideoQueue:
self._enqueue(vid)
elif row['status'] in ('pending', 'failed') and self._retry_allowed(row):
self._enqueue(row['id'])
elif row['status'] == 'done' and self._file_changed(row, path):
# 文件被覆盖rclone 重新同步/更新):重置 pending 重新分析
logger.info(f"文件内容变更,重置重新分析: {fn} (id={row['id']})")
self.db._conn.execute(
"UPDATE videos SET status='pending', retry_count=0, summary_json=NULL, "
"events_json=NULL, people_json=NULL, compute_provider=NULL, "
"processed_at=NULL, file_valid=1 WHERE id=?",
(row['id'],))
self.db._conn.commit()
self._enqueue(row['id'])
def _file_changed(self, row, path: str) -> bool:
"""判断视频文件在处理后被覆盖mtime 晚于 processed_at"""
try:
proc = row['processed_at'] or ''
if not proc:
return False
from datetime import datetime
pt = datetime.strptime(proc[:19], '%Y-%m-%d %H:%M:%S').timestamp()
return os.path.getmtime(path) > pt + 5 # 5s 容差
except Exception:
return False
def _enqueue_existing(self):
"""启动时把 DB 中未处理完的视频补入队(进程重启恢复)"""
@@ -161,13 +182,16 @@ class VideoQueue:
# 消费者
# ------------------------------------------------------------------
def _consume_loop(self):
# 每消费者独立 VideoProcessor各自 build_adapters
# 避免多消费者共享 adapter 实例导致 timeout 等实例属性改写竞争
processor = VideoProcessor(self.db)
while not self._consumer_stop.is_set():
try:
video_id = self._queue.get(timeout=1)
except queue.Empty:
continue
try:
self._consume_one(video_id)
self._consume_one(video_id, processor)
except Exception as e:
logger.error(f"消费 video_id={video_id} 异常: {e}", exc_info=True)
finally:
@@ -175,7 +199,7 @@ class VideoQueue:
self._queued.discard(video_id)
self._queue.task_done()
def _consume_one(self, video_id: int):
def _consume_one(self, video_id: int, processor: VideoProcessor):
row = self.db.get_video_by_id(video_id)
if row is None:
logger.warning(f"消费到不存在的 video_id={video_id},跳过")
@@ -185,7 +209,7 @@ class VideoQueue:
if not self._retry_allowed(row):
logger.warning(f"[video_id={video_id}] 已达重试上限({row['retry_count']}),放弃")
return
ok = self.processor.process_video(
ok = processor.process_video(
video_id, row['filename'], row['local_path'],
timeout_multiplier=self.timeout_multiplier)
if ok: