#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 缓存预热模块 预热常用镜像缓存提高命中率 """ import os import sys import json import time import threading import logging import requests from datetime import datetime from typing import Dict, List, Optional, Callable from dataclasses import dataclass, field from enum import Enum from concurrent.futures import ThreadPoolExecutor, as_completed logger = logging.getLogger(__name__) class PrewarmPriority(Enum): """预热优先级""" CRITICAL = "critical" # 关键,必须预热 HIGH = "high" # 高优先级 MEDIUM = "medium" # 中优先级 LOW = "low" # 低优先级 class ItemStatus(Enum): """项目状态""" PENDING = "pending" IN_PROGRESS = "in_progress" SUCCESS = "success" FAILED = "failed" SKIPPED = "skipped" @dataclass class PrewarmItem: """预热项目""" id: str mirror_type: str item_name: str url: str priority: str status: str = field(default=ItemStatus.PENDING.value) attempts: int = 0 max_attempts: int = 3 response_time_ms: float = 0 error_message: str = None prewarmed_at: float = None size_bytes: int = 0 def to_dict(self) -> Dict: return { 'id': self.id, 'mirror_type': self.mirror_type, 'item_name': self.item_name, 'url': self.url, 'priority': self.priority, 'status': self.status, 'attempts': self.attempts, 'response_time_ms': self.response_time_ms, 'error_message': self.error_message, 'prewarmed_at': self.prewarmed_at, 'size_bytes': self.size_bytes } @dataclass class PrewarmTarget: """预热目标配置""" mirror_type: str priority: str limit: int items: List[str] = field(default_factory=list) # 指定的预热项目列表 tags: List[str] = field(default_factory=list) # 按标签筛选 class CachePrewarmer: """缓存预热管理器""" def __init__(self, config: Dict = None): """ 初始化缓存预热管理器 Args: config: 预热配置 """ self.config = config or {} self.enabled = self.config.get('enabled', False) # 预热目标 self.targets = self._parse_targets(self.config.get('targets', [])) # 调度配置 self.schedule = self.config.get('schedule', '0 3 * * *') # 默认每天凌晨3点 self.batch_size = self.config.get('batch_size', 10) # 状态管理 self._items: Dict[str, PrewarmItem] = {} self._items_lock = threading.Lock() self._is_running = False self._run_lock = threading.Lock() # 历史记录 self._history: List[Dict] = [] self._history_lock = threading.Lock() # 回调函数 self._on_start: Optional[Callable] = None self._on_complete: Optional[Callable] = None self._on_item_complete: Optional[Callable] = None self._on_error: Optional[Callable] = None # HTTP 会话 self._session = requests.Session() self._session.headers.update({ 'User-Agent': 'HYC-Mirror-Prewarmer/1.0' }) # 超时设置 self._request_timeout = self.config.get('request_timeout', 30) # 常用镜像包列表 self._popular_items = self._load_popular_items() def _parse_targets(self, targets_config: List[Dict]) -> List[PrewarmTarget]: """解析预热目标配置""" targets = [] for t in targets_config: targets.append(PrewarmTarget( mirror_type=t.get('mirror_type', 'docker'), priority=t.get('priority', 'medium'), limit=t.get('limit', 100), items=t.get('items', []), tags=t.get('tags', []) )) return targets def _load_popular_items(self) -> Dict[str, List[str]]: """加载常用镜像包列表""" return { 'docker': [ 'library/alpine:latest', 'library/ubuntu:latest', 'library/debian:latest', 'library/centos:latest', 'library/nginx:latest', 'library/python:3.9', 'library/python:3.10', 'library/node:18', 'library/node:20', 'library/go:1.20', 'library/redis:alpine', 'library/mysql:8', 'library/postgres:15', ], 'pip': [ 'requests', 'numpy', 'pandas', 'flask', 'django', 'scipy', 'scikit-learn', 'torch', 'tensorflow', 'celery', 'pytest', 'black', ], 'npm': [ 'react', 'vue', 'angular', 'lodash', 'express', 'axios', 'typescript', 'webpack', 'vite', 'eslint', ], 'apt': [ 'ubuntu-desktop', 'ubuntu-standard', 'nginx', 'python3-pip', 'nodejs', 'docker.io', ], 'yum': [ 'epel-release', 'nginx', 'docker-ce', 'python3-pip', 'nodejs', ], 'go': [ 'golang.org/x/tools', 'github.com/gin-gonic/gin', 'github.com/beego/beego', 'github.com/gorilla/mux', ], } def _get_base_url(self, mirror_type: str) -> str: """获取镜像源基础 URL""" mirrors = self.config.get('mirrors', {}) if isinstance(mirrors, dict): mirror_config = mirrors.get(mirror_type, {}) if isinstance(mirror_config, dict): sources = mirror_config.get('sources', []) if sources: source_config = mirror_config.get('sources_config', {}).get(sources[0], {}) return source_config.get('url', '') return '' def _generate_url(self, mirror_type: str, item_name: str) -> str: """生成预热 URL""" base_url = self._get_base_url(mirror_type) if not base_url: return '' if mirror_type == 'docker': return f"{base_url}/v2/{item_name}/manifests/latest" elif mirror_type == 'pip': return f"{base_url}/simple/{item_name}/" elif mirror_type == 'npm': return f"{base_url}/{item_name}" elif mirror_type == 'apt': return f"{base_url}/dists/{item_name}/InRelease" elif mirror_type == 'yum': return f"{base_url}/repodata/repomd.xml" elif mirror_type == 'go': return f"{base_url}/{item_name}?go-get=1" else: return f"{base_url}/{item_name}" def _create_item( self, mirror_type: str, item_name: str, priority: str = 'medium' ) -> PrewarmItem: """创建预热项目""" url = self._generate_url(mirror_type, item_name) return PrewarmItem( id=f"{mirror_type}_{item_name}_{int(time.time())}", mirror_type=mirror_type, item_name=item_name, url=url, priority=priority ) def add_item(self, item: PrewarmItem): """添加预热项目""" with self._items_lock: self._items[item.id] = item def add_items_batch(self, mirror_type: str, items: List[str], priority: str = 'medium'): """批量添加预热项目""" for item_name in items: item = self._create_item(mirror_type, item_name, priority) self.add_item(item) def _prewarm_item(self, item: PrewarmItem) -> PrewarmItem: """ 预热单个项目 Args: item: 预热项目 Returns: 更新后的项目 """ item.attempts += 1 item.status = ItemStatus.IN_PROGRESS.value try: start_time = time.time() response = self._session.get( item.url, timeout=self._request_timeout, allow_redirects=True ) response.raise_for_status() item.response_time_ms = round((time.time() - start_time) * 1000, 2) item.size_bytes = len(response.content) item.status = ItemStatus.SUCCESS.value item.prewarmed_at = time.time() logger.debug(f"Prewarmed {item.mirror_type}/{item.item_name}: {item.response_time_ms}ms") except requests.exceptions.Timeout: item.error_message = f"Timeout after {self._request_timeout}s" if item.attempts < item.max_attempts: item.status = ItemStatus.PENDING.value else: item.status = ItemStatus.FAILED.value except requests.exceptions.HTTPError as e: item.error_message = f"HTTP Error: {e.response.status_code}" if item.attempts < item.max_attempts: item.status = ItemStatus.PENDING.value else: item.status = ItemStatus.FAILED.value except Exception as e: item.error_message = str(e) item.status = ItemStatus.FAILED.value return item def run(self, targets: List[PrewarmTarget] = None) -> Dict: """ 执行预热 Args: targets: 预热目标列表,None 表示使用配置的默认目标 Returns: 预热结果 """ with self._run_lock: if self._is_running: return { 'success': False, 'error': 'Prewarm already running' } self._is_running = True start_time = time.time() result = { 'success': True, 'total_items': 0, 'success_count': 0, 'failed_count': 0, 'skipped_count': 0, 'elapsed_seconds': 0, 'errors': [] } try: # 通知开始 if self._on_start: try: self._on_start() except Exception as e: logger.error(f"Prewarm start callback failed: {e}") # 确定要预热的目标 if targets is None: targets = self.targets # 如果没有指定目标,使用流行项目 if not targets: for mirror_type, items in self._popular_items.items(): targets.append(PrewarmTarget( mirror_type=mirror_type, priority='medium', limit=len(items), items=items )) # 添加项目到队列 total_added = 0 for target in targets: if target.items: # 使用指定的预热项目 for item_name in target.items[:target.limit]: item = self._create_item(target.mirror_type, item_name, target.priority) self.add_item(item) total_added += 1 else: # 使用流行项目列表 popular = self._popular_items.get(target.mirror_type, []) for item_name in popular[:target.limit]: item = self._create_item(target.mirror_type, item_name, target.priority) self.add_item(item) total_added += 1 result['total_items'] = total_added # 按优先级排序 priority_order = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 } with self._items_lock: sorted_items = sorted( self._items.values(), key=lambda x: (priority_order.get(x.priority, 99), x.id) ) # 分批执行 completed = 0 with ThreadPoolExecutor(max_workers=self.batch_size) as executor: futures = { executor.submit(self._prewarm_item, item): item for item in sorted_items } for future in as_completed(futures): item = futures[future] try: updated_item = future.result() # 更新项目状态 with self._items_lock: self._items[item.id] = updated_item # 统计 if updated_item.status == ItemStatus.SUCCESS.value: result['success_count'] += 1 elif updated_item.status == ItemStatus.FAILED.value: result['failed_count'] += 1 result['errors'].append({ 'item': updated_item.item_name, 'error': updated_item.error_message }) else: result['skipped_count'] += 1 completed += 1 # 回调 if self._on_item_complete: try: self._on_item_complete(updated_item) except Exception as e: logger.error(f"Item complete callback failed: {e}") except Exception as e: logger.error(f"Prewarm execution error: {e}") result['failed_count'] += 1 result['errors'].append({ 'item': item.item_name, 'error': str(e) }) result['elapsed_seconds'] = round(time.time() - start_time, 2) # 记录历史 self._add_to_history(result) # 通知完成 if self._on_complete: try: self._on_complete(result) except Exception as e: logger.error(f"Prewarm complete callback failed: {e}") except Exception as e: logger.error(f"Prewarm failed: {e}") result['success'] = False result['errors'].append({'error': str(e)}) finally: self._is_running = False return result def _add_to_history(self, result: Dict): """添加历史记录""" record = { 'timestamp': datetime.now().isoformat(), 'success': result.get('success', False), 'total_items': result.get('total_items', 0), 'success_count': result.get('success_count', 0), 'failed_count': result.get('failed_count', 0), 'elapsed_seconds': result.get('elapsed_seconds', 0) } with self._history_lock: self._history.append(record) # 只保留最近 50 条记录 self._history = self._history[-50:] def get_status(self) -> Dict: """获取预热状态""" with self._items_lock: items = list(self._items.values()) total = len(items) success = sum(1 for i in items if i.status == ItemStatus.SUCCESS.value) failed = sum(1 for i in items if i.status == ItemStatus.FAILED.value) in_progress = sum(1 for i in items if i.status == ItemStatus.IN_PROGRESS.value) pending = sum(1 for i in items if i.status == ItemStatus.PENDING.value) return { 'enabled': self.enabled, 'is_running': self._is_running, 'total_items': total, 'success_count': success, 'failed_count': failed, 'in_progress_count': in_progress, 'pending_count': pending, 'success_rate': (success / total * 100) if total > 0 else 0, 'targets_count': len(self.targets) } def get_items( self, status: str = None, mirror_type: str = None, limit: int = 50 ) -> List[Dict]: """获取预热项目列表""" with self._items_lock: items = [i.to_dict() for i in self._items.values()] # 过滤 if status: items = [i for i in items if i['status'] == status] if mirror_type: items = [i for i in items if i['mirror_type'] == mirror_type] return items[-limit:] def get_history(self, limit: int = 20) -> List[Dict]: """获取预热历史""" with self._history_lock: return list(self._history[-limit:]) def get_stats(self) -> Dict: """获取统计信息""" status = self.get_status() history = self.get_history(10) avg_duration = 0 if history: durations = [h['elapsed_seconds'] for h in history if 'elapsed_seconds' in h] if durations: avg_duration = sum(durations) / len(durations) return { 'enabled': self.enabled, 'is_running': self._is_running, 'total_prewarmed': status['success_count'], 'total_failed': status['failed_count'], 'success_rate': status['success_rate'], 'avg_duration_seconds': round(avg_duration, 2), 'recent_runs': len(history), 'targets': [ { 'mirror_type': t.mirror_type, 'priority': t.priority, 'limit': t.limit } for t in self.targets ] } def clear_items(self, status: str = None): """清除预热项目""" with self._items_lock: if status: self._items = { k: v for k, v in self._items.items() if v.status != status } else: self._items = {} def set_start_callback(self, callback: Callable): """设置开始回调""" self._on_start = callback def set_complete_callback(self, callback: Callable): """设置完成回调""" self._on_complete = callback def set_item_complete_callback(self, callback: Callable): """设置项目完成回调""" self._on_item_complete = callback def set_error_callback(self, callback: Callable): """设置错误回调""" self._on_error = callback def get_popular_items(self, mirror_type: str) -> List[str]: """获取指定镜像类型的流行项目列表""" return self._popular_items.get(mirror_type, []) def add_popular_items_to_queue( self, mirror_type: str, limit: int = None, priority: str = 'medium' ): """ 添加流行项目到预热队列 Args: mirror_type: 镜像类型 limit: 数量限制 priority: 优先级 """ popular = self._popular_items.get(mirror_type, []) if limit: popular = popular[:limit] self.add_items_batch(mirror_type, popular, priority) class PrewarmScheduler: """预热调度器""" def __init__(self, config: Dict = None): self.config = config or {} self._scheduler = None self._running = False def start(self, prewarmer: CachePrewarmer): """启动调度器""" if self._running: return schedule = self.config.get('schedule', '0 3 * * *') logger.info(f"Starting prewarm scheduler with schedule: {schedule}") # 使用简单的时间间隔检查 self._running = True self._scheduler_thread = threading.Thread( target=self._run_scheduler, args=(prewarmer,), daemon=True ) self._scheduler_thread.start() def _run_scheduler(self, prewarmer: CachePrewarmer): """运行调度器""" import croniter schedule = self.config.get('schedule', '0 3 * * *') try: cron = croniter.croniter(schedule, datetime.now()) next_run = cron.get_next(datetime) except Exception as e: logger.error(f"Invalid cron schedule: {e}") return while self._running: now = datetime.now() if now >= next_run: logger.info("Running scheduled prewarm") try: prewarmer.run() except Exception as e: logger.error(f"Scheduled prewarm failed: {e}") cron = croniter.croniter(schedule, datetime.now()) next_run = cron.get_next(datetime) # 检查间隔 time.sleep(60) def stop(self): """停止调度器""" self._running = False