微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
50
apps/etl/connectors/feiqiu/utils/cancellation.py
Normal file
50
apps/etl/connectors/feiqiu/utils/cancellation.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""线程安全的取消令牌,用于 ETL 管道的优雅中断。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
class CancellationToken:
|
||||
"""线程安全的取消令牌,封装 threading.Event。
|
||||
|
||||
支持手动取消和超时自动取消两种模式。
|
||||
取消操作不可逆——一旦 cancel() 被调用,is_cancelled 永远为 True。
|
||||
"""
|
||||
|
||||
def __init__(self, timeout: float | None = None):
|
||||
"""初始化取消令牌。
|
||||
|
||||
Args:
|
||||
timeout: 超时秒数。传入正数时启动守护定时器,
|
||||
到期后自动调用 cancel()。None 或 <=0 不启动定时器。
|
||||
"""
|
||||
self._event = threading.Event()
|
||||
self._timer: threading.Timer | None = None
|
||||
if timeout is not None and timeout > 0:
|
||||
self._timer = threading.Timer(timeout, self.cancel)
|
||||
self._timer.daemon = True
|
||||
self._timer.start()
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""发出取消信号(幂等,可多次调用)。"""
|
||||
self._event.set()
|
||||
|
||||
@property
|
||||
def is_cancelled(self) -> bool:
|
||||
"""当前是否已取消。"""
|
||||
return self._event.is_set()
|
||||
|
||||
@property
|
||||
def event(self) -> threading.Event:
|
||||
"""底层 Event 对象,供 RateLimiter 等组件轮询使用。"""
|
||||
return self._event
|
||||
|
||||
def dispose(self) -> None:
|
||||
"""清理超时定时器,防止资源泄漏。
|
||||
|
||||
管道结束后应主动调用;即使不调用,守护线程也会随主进程退出。
|
||||
"""
|
||||
if self._timer is not None:
|
||||
self._timer.cancel()
|
||||
self._timer = None
|
||||
@@ -12,7 +12,6 @@ ENDPOINT_FILENAME_MAP: dict[str, str] = {
|
||||
"/memberprofile/getmembercardbalancechange": "member_balance_changes.json",
|
||||
"/memberprofile/gettenantmembercardlist": "member_stored_value_cards.json",
|
||||
"/site/getrechargesettlelist": "recharge_settlements.json",
|
||||
"/assistantperformance/getabolitionassistant": "assistant_cancellation_records.json",
|
||||
"/assistantperformance/getorderassistantdetails": "assistant_service_records.json",
|
||||
"/personnelmanagement/searchassistantinfo": "assistant_accounts_master.json",
|
||||
"/table/getsitetables": "site_tables_master.json",
|
||||
|
||||
101
apps/etl/connectors/feiqiu/utils/task_log_buffer.py
Normal file
101
apps/etl/connectors/feiqiu/utils/task_log_buffer.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""任务级日志缓冲区,收集单个任务的所有日志,任务完成后一次性输出。
|
||||
|
||||
解决多任务并行执行时日志行交叉混乱的问题:每个任务维护独立的缓冲区,
|
||||
任务完成后将完整日志按时间顺序一次性输出到父 logger,添加 [task_code] 前缀。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class LogEntry:
|
||||
"""日志条目。"""
|
||||
|
||||
timestamp: datetime
|
||||
level: int
|
||||
task_code: str
|
||||
message: str
|
||||
|
||||
|
||||
class TaskLogBuffer:
|
||||
"""任务级日志缓冲区,收集单个任务的所有日志,任务完成后一次性输出。
|
||||
|
||||
所有写入操作线程安全(内部使用 threading.Lock)。
|
||||
"""
|
||||
|
||||
def __init__(self, task_code: str, parent_logger: logging.Logger) -> None:
|
||||
"""初始化日志缓冲区。
|
||||
|
||||
Args:
|
||||
task_code: 任务代码,用于日志前缀标识。
|
||||
parent_logger: 父 logger,flush() 时日志输出的目标。
|
||||
"""
|
||||
self.task_code = task_code
|
||||
self._parent = parent_logger
|
||||
self._buffer: list[LogEntry] = []
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def log(self, level: int, message: str, *args: object) -> None:
|
||||
"""线程安全地缓冲一条日志。
|
||||
|
||||
Args:
|
||||
level: 日志级别(如 logging.INFO)。
|
||||
message: 日志消息,支持 % 格式化。
|
||||
*args: 格式化参数。
|
||||
"""
|
||||
formatted = message % args if args else message
|
||||
entry = LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
level=level,
|
||||
task_code=self.task_code,
|
||||
message=formatted,
|
||||
)
|
||||
with self._lock:
|
||||
self._buffer.append(entry)
|
||||
|
||||
# ---- 便捷方法 ----
|
||||
|
||||
def debug(self, message: str, *args: object) -> None:
|
||||
self.log(logging.DEBUG, message, *args)
|
||||
|
||||
def info(self, message: str, *args: object) -> None:
|
||||
self.log(logging.INFO, message, *args)
|
||||
|
||||
def warning(self, message: str, *args: object) -> None:
|
||||
self.log(logging.WARNING, message, *args)
|
||||
|
||||
def error(self, message: str, *args: object) -> None:
|
||||
self.log(logging.ERROR, message, *args)
|
||||
|
||||
# ---- 输出 ----
|
||||
|
||||
def flush(self) -> list[LogEntry]:
|
||||
"""将缓冲区内容按时间顺序一次性输出到父 logger,并清空缓冲区。
|
||||
|
||||
输出时每条日志添加 [task_code] 前缀,保证日志归属可识别。
|
||||
|
||||
Returns:
|
||||
按时间戳升序排列的日志条目列表(副本)。
|
||||
"""
|
||||
with self._lock:
|
||||
entries = sorted(self._buffer, key=lambda e: e.timestamp)
|
||||
for entry in entries:
|
||||
self._parent.log(
|
||||
entry.level,
|
||||
"[%s] %s",
|
||||
entry.task_code,
|
||||
entry.message,
|
||||
)
|
||||
self._buffer.clear()
|
||||
return list(entries)
|
||||
|
||||
@property
|
||||
def entries(self) -> list[LogEntry]:
|
||||
"""返回当前缓冲区条目的副本(用于测试/检查)。"""
|
||||
with self._lock:
|
||||
return list(self._buffer)
|
||||
Reference in New Issue
Block a user