init: 项目初始提交 - NeoZQYY Monorepo 完整代码

This commit is contained in:
Neo
2026-02-15 14:58:14 +08:00
commit ded6dfb9d8
769 changed files with 182616 additions and 0 deletions

View File

@@ -0,0 +1,462 @@
# 设计文档ETL 调度器重构
## 概述
本次重构将 `ETLScheduler`(约 900 行,职责混乱的"上帝类")拆分为三层清晰的架构:
1. **CLI 层**`cli/main.py`):参数解析、配置加载、资源创建与释放
2. **PipelineRunner**`orchestration/pipeline_runner.py`):管道定义、层→任务映射、校验编排
3. **TaskExecutor**`orchestration/task_executor.py`):单任务执行、游标管理、运行记录
核心设计原则:**单个任务是最小执行单元,管道模式只是"调度拼接"**。每层通过依赖注入接收协作对象,不自行创建资源,便于独立测试。
## 架构
### 分层架构图
```mermaid
graph TD
CLI["CLI 层<br/>cli/main.py<br/>参数解析 · 配置加载 · 资源管理"]
PR["PipelineRunner<br/>orchestration/pipeline_runner.py<br/>管道定义 · 层→任务映射 · 校验编排"]
TE["TaskExecutor<br/>orchestration/task_executor.py<br/>单任务执行 · 游标管理 · 运行记录"]
TR["TaskRegistry<br/>orchestration/task_registry.py<br/>任务注册 · 元数据查询"]
CM["CursorManager"]
RT["RunTracker"]
DB["DatabaseConnection"]
API["APIClient"]
CLI -->|"创建并注入"| PR
CLI -->|"创建并注入"| TE
CLI -->|"管理生命周期"| DB
CLI -->|"管理生命周期"| API
PR -->|"委托执行"| TE
PR -->|"查询任务"| TR
TE -->|"查询元数据"| TR
TE -->|"管理游标"| CM
TE -->|"记录运行"| RT
TE -->|"使用"| DB
TE -->|"使用"| API
```
### 调用流程
**传统模式**`--tasks`
```
CLI → TaskExecutor.run_tasks([task_codes]) → TaskExecutor._run_single_task() × N
```
**管道模式**`--pipeline`
```
CLI → PipelineRunner.run(pipeline, processing_mode, ...)
→ PipelineRunner._resolve_tasks(layers)
→ TaskExecutor.run_tasks([resolved_tasks])
→ [可选] PipelineRunner._run_verification(layers, ...)
```
## 组件与接口
### TaskExecutor
负责单任务执行的完整生命周期。从原 `ETLScheduler` 中提取 `_run_single_task``_execute_fetch``_execute_ingest``_execute_ods_record_and_load``_run_utility_task` 等方法。
```python
class TaskExecutor:
def __init__(
self,
config: AppConfig,
db_ops: DatabaseOperations,
api_client: APIClient,
cursor_mgr: CursorManager,
run_tracker: RunTracker,
task_registry: TaskRegistry,
logger: logging.Logger,
):
...
def run_tasks(
self,
task_codes: list[str],
data_source: str = "hybrid", # online / offline / hybrid
) -> list[dict[str, Any]]:
"""批量执行任务列表,返回每个任务的结果。"""
...
def run_single_task(
self,
task_code: str,
run_uuid: str,
store_id: int,
data_source: str = "hybrid",
) -> dict[str, Any]:
"""执行单个任务的完整生命周期。"""
...
```
关键变化:
- `data_source` 作为显式参数传入,不再读取 `self.pipeline_flow` 全局状态
- 工具类任务判断通过 `TaskRegistry.get_metadata(task_code)` 查询,不再硬编码
- 不自行创建 `DatabaseConnection``APIClient`
### PipelineRunner
负责管道编排。从原 `ETLScheduler` 中提取 `run_pipeline_with_verification``_run_layer_verification``_get_tasks_for_layers` 等方法。
```python
class PipelineRunner:
# 管道定义(从 scheduler.py 模块级常量迁移至此)
PIPELINE_LAYERS: dict[str, list[str]] = {
"api_ods": ["ODS"],
"api_ods_dwd": ["ODS", "DWD"],
"api_full": ["ODS", "DWD", "DWS", "INDEX"],
"ods_dwd": ["DWD"],
"dwd_dws": ["DWS"],
"dwd_dws_index": ["DWS", "INDEX"],
"dwd_index": ["INDEX"],
}
def __init__(
self,
config: AppConfig,
task_executor: TaskExecutor,
task_registry: TaskRegistry,
db_conn: DatabaseConnection,
api_client: APIClient,
logger: logging.Logger,
):
...
def run(
self,
pipeline: str,
processing_mode: str = "increment_only",
data_source: str = "hybrid",
window_start: datetime | None = None,
window_end: datetime | None = None,
window_split: str | None = None,
task_codes: list[str] | None = None,
fetch_before_verify: bool = False,
verify_tables: list[str] | None = None,
) -> dict[str, Any]:
"""执行管道,返回汇总结果。"""
...
def _resolve_tasks(self, layers: list[str]) -> list[str]:
"""根据层列表解析任务代码,优先查询 TaskRegistry 元数据。"""
...
def _run_verification(self, layers, window_start, window_end, ...):
"""执行后置校验(从原 _run_layer_verification 迁移)。"""
...
```
### TaskRegistry增强
在现有注册功能基础上增加元数据支持。
```python
@dataclass
class TaskMeta:
"""任务元数据"""
task_class: type
requires_db_config: bool = True
layer: str | None = None # "ODS" / "DWD" / "DWS" / "INDEX" / None
task_type: str = "etl" # "etl" / "utility" / "verification"
class TaskRegistry:
def __init__(self):
self._tasks: dict[str, TaskMeta] = {}
def register(
self,
task_code: str,
task_class: type,
requires_db_config: bool = True,
layer: str | None = None,
task_type: str = "etl",
):
"""注册任务类及其元数据。"""
self._tasks[task_code.upper()] = TaskMeta(
task_class=task_class,
requires_db_config=requires_db_config,
layer=layer,
task_type=task_type,
)
def create_task(self, task_code, config, db_connection, api_client, logger):
"""创建任务实例(保持原有接口不变)。"""
...
def get_metadata(self, task_code: str) -> TaskMeta | None:
"""查询任务元数据。"""
...
def get_tasks_by_layer(self, layer: str) -> list[str]:
"""获取指定层的所有任务代码。"""
...
def is_utility_task(self, task_code: str) -> bool:
"""判断是否为工具类任务(不需要游标/运行记录)。"""
meta = self.get_metadata(task_code)
return meta is not None and not meta.requires_db_config
def get_all_task_codes(self) -> list[str]:
"""获取所有已注册的任务代码(保持原有接口)。"""
...
```
### CLI 层重构
```python
# cli/main.py 核心流程伪代码
def main():
args = parse_args()
config = AppConfig.load(build_cli_overrides(args))
# 资源创建
db_conn = DatabaseConnection(...)
api_client = APIClient(...)
try:
# 组装依赖
db_ops = DatabaseOperations(db_conn)
cursor_mgr = CursorManager(db_conn)
run_tracker = RunTracker(db_conn)
registry = default_registry
executor = TaskExecutor(config, db_ops, api_client, cursor_mgr, run_tracker, registry, logger)
if args.pipeline:
runner = PipelineRunner(config, executor, registry, db_conn, api_client, logger)
runner.run(
pipeline=args.pipeline,
processing_mode=args.processing_mode,
data_source=resolve_data_source(args),
...
)
else:
task_codes = config.get("run.tasks")
data_source = resolve_data_source(args)
executor.run_tasks(task_codes, data_source=data_source)
finally:
db_conn.close()
```
### 参数映射
| 旧参数 | 旧值 | 新参数 | 新值 | 说明 |
|--------|------|--------|------|------|
| `--pipeline-flow` | `FULL` | `--data-source` | `hybrid` | 在线抓取 + 本地入库 |
| `--pipeline-flow` | `FETCH_ONLY` | `--data-source` | `online` | 仅在线抓取落盘 |
| `--pipeline-flow` | `INGEST_ONLY` | `--data-source` | `offline` | 仅本地清洗入库 |
### 静态方法归位
| 方法 | 原位置 | 新位置 | 理由 |
|------|--------|--------|------|
| `_map_run_status` | `ETLScheduler` | `RunTracker` | 状态映射是运行记录的职责 |
| `_filter_verify_tables` | `ETLScheduler` | `tasks/verification/` 模块 | 校验表过滤是校验模块的职责 |
## 数据模型
### TaskMeta新增
```python
@dataclass
class TaskMeta:
task_class: type # 任务类引用
requires_db_config: bool = True # 是否需要数据库任务配置(游标/运行记录)
layer: str | None = None # 所属层:"ODS"/"DWD"/"DWS"/"INDEX"/None
task_type: str = "etl" # 任务类型:"etl"/"utility"/"verification"
```
### DataSource 枚举
```python
class DataSource(str, Enum):
ONLINE = "online" # 仅在线抓取(原 FETCH_ONLY
OFFLINE = "offline" # 仅本地入库(原 INGEST_ONLY
HYBRID = "hybrid" # 抓取 + 入库(原 FULL
```
### 配置键映射
| 旧键 | 新键 | 默认值 |
|------|------|--------|
| `app.timezone` | `app.timezone` | `Asia/Shanghai`(原 `Asia/Shanghai` |
| `pipeline.flow` | `run.data_source` | `hybrid` |
| `pipeline.fetch_root` | `io.fetch_root` | `export/JSON` |
| `pipeline.ingest_source_dir` | `io.ingest_source_dir` | `""` |
### 任务执行结果(不变)
```python
# 单任务结果
{
"task_code": str,
"status": str, # "SUCCESS" / "FAIL" / "SKIP"
"counts": {
"fetched": int,
"inserted": int,
"updated": int,
"skipped": int,
"errors": int,
},
"window": {"start": datetime, "end": datetime, "minutes": int} | None,
"dump_dir": str | None,
}
# 管道结果
{
"status": str,
"pipeline": str,
"layers": list[str],
"results": list[dict], # 各任务结果
"verification_summary": dict | None, # 校验汇总
}
```
## 正确性属性
*正确性属性是一种在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格与机器可验证正确性保证之间的桥梁。*
### Property 1data_source 参数决定执行路径
*对于任意* 任务代码和任意 `data_source`online/offline/hybridTaskExecutor 执行该任务时,抓取阶段执行当且仅当 `data_source``online``hybrid`,入库阶段执行当且仅当 `data_source``offline``hybrid`
**验证:需求 1.2**
### Property 2成功任务推进游标
*对于任意* 非工具类任务,当任务执行成功且返回包含有效 `window`(含 `start``end`的结果时CursorManager.advance 应被调用且参数与返回的窗口一致。
**验证:需求 1.3**
### Property 3失败任务标记 FAIL 并重新抛出
*对于任意* 非工具类任务当任务执行过程中抛出异常时RunTracker 应被更新为 FAIL 状态,且该异常应被重新抛出给调用方。
**验证:需求 1.4**
### Property 4工具类任务由元数据决定
*对于任意* 任务代码TaskExecutor 是否跳过游标管理和运行记录,取决于 TaskRegistry 中该任务的 `requires_db_config` 元数据。当 `requires_db_config=False` 时跳过,否则执行完整生命周期。
**验证:需求 1.6, 4.2**
### Property 5管道名称→层列表映射
*对于任意* 有效的管道名称PipelineRunner 解析出的层列表应与 `PIPELINE_LAYERS` 字典中的定义完全一致。
**验证:需求 2.1**
### Property 6processing_mode 控制执行流程
*对于任意* processing_mode 值,增量 ETL 执行当且仅当模式包含 `increment`(即 `increment_only``increment_verify`),校验流程执行当且仅当模式包含 `verify`(即 `verify_only``increment_verify`)。
**验证:需求 2.3, 2.4**
### Property 7管道结果汇总完整性
*对于任意* 一组任务执行结果PipelineRunner 返回的汇总字典应包含 `status``pipeline``layers``results` 字段,且 `results` 列表长度等于实际执行的任务数。
**验证:需求 2.6**
### Property 8TaskRegistry 元数据 round-trip
*对于任意* 任务代码、任务类和元数据组合requires_db_config、layer、task_type注册后通过 `get_metadata` 查询应返回相同的元数据值。
**验证:需求 4.1**
### Property 9TaskRegistry 向后兼容默认值
*对于任意* 使用旧接口(仅 task_code 和 task_class注册的任务查询元数据应返回 `requires_db_config=True``layer=None``task_type="etl"`
**验证:需求 4.4**
### Property 10按层查询任务
*对于任意* 注册了 `layer` 元数据的任务集合,`get_tasks_by_layer(layer)` 返回的任务代码集合应等于所有 `layer` 匹配的已注册任务代码集合。
**验证:需求 4.3**
### Property 11pipeline_flow → data_source 映射一致性
*对于任意*`pipeline_flow`FULL/FETCH_ONLY/INGEST_ONLY映射到 `data_source` 的结果应与预定义映射表一致FULL→hybrid、FETCH_ONLY→online、INGEST_ONLY→offline。同样配置键 `pipeline.flow` 应自动映射到 `run.data_source`
**验证:需求 8.1, 8.2, 8.3, 5.2, 8.4**
## 错误处理
### TaskExecutor 错误处理
- 任务执行异常:更新 RunTracker 状态为 FAIL含 error_message然后重新抛出异常
- 游标推进失败:记录错误日志,不影响任务结果(任务本身已成功)
- 任务配置不存在:返回 `{"status": "SKIP"}` 结果,不抛异常
### PipelineRunner 错误处理
- 单个任务失败:记录错误,继续执行后续任务(与当前行为一致)
- 校验框架未安装:返回 `{"status": "SKIPPED"}` 并记录警告
- 无效管道名称:抛出 `ValueError`
### CLI 错误处理
- 配置加载失败:`SystemExit` 并输出错误信息
- 资源创建失败:`SystemExit` 并输出错误信息
- 执行过程异常:记录错误日志,`finally` 块确保资源释放,返回非零退出码
### 弃用警告
- 使用 Python `warnings.warn(DeprecationWarning)` 发出弃用警告
- 同时在日志中记录映射详情,便于运维排查
## 测试策略
### 单元测试
使用 `pytest` + 现有的 `FakeDB`/`FakeAPI` 测试工具(`tests/unit/task_test_utils.py`)。
**TaskExecutor 测试**
- 注入 mock 依赖FakeDB、FakeAPI、mock CursorManager、mock RunTracker
- 验证成功/失败/跳过三种路径
- 验证工具类任务不触发游标/运行记录
- 验证 data_source 参数正确控制抓取/入库阶段
**PipelineRunner 测试**
- 注入 mock TaskExecutor
- 验证不同 processing_mode 下的执行流程
- 验证管道→层→任务的解析链
**TaskRegistry 测试**
- 验证元数据注册和查询
- 验证向后兼容(无元数据注册)
- 验证按层查询
**配置兼容性测试**
- 验证旧键→新键映射
- 验证优先级规则
- 验证默认值变更
### 属性测试
使用 `hypothesis` 库进行属性测试,每个属性至少运行 100 次迭代。
每个属性测试必须用注释标注对应的设计属性编号:
```python
# Feature: scheduler-refactor, Property 8: TaskRegistry 元数据 round-trip
```
**属性测试覆盖**
- Property 1: data_source 参数决定执行路径
- Property 2: 成功任务推进游标
- Property 3: 失败任务标记 FAIL 并重新抛出
- Property 4: 工具类任务由元数据决定
- Property 5: 管道名称→层列表映射
- Property 6: processing_mode 控制执行流程
- Property 7: 管道结果汇总完整性
- Property 8: TaskRegistry 元数据 round-trip
- Property 9: TaskRegistry 向后兼容默认值
- Property 10: 按层查询任务
- Property 11: pipeline_flow → data_source 映射一致性