""" Webhook 处理服务 实现智能分发、任务排队和防抖策略 """ import asyncio from typing import Optional, Dict, Any from datetime import datetime import structlog from celery import Celery from app.config import get_settings from app.models.gitea import GiteaWebhook, WebhookResponse from app.services.dedup_service import DeduplicationService from app.services.jenkins_service import JenkinsService from app.services.database_service import get_database_service from app.tasks.jenkins_tasks import trigger_jenkins_job logger = structlog.get_logger() class WebhookService: """Webhook 处理服务""" def __init__( self, dedup_service: DeduplicationService, jenkins_service: JenkinsService, celery_app: Celery ): self.dedup_service = dedup_service self.jenkins_service = jenkins_service self.celery_app = celery_app self.settings = get_settings() self.db_service = get_database_service() async def process_webhook(self, webhook: GiteaWebhook) -> WebhookResponse: """ 处理 Webhook 事件 Args: webhook: Gitea Webhook 数据 Returns: WebhookResponse: 处理结果 """ try: # 1. 验证事件类型 if not webhook.is_push_event(): return WebhookResponse( success=True, message="Non-push event ignored", event_id=webhook.get_event_id() ) # 2. 提取关键信息 branch = webhook.get_branch_name() commit_hash = webhook.get_commit_hash() repository = webhook.repository.full_name logger.info("Processing webhook", repository=repository, branch=branch, commit_hash=commit_hash) # 3. 防抖检查 dedup_key = self.dedup_service.generate_dedup_key(commit_hash, branch) if await self.dedup_service.is_duplicate(dedup_key): return WebhookResponse( success=True, message="Duplicate event ignored", event_id=webhook.get_event_id() ) # 4. 获取项目映射和任务名 job_name = await self._determine_job_name(repository, branch) if not job_name: return WebhookResponse( success=True, message=f"No Jenkins job mapping for repository: {repository}, branch: {branch}", event_id=webhook.get_event_id() ) # 5. 准备任务参数 job_params = self._prepare_job_parameters(webhook, job_name) # 6. 提交任务到队列 task_result = await self._submit_job_to_queue( webhook, job_name, job_params ) if task_result: return WebhookResponse( success=True, message="Job queued successfully", event_id=webhook.get_event_id(), job_name=job_name ) else: return WebhookResponse( success=False, message="Failed to queue job", event_id=webhook.get_event_id() ) except Exception as e: logger.error("Error processing webhook", repository=webhook.repository.full_name, error=str(e)) return WebhookResponse( success=False, message=f"Internal server error: {str(e)}", event_id=webhook.get_event_id() ) async def _determine_job_name(self, repository: str, branch: str) -> Optional[str]: """根据仓库和分支确定任务名""" # 首先尝试从数据库获取项目映射 job_name = await self.db_service.determine_job_name(repository, branch) if job_name: return job_name # 如果数据库中没有映射,使用配置文件中的环境分发 environment = self.settings.get_environment_for_branch(branch) if environment: return environment.jenkins_job return None def _prepare_job_parameters(self, webhook: GiteaWebhook, job_name: str) -> Dict[str, str]: """准备 Jenkins 任务参数""" author_info = webhook.get_author_info() return { "BRANCH_NAME": webhook.get_branch_name(), "COMMIT_SHA": webhook.get_commit_hash(), "REPOSITORY_URL": webhook.repository.clone_url, "REPOSITORY_NAME": webhook.repository.full_name, "PUSHER_NAME": author_info["name"], "PUSHER_EMAIL": author_info["email"], "PUSHER_USERNAME": author_info["username"], "COMMIT_MESSAGE": webhook.get_commit_message(), "JOB_NAME": job_name, "WEBHOOK_EVENT_ID": webhook.get_event_id(), "TRIGGER_TIME": datetime.utcnow().isoformat() } async def _submit_job_to_queue( self, webhook: GiteaWebhook, job_name: str, job_params: Dict[str, str] ) -> bool: """提交任务到 Celery 队列""" try: # 创建任务 task_kwargs = { "job_name": job_name, "jenkins_url": self.settings.jenkins.url, "parameters": job_params, "event_id": webhook.get_event_id(), "repository": webhook.repository.full_name, "branch": webhook.get_branch_name(), "commit_hash": webhook.get_commit_hash(), "priority": 1 # 默认优先级 } # 提交到 Celery 队列 task = self.celery_app.send_task( "app.tasks.jenkins_tasks.trigger_jenkins_job", kwargs=task_kwargs, priority=environment.priority ) logger.info("Job submitted to queue", task_id=task.id, job_name=job_name, repository=webhook.repository.full_name, branch=webhook.get_branch_name()) return True except Exception as e: logger.error("Failed to submit job to queue", job_name=job_name, error=str(e)) return False async def get_webhook_stats(self) -> Dict[str, Any]: """获取 Webhook 处理统计""" try: # 获取队列统计 queue_stats = await self._get_queue_stats() # 获取防抖统计 dedup_stats = await self.dedup_service.get_stats() # 获取环境配置 environments = {} for name, config in self.settings.environments.items(): environments[name] = { "branches": config.branches, "jenkins_job": config.jenkins_job, "jenkins_url": config.jenkins_url, "priority": config.priority } return { "queue": queue_stats, "deduplication": dedup_stats, "environments": environments, "config": { "max_concurrent": self.settings.queue.max_concurrent, "max_retries": self.settings.queue.max_retries, "retry_delay": self.settings.queue.retry_delay }, "timestamp": datetime.utcnow().isoformat() } except Exception as e: logger.error("Error getting webhook stats", error=str(e)) return {"error": str(e)} async def _get_queue_stats(self) -> Dict[str, Any]: """获取队列统计信息""" try: # 获取 Celery 队列统计 inspect = self.celery_app.control.inspect() # 活跃任务 active = inspect.active() active_count = sum(len(tasks) for tasks in active.values()) if active else 0 # 等待任务 reserved = inspect.reserved() reserved_count = sum(len(tasks) for tasks in reserved.values()) if reserved else 0 # 注册的 worker registered = inspect.registered() worker_count = len(registered) if registered else 0 return { "active_tasks": active_count, "queued_tasks": reserved_count, "worker_count": worker_count, "queue_length": active_count + reserved_count } except Exception as e: logger.error("Error getting queue stats", error=str(e)) return {"error": str(e)} async def clear_queue(self) -> Dict[str, Any]: """清空队列""" try: # 撤销所有活跃任务 inspect = self.celery_app.control.inspect() active = inspect.active() revoked_count = 0 if active: for worker, tasks in active.items(): for task in tasks: self.celery_app.control.revoke(task["id"], terminate=True) revoked_count += 1 logger.info("Queue cleared", revoked_count=revoked_count) return { "success": True, "revoked_count": revoked_count, "message": f"Cleared {revoked_count} tasks from queue" } except Exception as e: logger.error("Error clearing queue", error=str(e)) return { "success": False, "error": str(e) }