ForcePilot/backend/package/yuxi/channels/adapters/arq_queue_adapter.py
Kris 742299cb07 chore: 批量清理报告相关代码并完成多项功能迭代
本次提交包含多维度代码优化与功能增强:
1.  移除报告模块冗余导入与枚举,清理报表相关代码
2.  新增扫码登录支持方法与飞书适配器适配
3.  完善异常日志与健康检查信息
4.  扩展目录、配对管理、能力查询等接口
5.  优化出站管道与事务提交后钩子逻辑
6.  修复飞书消息解析与响应空值问题
7.  重构配置更新与服务账号创建逻辑
8.  统一传输错误分类契约与错误基类扩展
2026-07-06 20:49:35 +08:00

221 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""ARQQueueAdapter实现 QueuePort,复用现有 arq.enqueue_job。
- enqueue 必须复用 arq.enqueue_job
- 不得另起重试 Worker
- ARQ 不可用时 Outbox 必须暂停重试并触发优雅降级
依赖边界:只依赖 yuxi.channels.contract端口 + DTO + 错误)。
ARQ 连接池与 ``LoggerPort`` 通过构造函数注入,由外部 factory 管理生命周期,
不依赖 yuxi.servicesADP-005
"""
from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING, Any
from yuxi.channels.contract.dtos.health import WorkerStatus
from yuxi.channels.contract.dtos.queue import EnqueueCmd, JobId, JobStatus
from yuxi.channels.contract.errors import DependencyError, NotFoundError, ValidationError
from yuxi.channels.contract.errors.base import Error
from yuxi.channels.contract.ports.driven.logger_port import LoggerPort
from yuxi.channels.contract.ports.driven.queue_port import QueuePort
if TYPE_CHECKING:
from arq import ArqRedis
__all__ = ["ARQQueueAdapter"]
class ARQQueueAdapter(QueuePort):
"""实现 QueuePort,复用现有 arq.enqueue_job。
通过构造函数注入的 ``ArqRedis`` 连接池复用 ``arq.enqueue_job`` 入队,
通过 ``arq.jobs.Job`` 查询状态与取消任务。ARQ 不可用时抛出
``DependencyError``,由调用方暂停 Outbox 重试并触发优雅降级。
ARQ 连接池与 ``LoggerPort`` 由外部 factory 创建并注入,适配器仅持有
引用,不负责其生命周期管理(参见 ``close``)。
"""
def __init__(self, arq_pool: ArqRedis, logger: LoggerPort) -> None:
"""初始化适配器,注入 ARQ 连接池与日志端口。
Args:
arq_pool: ARQ 连接池实例(``arq.ArqRedis``,由外部 factory
创建并管理生命周期。适配器仅持有引用用于入队、状态查询
与取消任务。
logger: 日志被驱动端口,用于 ``ping`` 故障时记录告警
(硬约束:所有适配器必须使用注入的 ``LoggerPort`` 而非
全局 logger 工具)。
"""
self._arq_pool = arq_pool
self._logger = logger
async def enqueue(self, cmd: EnqueueCmd) -> JobId:
"""入队异步任务,复用 arq.enqueue_job。
校验 task_name 与 payload 非空后,通过 ``arq.enqueue_job`` 入队。
幂等键重复入队时返回首次入队的 JobIdARQ 不可用时抛出
``DependencyError``。
Args:
cmd: 入队命令,携带任务名称、负载与幂等键。
Returns:
任务 ID。
Raises:
ValidationError: task_name 或 payload 为空。
DependencyError: ARQ 不可用或 enqueue_job 返回 None 且无幂等键。
"""
try:
if not cmd.task_name:
raise ValidationError("task_name", "must not be empty")
if not cmd.payload:
raise ValidationError("payload", "must not be empty")
queue = self._arq_pool
# 消费 scheduled_at 字段,非空时通过 _defer_until 交给 ARQ 延迟投递
enqueue_kwargs: dict[str, Any] = {"_job_id": cmd.idempotency_key}
if cmd.scheduled_at is not None:
enqueue_kwargs["_defer_until"] = cmd.scheduled_at
job = await queue.enqueue_job(cmd.task_name, **cmd.payload, **enqueue_kwargs)
if job is None:
if cmd.idempotency_key:
return JobId(cmd.idempotency_key)
raise DependencyError("arq", Error("enqueue_job returned None"))
return JobId(job.job_id)
except (ValidationError, DependencyError):
raise
except Exception as exc:
raise DependencyError("arq", exc) from exc
async def getJobStatus(self, job_id: JobId) -> JobStatus:
"""查询任务状态。
通过 ``arq.jobs.Job.status()`` 获取 ARQ 状态并映射为契约层
``JobStatus`` 枚举。任务不存在时抛出 ``NotFoundError``ARQ 不可用
时抛出 ``DependencyError``。
状态映射:
- arq ``deferred`` / ``queued`` → ``JobStatus.QUEUED``
- arq ``in_progress`` → ``JobStatus.RUNNING``
- arq ``complete`` → 根据 ``result_info.success`` 区分
``COMPLETED`` / ``FAILED`` / ``CANCELLED``
Args:
job_id: 任务 ID。
Returns:
任务状态枚举值。
Raises:
NotFoundError: 任务不存在。
DependencyError: ARQ 不可用。
"""
try:
from arq.jobs import Job as ArqJob
from arq.jobs import JobStatus as ArqJobStatus
queue = self._arq_pool
job = ArqJob(
job_id.value,
redis=queue,
_queue_name=queue.default_queue_name,
_deserializer=queue.job_deserializer,
)
status = await job.status()
if status == ArqJobStatus.not_found:
raise NotFoundError("job", job_id.value)
if status == ArqJobStatus.complete:
result_info = await job.result_info()
if result_info is not None and not result_info.success:
if isinstance(result_info.result, asyncio.CancelledError):
return JobStatus.CANCELLED
return JobStatus.FAILED
return JobStatus.COMPLETED
if status == ArqJobStatus.in_progress:
return JobStatus.RUNNING
return JobStatus.QUEUED
except NotFoundError:
raise
except Exception as exc:
raise DependencyError("arq", Error(str(exc))) from exc
async def cancelJob(self, job_id: JobId) -> bool:
"""取消任务,用于 Outbox 重试取消场景。
通过 ``arq.jobs.Job.abort()`` 取消任务。任务已取消返回 ``True``
任务已完成或不存在返回 ``False``ARQ 不可用时抛出
``DependencyError``。
Args:
job_id: 任务 ID。
Returns:
任务已取消返回 ``True``,已完成或不存在返回 ``False``。
Raises:
DependencyError: ARQ 不可用。
"""
try:
from arq.jobs import Job as ArqJob
queue = self._arq_pool
job = ArqJob(
job_id.value,
redis=queue,
_queue_name=queue.default_queue_name,
_deserializer=queue.job_deserializer,
)
return await job.abort()
except Exception as exc:
raise DependencyError("arq", Error(str(exc))) from exc
async def getWorkerStatus(self) -> WorkerStatus:
"""获取 ARQ Worker 状态。
返回 ``WorkerStatus``,包含队列名称、可用性与队列深度(积压任务数)。
``queue_depth`` 通过 ``zcard`` 查询 ARQ 队列 ZSET
``arq:queue:<queue_name>``)的成员数,供 ``DiagnosticsExporter``
填充诊断包、``DashboardHandler`` 聚合实时面板。``worker_utilization``
返回 ``None``——ARQ 客户端不暴露 worker 级别的利用率统计,该字段为
未采集占位,由调用方按需消费。
Raises:
DependencyError: ARQ 不可用。
"""
try:
queue = self._arq_pool
queue_key = f"arq:queue:{queue.default_queue_name}"
depth = await queue.zcard(queue_key)
return WorkerStatus(
queue_name=queue.default_queue_name,
available=True,
queue_depth=depth,
worker_utilization=None,
)
except Exception as exc:
raise DependencyError("arq", Error(str(exc))) from exc
async def ping(self) -> bool:
"""主动探测 ARQ 连接可用性,故障时返回 False降级不阻断
执行 ARQ 连接池 ``ping`` 验证连接可用性。故障时返回 False 并通过
注入的 ``LoggerPort`` 记录 warning 日志,不抛异常,供 ``HostBootstrap``
启动期连通性检查使用。
Returns:
True 表示连接可用False 表示故障。
"""
try:
await self._arq_pool.ping()
return True
except Exception as exc:
await self._logger.warn(
"arq queue ping failed",
error_type=type(exc).__name__,
error=str(exc),
)
return False