初始提交:飞球 ETL 系统全量代码
This commit is contained in:
1
.kiro/specs/scheduler-refactor/.config.kiro
Normal file
1
.kiro/specs/scheduler-refactor/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"generationMode": "requirements-first"}
|
||||
462
.kiro/specs/scheduler-refactor/design.md
Normal file
462
.kiro/specs/scheduler-refactor/design.md
Normal 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/Taipei`) |
|
||||
| `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 1:data_source 参数决定执行路径
|
||||
|
||||
*对于任意* 任务代码和任意 `data_source` 值(online/offline/hybrid),TaskExecutor 执行该任务时,抓取阶段执行当且仅当 `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 6:processing_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 8:TaskRegistry 元数据 round-trip
|
||||
|
||||
*对于任意* 任务代码、任务类和元数据组合(requires_db_config、layer、task_type),注册后通过 `get_metadata` 查询应返回相同的元数据值。
|
||||
|
||||
**验证:需求 4.1**
|
||||
|
||||
### Property 9:TaskRegistry 向后兼容默认值
|
||||
|
||||
*对于任意* 使用旧接口(仅 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 11:pipeline_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 映射一致性
|
||||
123
.kiro/specs/scheduler-refactor/requirements.md
Normal file
123
.kiro/specs/scheduler-refactor/requirements.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 需求文档:ETL 调度器重构
|
||||
|
||||
## 简介
|
||||
|
||||
当前 `orchestration/scheduler.py`(约 900 行)中的 `ETLScheduler` 类承担了过多职责:单任务执行、管道编排、资源管理。CLI 参数命名混乱(`--pipeline` vs `--pipeline-flow` vs `--processing-mode`),全局状态耦合严重,配置键语义重叠。本次重构将调度器拆分为三层架构(CLI → PipelineRunner → TaskExecutor),重新设计参数命名,消除全局状态依赖,使每层可独立测试。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **TaskExecutor**:任务执行器,负责单个 ETL 任务的执行、游标管理和运行记录
|
||||
- **PipelineRunner**:管道运行器,负责管道定义、层→任务映射、校验编排
|
||||
- **TaskRegistry**:任务注册表,管理所有已注册的任务类及其元数据
|
||||
- **DataSource**:数据源模式,取代原 `pipeline.flow`,表示数据来自在线 API(`online`)、本地 JSON(`offline`)或混合模式(`hybrid`)
|
||||
- **ProcessingMode**:处理模式,控制 ETL 执行策略(仅增量 / 仅校验 / 增量+校验)
|
||||
- **Pipeline**:管道,定义一组按层顺序执行的 ETL 任务集合(如 `api_full` = ODS → DWD → DWS → INDEX)
|
||||
- **CursorManager**:游标管理器,管理任务的时间水位(上次处理到哪里)
|
||||
- **RunTracker**:运行记录器,在 `etl_admin` Schema 中记录每次任务执行的状态和统计
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:架构分层 — TaskExecutor(执行层)
|
||||
|
||||
**用户故事:** 作为开发者,我希望单任务执行逻辑独立封装在 TaskExecutor 中,以便可以脱离管道上下文独立测试和复用。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskExecutor SHALL 封装单个任务的完整执行生命周期:创建运行记录、执行任务、更新游标、记录结果
|
||||
2. WHEN TaskExecutor 执行一个任务时,THE TaskExecutor SHALL 接收显式的 `data_source` 参数,而非读取全局状态
|
||||
3. WHEN 任务执行成功且返回有效时间窗口时,THE TaskExecutor SHALL 推进该任务的游标水位
|
||||
4. WHEN 任务执行过程中发生异常时,THE TaskExecutor SHALL 将运行记录状态更新为 FAIL 并重新抛出异常
|
||||
5. THE TaskExecutor SHALL 通过构造函数接收 `db_ops`、`api_client`、`cursor_manager`、`run_tracker`、`task_registry` 等依赖,而非自行创建
|
||||
6. WHEN 执行工具类任务(如 INIT_ODS_SCHEMA)时,THE TaskExecutor SHALL 跳过游标管理和运行记录,直接执行任务
|
||||
|
||||
### 需求 2:架构分层 — PipelineRunner(编排层)
|
||||
|
||||
**用户故事:** 作为开发者,我希望管道编排逻辑独立封装在 PipelineRunner 中,以便管道定义和校验流程可以独立演进。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE PipelineRunner SHALL 根据管道名称解析出需要执行的层列表(如 `api_full` → `["ODS", "DWD", "DWS", "INDEX"]`)
|
||||
2. WHEN PipelineRunner 执行管道时,THE PipelineRunner SHALL 委托 TaskExecutor 逐个执行任务,而非直接操作数据库或 API
|
||||
3. WHEN 处理模式为 `verify_only` 时,THE PipelineRunner SHALL 跳过增量 ETL,仅执行校验流程
|
||||
4. WHEN 处理模式为 `increment_verify` 时,THE PipelineRunner SHALL 先执行增量 ETL,再执行校验流程
|
||||
5. THE PipelineRunner SHALL 根据层列表自动选择对应的任务代码,支持配置覆盖
|
||||
6. WHEN 管道执行完成时,THE PipelineRunner SHALL 汇总所有任务的执行结果并返回统一的结果字典
|
||||
|
||||
### 需求 3:架构分层 — CLI 层重构
|
||||
|
||||
**用户故事:** 作为运维人员,我希望 CLI 参数命名清晰、语义无歧义,以便快速理解和正确使用各种执行模式。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CLI SHALL 将 `--pipeline-flow`(FULL/FETCH_ONLY/INGEST_ONLY)重命名为 `--data-source`(online/offline/hybrid),并保留旧名称作为别名
|
||||
2. THE CLI SHALL 保留 `--pipeline` 参数用于管道模式,保留 `--tasks` 参数用于传统模式
|
||||
3. WHEN 用户同时指定 `--pipeline` 和 `--tasks` 时,THE CLI SHALL 将 `--tasks` 作为管道内的任务过滤器
|
||||
4. THE CLI SHALL 保留 `--processing-mode`(increment_only/verify_only/increment_verify)参数不变
|
||||
5. WHEN 用户使用旧参数名 `--pipeline-flow` 时,THE CLI SHALL 发出弃用警告并将值映射到新的 `--data-source` 参数
|
||||
6. THE CLI SHALL 仅负责参数解析和配置加载,将执行逻辑委托给 PipelineRunner 或 TaskExecutor
|
||||
|
||||
### 需求 4:任务分类元数据化
|
||||
|
||||
**用户故事:** 作为开发者,我希望任务的分类信息(是否需要数据库配置、所属层等)由任务注册表管理,而非硬编码在调度器中。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskRegistry SHALL 支持在注册任务时附带元数据(`requires_db_config`、`layer`、`task_type`)
|
||||
2. WHEN TaskExecutor 需要判断任务是否为工具类任务时,THE TaskExecutor SHALL 查询 TaskRegistry 的元数据,而非检查硬编码集合
|
||||
3. WHEN PipelineRunner 需要根据层获取任务列表时,THE PipelineRunner SHALL 查询 TaskRegistry 的 `layer` 元数据
|
||||
4. THE TaskRegistry SHALL 保持向后兼容,无元数据的任务默认为 `requires_db_config=True`、`layer=None`
|
||||
|
||||
### 需求 5:配置键重构
|
||||
|
||||
**用户故事:** 作为运维人员,我希望配置键命名合理、语义清晰,以便正确配置 ETL 系统的运行参数。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE AppConfig SHALL 将 `app.timezone` 默认值从 `Asia/Taipei` 改为 `Asia/Shanghai`
|
||||
2. THE AppConfig SHALL 将 `pipeline.flow` 配置键重命名为 `run.data_source`,并保留旧键作为兼容别名
|
||||
3. WHEN 配置中同时存在旧键 `pipeline.flow` 和新键 `run.data_source` 时,THE AppConfig SHALL 优先使用新键的值
|
||||
4. THE AppConfig SHALL 将 `pipeline.fetch_root` 和 `pipeline.ingest_source_dir` 移至 `io` 命名空间下(`io.fetch_root`、`io.ingest_source_dir`)
|
||||
|
||||
### 需求 6:资源管理与生命周期
|
||||
|
||||
**用户故事:** 作为开发者,我希望数据库连接和 API 客户端的创建与关闭由 CLI 层统一管理,以便确保资源正确释放。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE CLI SHALL 在 `finally` 块中关闭数据库连接和 API 客户端,确保异常情况下资源也能释放
|
||||
2. THE TaskExecutor SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
|
||||
3. THE PipelineRunner SHALL 通过依赖注入接收已创建的数据库连接和 API 客户端,而非自行创建
|
||||
4. WHEN CLI 创建资源时,THE CLI SHALL 使用 Python 上下文管理器(`with` 语句)或 `try/finally` 模式管理生命周期
|
||||
|
||||
### 需求 7:静态方法归位
|
||||
|
||||
**用户故事:** 作为开发者,我希望与调度器无关的静态工具方法移至合适的模块,以便保持类的职责单一。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE `_map_run_status` 方法 SHALL 从 ETLScheduler 移至 RunTracker 或独立的工具模块
|
||||
2. THE `_filter_verify_tables` 方法 SHALL 从 ETLScheduler 移至校验相关模块
|
||||
3. WHEN 静态方法被移动后,THE 原调用方 SHALL 更新导入路径以引用新位置
|
||||
|
||||
### 需求 8:向后兼容与过渡
|
||||
|
||||
**用户故事:** 作为运维人员,我希望重构后的系统在过渡期内兼容旧的 CLI 参数和配置键,以便平滑迁移。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 用户使用旧参数 `--pipeline-flow FULL` 时,THE CLI SHALL 将其等价映射为 `--data-source hybrid` 并发出弃用警告
|
||||
2. WHEN 用户使用旧参数 `--pipeline-flow FETCH_ONLY` 时,THE CLI SHALL 将其等价映射为 `--data-source online` 并发出弃用警告
|
||||
3. WHEN 用户使用旧参数 `--pipeline-flow INGEST_ONLY` 时,THE CLI SHALL 将其等价映射为 `--data-source offline` 并发出弃用警告
|
||||
4. WHEN 配置文件中使用旧键 `pipeline.flow` 时,THE AppConfig SHALL 自动映射到新键 `run.data_source`
|
||||
5. THE 系统 SHALL 在日志中记录所有弃用映射,便于运维人员逐步迁移
|
||||
|
||||
### 需求 9:可测试性
|
||||
|
||||
**用户故事:** 作为开发者,我希望重构后的每一层都可以独立进行单元测试,以便快速验证逻辑正确性。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE TaskExecutor SHALL 支持通过注入 mock 依赖(FakeDB、FakeAPI)进行单元测试,无需真实数据库
|
||||
2. THE PipelineRunner SHALL 支持通过注入 mock TaskExecutor 进行单元测试,无需执行真实任务
|
||||
3. THE TaskRegistry SHALL 支持在测试中创建独立实例,不依赖全局 `default_registry`
|
||||
4. WHEN 运行单元测试时,THE 测试 SHALL 验证各层之间的交互契约(调用参数、返回值格式)
|
||||
147
.kiro/specs/scheduler-refactor/tasks.md
Normal file
147
.kiro/specs/scheduler-refactor/tasks.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 实现计划:ETL 调度器重构
|
||||
|
||||
## 概述
|
||||
|
||||
将 `ETLScheduler`(~900 行)拆分为 TaskExecutor(执行层)、PipelineRunner(编排层)、增强版 TaskRegistry(元数据),重构 CLI 参数和配置键,保持向后兼容。采用自底向上的实现顺序:先基础组件,再上层编排,最后 CLI 集成。
|
||||
|
||||
## 任务
|
||||
|
||||
- [x] 1. 增强 TaskRegistry,支持元数据注册与查询
|
||||
- [x] 1.1 扩展 TaskRegistry 类,添加 TaskMeta 数据类和元数据相关方法
|
||||
- 在 `orchestration/task_registry.py` 中添加 `TaskMeta` dataclass(`task_class`、`requires_db_config`、`layer`、`task_type`)
|
||||
- 修改 `register()` 方法签名,增加可选的 `requires_db_config`、`layer`、`task_type` 参数
|
||||
- 添加 `get_metadata()`、`get_tasks_by_layer()`、`is_utility_task()` 方法
|
||||
- 保持 `create_task()` 和 `get_all_task_codes()` 接口不变
|
||||
- _需求: 4.1, 4.4_
|
||||
|
||||
- [x] 1.2 更新所有任务注册调用,添加元数据
|
||||
- 将原 `NO_DB_CONFIG_TASKS` 硬编码集合中的任务标记为 `requires_db_config=False`
|
||||
- 为 ODS 任务添加 `layer="ODS"`,DWD 任务添加 `layer="DWD"`,DWS 任务添加 `layer="DWS"`,INDEX 任务添加 `layer="INDEX"`
|
||||
- 工具类任务标记 `task_type="utility"`,校验类任务标记 `task_type="verification"`
|
||||
- _需求: 4.1, 4.2, 4.3_
|
||||
|
||||
- [x] 1.3 编写 TaskRegistry 属性测试
|
||||
- **Property 8: TaskRegistry 元数据 round-trip**
|
||||
- **验证: 需求 4.1**
|
||||
|
||||
- [x] 1.4 编写 TaskRegistry 向后兼容和按层查询属性测试
|
||||
- **Property 9: TaskRegistry 向后兼容默认值**
|
||||
- **Property 10: 按层查询任务**
|
||||
- **验证: 需求 4.4, 4.3**
|
||||
|
||||
- [x] 2. 配置键重构与向后兼容
|
||||
- [x] 2.1 修改 `config/defaults.py` 默认值
|
||||
- 将 `app.timezone` 默认值从 `Asia/Taipei` 改为 `Asia/Shanghai`
|
||||
- 将 `db.session.timezone` 默认值从 `Asia/Taipei` 改为 `Asia/Shanghai`
|
||||
- 添加 `run.data_source` 键(默认 `hybrid`)
|
||||
- 将 `pipeline.fetch_root` 和 `pipeline.ingest_source_dir` 复制到 `io.fetch_root` 和 `io.ingest_source_dir`(保留旧键兼容)
|
||||
- _需求: 5.1, 5.2, 5.4_
|
||||
|
||||
- [x] 2.2 在 `config/settings.py` 的 `_normalize()` 中添加兼容映射逻辑
|
||||
- 旧键 `pipeline.flow` → 新键 `run.data_source`(值映射:FULL→hybrid, FETCH_ONLY→online, INGEST_ONLY→offline)
|
||||
- 旧键 `pipeline.fetch_root` → `io.fetch_root`,`pipeline.ingest_source_dir` → `io.ingest_source_dir`
|
||||
- 新键优先:当新旧键同时存在时,使用新键的值
|
||||
- 记录弃用警告日志
|
||||
- _需求: 5.2, 5.3, 5.4, 8.4, 8.5_
|
||||
|
||||
- [x] 2.3 编写配置映射属性测试
|
||||
- **Property 11: pipeline_flow → data_source 映射一致性**
|
||||
- **验证: 需求 8.1, 8.2, 8.3, 5.2, 8.4**
|
||||
|
||||
- [x] 3. 静态方法归位
|
||||
- [x] 3.1 将 `_map_run_status` 移至 RunTracker
|
||||
- 在 `orchestration/run_tracker.py` 中添加 `map_run_status()` 静态方法(从 `ETLScheduler._map_run_status` 复制)
|
||||
- _需求: 7.1_
|
||||
|
||||
- [x] 3.2 将 `_filter_verify_tables` 移至校验模块
|
||||
- 在 `tasks/verification/` 下合适的模块中添加 `filter_verify_tables()` 函数
|
||||
- _需求: 7.2_
|
||||
|
||||
- [x] 4. 检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 5. 实现 TaskExecutor(执行层)
|
||||
- [x] 5.1 创建 `orchestration/task_executor.py`
|
||||
- 实现 `TaskExecutor` 类,构造函数接收 `config`、`db_ops`、`api_client`、`cursor_mgr`、`run_tracker`、`task_registry`、`logger`
|
||||
- 从 `ETLScheduler` 迁移以下方法:`run_tasks`、`_run_single_task`、`_execute_fetch`、`_execute_ingest`、`_execute_ods_record_and_load`、`_run_utility_task`、`_build_fetch_dir`、`_resolve_ingest_source`、`_counts_from_fetch`、`_load_task_config`、`_maybe_run_integrity_check`、`_attach_run_file_logger`
|
||||
- 将 `data_source` 改为方法参数(替代原 `self.pipeline_flow` 全局状态)
|
||||
- 使用 `self.task_registry.is_utility_task()` 替代硬编码的 `NO_DB_CONFIG_TASKS`
|
||||
- 使用 `RunTracker.map_run_status()` 替代 `self._map_run_status()`
|
||||
- 添加 `DataSource` 枚举类(`online`/`offline`/`hybrid`)
|
||||
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
|
||||
|
||||
- [x] 5.2 编写 TaskExecutor 属性测试
|
||||
- **Property 1: data_source 参数决定执行路径**
|
||||
- **Property 2: 成功任务推进游标**
|
||||
- **Property 3: 失败任务标记 FAIL 并重新抛出**
|
||||
- **Property 4: 工具类任务由元数据决定**
|
||||
- **验证: 需求 1.2, 1.3, 1.4, 1.6, 4.2**
|
||||
|
||||
- [x] 6. 实现 PipelineRunner(编排层)
|
||||
- [x] 6.1 创建 `orchestration/pipeline_runner.py`
|
||||
- 实现 `PipelineRunner` 类,构造函数接收 `config`、`task_executor`、`task_registry`、`db_conn`、`api_client`、`logger`
|
||||
- 将 `PIPELINE_LAYERS` 常量从 `scheduler.py` 迁移至此
|
||||
- 从 `ETLScheduler` 迁移以下方法:`run_pipeline_with_verification`(重命名为 `run`)、`_run_layer_verification`(重命名为 `_run_verification`)、`_get_tasks_for_layers`(重命名为 `_resolve_tasks`)
|
||||
- 使用 `filter_verify_tables()`(已移至校验模块)替代原内联静态方法
|
||||
- 使用 `task_registry.get_tasks_by_layer()` 作为默认任务解析,配置覆盖优先
|
||||
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
|
||||
- [x] 6.2 编写 PipelineRunner 属性测试
|
||||
- **Property 5: 管道名称→层列表映射**
|
||||
- **Property 6: processing_mode 控制执行流程**
|
||||
- **Property 7: 管道结果汇总完整性**
|
||||
- **验证: 需求 2.1, 2.3, 2.4, 2.6**
|
||||
|
||||
- [x] 7. 检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
- [x] 8. 重构 CLI 层
|
||||
- [x] 8.1 重构 `cli/main.py` 参数解析
|
||||
- 添加 `--data-source` 参数(choices: online/offline/hybrid,默认 hybrid)
|
||||
- 保留 `--pipeline-flow` 作为弃用别名,使用时发出 `DeprecationWarning` 并映射到 `--data-source`
|
||||
- 更新 `build_cli_overrides()` 将 `--data-source` 写入 `run.data_source` 配置键
|
||||
- _需求: 3.1, 3.5, 8.1, 8.2, 8.3_
|
||||
|
||||
- [x] 8.2 重构 `cli/main.py` 的 `main()` 函数
|
||||
- 在 `try/finally` 块中管理 `DatabaseConnection` 和 `APIClient` 的生命周期
|
||||
- 在 `try` 块内组装 `TaskExecutor` 和 `PipelineRunner`(依赖注入)
|
||||
- 管道模式委托 `PipelineRunner.run()`,传统模式委托 `TaskExecutor.run_tasks()`
|
||||
- 添加 `resolve_data_source(args)` 辅助函数处理新旧参数映射
|
||||
- _需求: 3.2, 3.3, 3.4, 3.6, 6.1, 6.4_
|
||||
|
||||
- [x] 8.3 编写 CLI 参数解析单元测试
|
||||
- 测试 `--data-source` 新参数正确解析
|
||||
- 测试 `--pipeline-flow` 旧参数弃用映射
|
||||
- 测试 `--pipeline` + `--tasks` 同时使用时的行为
|
||||
- _需求: 3.1, 3.3, 3.5_
|
||||
|
||||
- [x] 9. 清理旧代码与集成
|
||||
- [x] 9.1 重构 `orchestration/scheduler.py` 为薄包装层
|
||||
- 将 `ETLScheduler` 改为薄包装,内部委托 `TaskExecutor` 和 `PipelineRunner`
|
||||
- 保留 `ETLScheduler` 类名和 `run_tasks()`、`run_pipeline_with_verification()`、`close()` 公共接口,标记为弃用
|
||||
- 确保 GUI 层(`gui/workers/`)等现有调用方无需立即修改
|
||||
- _需求: 8.1, 8.4_
|
||||
|
||||
- [x] 9.2 更新 GUI 工作线程中的调度器引用
|
||||
- 检查 `gui/workers/` 中对 `ETLScheduler` 的使用
|
||||
- 如有直接引用内部方法,更新为使用新的公共接口
|
||||
- _需求: 7.3_
|
||||
|
||||
- [x] 9.3 编写集成测试验证端到端流程
|
||||
- 使用 FakeDB/FakeAPI 验证 CLI → PipelineRunner → TaskExecutor 完整调用链
|
||||
- 验证传统模式和管道模式均正常工作
|
||||
- _需求: 9.4_
|
||||
|
||||
- [x] 10. 最终检查点 — 确保所有测试通过
|
||||
- 运行 `pytest tests/unit`,确保所有测试通过,如有问题请询问用户。
|
||||
|
||||
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的子任务为可选测试任务,可跳过以加速 MVP
|
||||
- 每个任务引用了具体的需求编号,确保可追溯性
|
||||
- 检查点确保增量验证,避免问题累积
|
||||
- 属性测试使用 `hypothesis` 库,验证通用正确性属性
|
||||
- 单元测试验证具体示例和边界条件
|
||||
- `ETLScheduler` 保留为薄包装层,确保 GUI 等现有调用方平滑过渡
|
||||
Reference in New Issue
Block a user