本次提交包含多维度代码优化与功能增强: 1. 移除报告模块冗余导入与枚举,清理报表相关代码 2. 新增扫码登录支持方法与飞书适配器适配 3. 完善异常日志与健康检查信息 4. 扩展目录、配对管理、能力查询等接口 5. 优化出站管道与事务提交后钩子逻辑 6. 修复飞书消息解析与响应空值问题 7. 重构配置更新与服务账号创建逻辑 8. 统一传输错误分类契约与错误基类扩展
221 lines
8.5 KiB
Python
221 lines
8.5 KiB
Python
"""ARQQueueAdapter:实现 QueuePort,复用现有 arq.enqueue_job。
|
||
|
||
- enqueue 必须复用 arq.enqueue_job
|
||
- 不得另起重试 Worker
|
||
- ARQ 不可用时 Outbox 必须暂停重试并触发优雅降级
|
||
|
||
依赖边界:只依赖 yuxi.channels.contract(端口 + DTO + 错误)。
|
||
ARQ 连接池与 ``LoggerPort`` 通过构造函数注入,由外部 factory 管理生命周期,
|
||
不依赖 yuxi.services(ADP-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`` 入队。
|
||
幂等键重复入队时返回首次入队的 JobId;ARQ 不可用时抛出
|
||
``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
|