#!/usr/bin/env python # -*- coding: utf-8 -*- """ 队列管理器 支持任务持久化和断点恢复 """ import asyncio import json import sqlite3 import time import logging import pickle from typing import List, Dict, Optional, Any from pathlib import Path from dataclasses import dataclass, asdict from enum import Enum from apiproxy.douyin.strategies.base import DownloadTask, TaskStatus, TaskType logger = logging.getLogger(__name__) class PersistentQueue: """持久化队列管理器""" def __init__( self, db_path: str = "download_queue.db", max_size: int = 10000, checkpoint_interval: int = 60 ): """ 初始化队列管理器 Args: db_path: 数据库文件路径 max_size: 队列最大容量 checkpoint_interval: 检查点保存间隔(秒) """ self.db_path = Path(db_path) self.max_size = max_size self.checkpoint_interval = checkpoint_interval self.conn: Optional[sqlite3.Connection] = None self.queue = asyncio.Queue(maxsize=max_size) self._checkpoint_task = None self._lock = asyncio.Lock() # 初始化数据库 self._init_database() # 恢复未完成的任务 self._restore_tasks() def _init_database(self): """初始化数据库""" self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False) cursor = self.conn.cursor() # 创建任务表 cursor.execute(''' CREATE TABLE IF NOT EXISTS tasks ( task_id TEXT PRIMARY KEY, url TEXT NOT NULL, task_type TEXT NOT NULL, priority INTEGER DEFAULT 0, status TEXT NOT NULL, retry_count INTEGER DEFAULT 0, max_retries INTEGER DEFAULT 3, metadata TEXT, created_at REAL NOT NULL, updated_at REAL NOT NULL, completed_at REAL, error_message TEXT, result TEXT ) ''') # 创建索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_priority ON tasks(priority DESC)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_created_at ON tasks(created_at)') # 创建进度表 cursor.execute(''' CREATE TABLE IF NOT EXISTS progress ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL NOT NULL, total_tasks INTEGER, pending_tasks INTEGER, active_tasks INTEGER, completed_tasks INTEGER, failed_tasks INTEGER, success_rate REAL, average_duration REAL ) ''') self.conn.commit() logger.info(f"数据库初始化完成: {self.db_path}") def _restore_tasks(self): """从数据库恢复未完成的任务""" cursor = self.conn.cursor() # 将所有PROCESSING状态的任务重置为PENDING cursor.execute(''' UPDATE tasks SET status = ?, updated_at = ? WHERE status = ? ''', (TaskStatus.PENDING.value, time.time(), TaskStatus.PROCESSING.value)) # 获取所有待处理的任务 cursor.execute(''' SELECT task_id, url, task_type, priority, retry_count, max_retries, metadata, created_at FROM tasks WHERE status IN (?, ?) ORDER BY priority DESC, created_at ASC ''', (TaskStatus.PENDING.value, TaskStatus.RETRYING.value)) restored_count = 0 for row in cursor.fetchall(): task = self._row_to_task(row) if task: try: self.queue.put_nowait(task) restored_count += 1 except asyncio.QueueFull: break self.conn.commit() if restored_count > 0: logger.info(f"从数据库恢复了 {restored_count} 个未完成任务") def _row_to_task(self, row: tuple) -> Optional[DownloadTask]: """将数据库行转换为任务对象""" try: task_id, url, task_type, priority, retry_count, max_retries, metadata_str, created_at = row metadata = {} if metadata_str: try: metadata = json.loads(metadata_str) except: pass return DownloadTask( task_id=task_id, url=url, task_type=TaskType(task_type), priority=priority, retry_count=retry_count, max_retries=max_retries, metadata=metadata, created_at=created_at ) except Exception as e: logger.error(f"转换任务失败: {e}") return None async def add_task(self, task: DownloadTask) -> bool: """ 添加任务到队列 Args: task: 下载任务 Returns: 是否成功添加 """ async with self._lock: try: # 保存到数据库 cursor = self.conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO tasks ( task_id, url, task_type, priority, status, retry_count, max_retries, metadata, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( task.task_id, task.url, task.task_type.value, task.priority, task.status.value, task.retry_count, task.max_retries, json.dumps(task.metadata), task.created_at, task.updated_at )) self.conn.commit() # 添加到内存队列 await self.queue.put(task) logger.debug(f"任务 {task.task_id} 已添加到队列") return True except Exception as e: logger.error(f"添加任务失败: {e}") return False async def get_task(self, timeout: float = 1.0) -> Optional[DownloadTask]: """ 从队列获取任务 Args: timeout: 超时时间 Returns: 下载任务 """ try: task = await asyncio.wait_for(self.queue.get(), timeout=timeout) # 更新数据库状态 await self.update_task_status(task.task_id, TaskStatus.PROCESSING) return task except asyncio.TimeoutError: return None async def update_task_status( self, task_id: str, status: TaskStatus, error_message: Optional[str] = None, result: Optional[Dict] = None ): """ 更新任务状态 Args: task_id: 任务ID status: 新状态 error_message: 错误信息 result: 执行结果 """ async with self._lock: cursor = self.conn.cursor() update_fields = { 'status': status.value, 'updated_at': time.time() } if error_message: update_fields['error_message'] = error_message if result: update_fields['result'] = json.dumps(result) if status == TaskStatus.COMPLETED: update_fields['completed_at'] = time.time() # 构建UPDATE语句 set_clause = ', '.join([f'{k} = ?' for k in update_fields.keys()]) values = list(update_fields.values()) + [task_id] cursor.execute( f'UPDATE tasks SET {set_clause} WHERE task_id = ?', values ) self.conn.commit() async def requeue_task(self, task: DownloadTask): """ 将任务重新加入队列 Args: task: 下载任务 """ task.retry_count += 1 task.status = TaskStatus.RETRYING task.updated_at = time.time() await self.add_task(task) logger.info(f"任务 {task.task_id} 重新加入队列 (重试 {task.retry_count}/{task.max_retries})") def get_statistics(self) -> Dict[str, Any]: """获取队列统计信息""" cursor = self.conn.cursor() # 统计各状态任务数 cursor.execute(''' SELECT status, COUNT(*) FROM tasks GROUP BY status ''') status_counts = {} for status, count in cursor.fetchall(): status_counts[status] = count # 计算成功率 total = sum(status_counts.values()) completed = status_counts.get(TaskStatus.COMPLETED.value, 0) success_rate = (completed / total * 100) if total > 0 else 0 # 计算平均耗时 cursor.execute(''' SELECT AVG(completed_at - created_at) FROM tasks WHERE status = ? AND completed_at IS NOT NULL ''', (TaskStatus.COMPLETED.value,)) avg_duration = cursor.fetchone()[0] or 0 stats = { 'total_tasks': total, 'pending_tasks': status_counts.get(TaskStatus.PENDING.value, 0), 'processing_tasks': status_counts.get(TaskStatus.PROCESSING.value, 0), 'completed_tasks': completed, 'failed_tasks': status_counts.get(TaskStatus.FAILED.value, 0), 'retrying_tasks': status_counts.get(TaskStatus.RETRYING.value, 0), 'success_rate': success_rate, 'average_duration': avg_duration, 'queue_size': self.queue.qsize() } return stats async def save_progress(self): """保存进度到数据库""" stats = self.get_statistics() cursor = self.conn.cursor() cursor.execute(''' INSERT INTO progress ( timestamp, total_tasks, pending_tasks, active_tasks, completed_tasks, failed_tasks, success_rate, average_duration ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ''', ( time.time(), stats['total_tasks'], stats['pending_tasks'], stats['processing_tasks'], stats['completed_tasks'], stats['failed_tasks'], stats['success_rate'], stats['average_duration'] )) self.conn.commit() logger.debug("进度已保存") async def start_checkpoint(self): """启动检查点保存任务""" if not self._checkpoint_task: self._checkpoint_task = asyncio.create_task(self._checkpoint_loop()) logger.info("检查点保存任务已启动") async def stop_checkpoint(self): """停止检查点保存任务""" if self._checkpoint_task: self._checkpoint_task.cancel() try: await self._checkpoint_task except asyncio.CancelledError: pass self._checkpoint_task = None logger.info("检查点保存任务已停止") async def _checkpoint_loop(self): """检查点保存循环""" while True: try: await asyncio.sleep(self.checkpoint_interval) await self.save_progress() except asyncio.CancelledError: break except Exception as e: logger.error(f"保存检查点失败: {e}") def get_recent_progress(self, hours: int = 24) -> List[Dict]: """ 获取最近的进度记录 Args: hours: 获取最近多少小时的记录 Returns: 进度记录列表 """ cursor = self.conn.cursor() since = time.time() - hours * 3600 cursor.execute(''' SELECT timestamp, total_tasks, completed_tasks, failed_tasks, success_rate FROM progress WHERE timestamp > ? ORDER BY timestamp DESC LIMIT 100 ''', (since,)) records = [] for row in cursor.fetchall(): records.append({ 'timestamp': row[0], 'total_tasks': row[1], 'completed_tasks': row[2], 'failed_tasks': row[3], 'success_rate': row[4] }) return records def cleanup_old_tasks(self, days: int = 7): """ 清理旧任务记录 Args: days: 保留最近多少天的记录 """ cursor = self.conn.cursor() cutoff = time.time() - days * 86400 cursor.execute(''' DELETE FROM tasks WHERE status IN (?, ?) AND updated_at < ? ''', (TaskStatus.COMPLETED.value, TaskStatus.FAILED.value, cutoff)) deleted = cursor.rowcount self.conn.commit() if deleted > 0: logger.info(f"清理了 {deleted} 条旧任务记录") def export_tasks(self, status: Optional[TaskStatus] = None) -> List[Dict]: """ 导出任务列表 Args: status: 筛选状态 Returns: 任务列表 """ cursor = self.conn.cursor() if status: cursor.execute(''' SELECT * FROM tasks WHERE status = ? ORDER BY created_at DESC ''', (status.value,)) else: cursor.execute(''' SELECT * FROM tasks ORDER BY created_at DESC ''') tasks = [] columns = [desc[0] for desc in cursor.description] for row in cursor.fetchall(): task_dict = dict(zip(columns, row)) # 解析JSON字段 if task_dict.get('metadata'): try: task_dict['metadata'] = json.loads(task_dict['metadata']) except: pass if task_dict.get('result'): try: task_dict['result'] = json.loads(task_dict['result']) except: pass tasks.append(task_dict) return tasks def close(self): """关闭数据库连接""" if self.conn: self.conn.close() self.conn = None logger.info("数据库连接已关闭") async def __aenter__(self): """异步上下文管理器入口""" await self.start_checkpoint() return self async def __aexit__(self, exc_type, exc_val, exc_tb): """异步上下文管理器出口""" await self.stop_checkpoint() await self.save_progress() self.close()