增加sqlalchemy后端,优化日志结构

This commit is contained in:
strepsiades
2026-04-07 20:22:25 +08:00
parent c377264610
commit ba73b30a04
23 changed files with 1586 additions and 308 deletions
+415 -206
View File
@@ -1,299 +1,508 @@
# 运行配置业务说明
# 运行配置写法说明
本文档从业务视角说明配置字段含义、对阶段行为的影响,以及推荐的覆盖方式
这份文档只讲一件事:现在 `ecm_sync_system` 的配置文件应该怎么写
## 0. 配置约定
重点不是把所有默认值抄一遍,而是说明当前真实生效的写法、哪些字段是内置能力、哪些字段来自业务侧扩展,以及单项目和多项目文件各自长什么样。
当前运行配置分成两层:
## 1. 先分清两类配置文件
1. 输入配置
- 你写在 `run_profiles/*.yaml``run_profiles/preset/*.default.yaml` 里的内容。
- 只建议写必要覆盖项,不要把默认值全部展开。
2. Resolved Full Config
- pipeline 启动时打印并写入日志的全量配置。
- 其中已经补齐 datasource 默认值、策略默认值和日志/持久化默认值。
当前有两类 YAML
也就是说:
- 模板应当尽量短。
- 排障时看启动日志里的 **Resolved Full Config**
1. 单 pipeline 配置
- 对应 `PipelineRunConfig`
- 用来描述一条完整同步链路
- 顶层通常包含:`node_types``local_datasource``remote_datasource``persist``logging``strategies`
2. 多项目编排配置
- 对应 `MultiProjectRunConfig`
- 只负责描述“先跑全局,再按项目并发跑哪些项目”
- 顶层包含:`global_config``default_project_config``max_concurrency``project_bootstrap_node_types``projects`
### 0.1 一个最小输入示例
判断规则很简单:
- 如果文件里有 `global_config``default_project_config``projects`,它就是多项目编排配置。
- 否则按单 pipeline 配置解析。
## 2. 配置加载约定
当前约定如下:
- 跟踪在仓库里的模板和示例,只写必要覆盖项,不重复展开默认值。
- pipeline 启动后会打印一份 `Resolved Full Config`,它才是最终生效的完整配置。
- 排障优先看 `Resolved Full Config`,不要只盯着输入 YAML。
## 3. 占位符规则
`ecm_sync_system` 核心库内置只认识一个占位符:
- `${PROJECT_ROOT}`:运行时替换为 `ecm_sync_system` 仓库根目录绝对路径。
例如:
```yaml
persist:
backend: sqlite
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/demo.db
```
需要特别区分的是:
- `${BACKEND_ROOT}``${ECM_SYNC_LOG_DIR}` 这类占位符不是核心库通用语义。
- 它们通常是 backend 集成脚本在读取 profile 时额外替换的调用方语义。
- 如果你不是通过 backend 的联调脚本运行,而是直接在 `ecm_sync_system` 内跑 profile,就不要假设这些占位符一定可用。
## 4. 单 pipeline 配置的当前写法
### 4.1 最小骨架
```yaml
node_types:
- company
- supplier
- project
local_datasource:
type: jsonl
jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
remote_datasource:
type: jsonl
jsonl_dir: ${PROJECT_ROOT}/remote_datasource/datasource
type: api
api_base_url: http://127.0.0.1:8000/api/third/v2
api_uid: your_uid
api_secret: your_secret
persist:
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/demo.db
backend: sqlite
backend_options: {}
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/demo.db
logging:
file_dir: ${PROJECT_ROOT}/test_results/sync_state_machine
file_name_pattern: demo_{timestamp}.log
strategies:
project:
config:
auto_bind: true
auto_bind_fields:
- name
local_orphan_action: none
remote_orphan_action: none
update_direction: push
```
### 0.2 对应的全量 resolved 结果骨架
### 4.2 顶层字段含义
- `node_types`
- 本轮参与同步的节点类型列表。
- 顺序会影响执行顺序,依赖链通常要把上游类型放前面。
- `local_datasource` / `remote_datasource`
- 定义两侧数据源类型和对应参数。
- `persist`
- 定义持久化后端及目标位置。
- `logging`
- 定义日志初始化、落盘位置和级别。
- `strategies`
- 定义每个 `node_type` 的同步策略。
## 5. datasource 字段怎么写
### 5.1 内置 datasource type
核心库内置注册了两种 datasource type
1. `jsonl`
2. `api`
#### `jsonl` 写法
```yaml
node_types:
- company
- supplier
- user
- project
...
local_datasource:
type: jsonl
jsonl_dir: /abs/path/filtered_datasource/datasource/filtered
jsonl_dir: ${PROJECT_ROOT}/filtered_datasource/datasource/filtered
read_only: false
handler_configs: {}
handler_configs:
project:
data_id_filter:
- project-id-1
```
字段说明:
- `jsonl_dir`JSONL 数据目录。
- `read_only`:是否只读。
- `handler_configs`:按 `node_type` 下发给 handler 的扩展参数。
#### `api` 写法
```yaml
remote_datasource:
type: jsonl
jsonl_dir: /abs/path/remote_datasource/datasource
read_only: false
handler_configs: {}
type: api
api_base_url: http://127.0.0.1:8000/api/third/v2
api_uid: your_uid
api_secret: your_secret
api_debug: true
poll_max_retries: 10
poll_interval: 0.5
api_rate_limit_max_requests: 30
api_rate_limit_window_seconds: 10.0
handler_configs:
project:
data_id_filter:
- remote-project-id-1
```
字段说明:
- `api_debug`:开启后输出更详细的请求日志。
- `poll_max_retries` / `poll_interval`:异步任务轮询控制。
- `api_rate_limit_max_requests` / `api_rate_limit_window_seconds`:全局限流窗口。
- `handler_configs`:同样按 `node_type` 下发业务参数。
### 5.2 业务侧注册的 datasource type
核心库允许业务系统在启动阶段注册自己的 datasource type。
因此,配置里不只可以写 `jsonl``api`,还可以写业务侧注册出来的类型,例如 backend 里的 `depm_db`
当前 backend 的 `depm_db` 真实写法是:
```yaml
local_datasource:
type: depm_db
tenant_id: "019976df-0628-7c46-a896-81d3f290a7df"
app:
config_file: ${BACKEND_ROOT}/config.yaml
overrides:
database:
url: mysql+aiomysql://root:123456@127.0.0.1/depm_prod_local_auto_test?charset=utf8mb4
handler_configs:
project:
data_id_filter:
- 019a62a5-2323-7be2-99c8-82534f8befa4
```
这个例子说明两点:
1. datasource 的可写字段由对应注册模型决定,不是所有 type 都共用同一套字段。
2. 如果 `type` 没有先被注册,配置校验会直接报错,不会做降级处理。
`depm_db` 这类业务扩展字段需要以业务侧实现为准。对 backend 当前实现来说:
- `tenant_id`DEPM 本地 datasource 的租户过滤 ID。
- `app.config_file`:backend 配置文件路径,默认可落回 `backend/config.yaml`
- `app.overrides`:直接传给 backend `Settings()` 的覆盖项。
## 6. strategy 写法
### 6.1 当前推荐写法
`strategies` 现在推荐写成“按 `node_type` 为键”的映射,而不是手工写一长串列表。
```yaml
strategies:
- node_type: project
skip_sync: false
supplier:
skip_post_check: true
config:
auto_bind: true
auto_bind_fields:
- name
pre_bind_data_id: []
- credit_code
depend_fields: {}
local_orphan_action: none
remote_orphan_action: create_local
update_direction: none
project:
domain_option:
pull_group_plan_production_date: true
config:
auto_bind: false
auto_bind_fields: []
pre_bind_data_id:
- local_data_id: local-project-id
remote_data_id: remote-project-id
depend_fields:
company_id: company
local_orphan_action: create_remote
local_orphan_action: none
remote_orphan_action: none
update_direction: push
post_check_ignore_fields: []
compare_ignore_fields: []
domain_option: {}
```
### 6.2 常用字段
- `auto_bind`
- 是否开启自动绑定。
- `auto_bind_fields`
- 自动绑定使用的业务键字段。
- `pre_bind_data_id`
- 显式 local/remote data_id 绑定对。
- `depend_fields`
- 依赖字段到依赖 `node_type` 的映射。
- `local_orphan_action` / `remote_orphan_action`
- 孤儿节点创建策略。
- `update_direction`
- `push` / `pull` / `none`
- `post_check_ignore_fields`
- 只影响最终一致性检查,不影响 create/update 差异检测。
- `compare_ignore_fields`
- schema diff / validate 时忽略的顶层字段。
- `skip_sync`
- 只跳过 create/update,不跳过 load/bind。
- `skip_post_check`
- 跳过该类型的 post-check reload 与一致性校验。
- `domain_option`
- 领域策略自己的扩展参数,由对应 domain 自己解释。
### 6.3 `preset` 仍然可用
如果只是想套一组预设策略,可以这样写:
```yaml
strategies:
project:
preset: pull_only
```
当前内置预设包括:
- `first_sync`
- `daily_sync`
- `repair_sync`
- `pull_only`
- `push_only`
### 6.4 已废弃的旧字段不要再写
以下 legacy key 已明确拒绝:
- `force_sync`
- `load_mode`
如果你在 strategy 里写这两个字段,配置会直接报错。
当前替代方式是:
- 是否跳过同步,用 `skip_sync`
- handler 特有加载参数,写到 `datasource.handler_configs.<node_type>`
## 7. persist 的当前写法
### 7.1 校验规则
当前 `persist` 配置遵循下面的规则:
1. `db_path``url` 至少要有一个。
2.`backend != sqlite` 时,必须显式提供 `url`
3. `wipe_on_start=true` 只会尝试删除可映射到文件路径的持久化文件;对 MySQL 这类远程数据库不会帮你删库。
### 7.2 SQLite 文件持久化
```yaml
persist:
backend: sqlite
db_path: /abs/path/test_results/sync_state_machine/demo.db
backend_options: {}
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/simple_pipeline_sm.db
enable: true
wipe_on_start: false
```
### 7.3 SQLAlchemy 持久化到 SQLite URL
```yaml
persist:
backend: sqlalchemy
url: sqlite+aiosqlite:////abs/path/to/pipeline.db
enable: true
wipe_on_start: false
```
### 7.4 SQLAlchemy 持久化到 MySQL
```yaml
persist:
backend: sqlalchemy
url: mysql+aiomysql://root:123456@127.0.0.1/ecm_sync_system?charset=utf8mb4
enable: true
wipe_on_start: false
```
当前语义:
- `sqlalchemy` backend 会自动创建它自己的 `nodes``bindings` 表及索引。
- 它不会替你创建 MySQL 数据库本身,所以 `ecm_sync_system` 这个库需要提前存在。
- 现有表结构已经兼容 MySQL,不需要再额外给主键字段手工指定前缀长度。
### 7.5 `backend_options` 什么时候用
`backend_options` 仍然保留,但现在优先使用显式字段:
- `db_path`
- `url`
- `enable`
- `wipe_on_start`
除非某个自定义 backend 明确要求,否则不要把主连接信息再塞进 `backend_options`
## 8. logging 的当前写法
```yaml
logging:
initialize: true
file_path: null
file_dir: null
file_name_pattern: null
file_dir: ${PROJECT_ROOT}/test_results/sync_state_machine
file_name_pattern: simple_pipeline_sm_{timestamp}.log
level: 20
suppress_node_logs: true
```
## 1. 推荐使用方式(先 default,再覆盖)
字段说明:
1. 先使用基线配置运行:
- `run_profiles/preset/jsonl_to_jsonl.default.yaml`
2. 若需项目定制,再复制为:
- `run_profiles/jsonl_to_jsonl.local.yaml`
3. 仅改有业务差异的字段,避免一次改太多导致排障困难。
- `initialize`
- 是否初始化应用日志。
- `file_path`
- 直接指定日志文件绝对路径。
- `file_dir`
- 只指定目录时,实际文件名由 `file_name_pattern` 生成。
- `file_name_pattern`
- 支持 `{timestamp}` 占位。
- `level`
- 默认 `20`,即 `INFO`
- `suppress_node_logs`
- 是否压缩逐节点日志噪音。
占位符:
- `${PROJECT_ROOT}` 会在运行时替换为项目根目录绝对路径。
## 9. 多项目编排文件怎么写
## 2. 顶层字段(你在改什么)
### 9.1 结构
- `node_types`
- 本轮参与同步的业务类型列表。
- 顺序会影响依赖链执行时机(当前按 node_type 串行执行 bind/create/update)。
- `local_datasource` / `remote_datasource`
- 分别定义两侧数据来源与执行端。
- `strategies`
- 每个业务类型的行为策略(依赖、自动绑定、创建方向、更新方向、跳过等)。
- `persist`
- 持久化开关与数据库位置。
- `persist.backend`
- 持久化后端注册表里的名称,例如 `sqlite`
- backend 实现会自行解释 `persist` 的其余字段。
- `persist.backend_options`
- 后端私有配置,后端自己决定如何解释。
### 6. 持久化后端注册
如果你希望在 backend 初始化阶段替换持久化后端,可以先在启动代码里按名称注册,再通过配置选择,不需要改调用方。
#### 6.1 内置 SQLite
多项目文件不是把所有字段都堆在一起,而是引用两份单 pipeline 配置:
```yaml
persist:
backend: sqlite
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/demo.db
backend_options: {}
global_config: run_profiles/jsonl_to_jsonl.multi_project.global.yaml
default_project_config: run_profiles/jsonl_to_jsonl.multi_project.project.yaml
project_bootstrap_node_types:
- company
- supplier
max_concurrency: 8
projects:
- local_project_id: local-project-1
remote_project_id: remote-project-1
- local_project_id: local-project-2
remote_project_id: remote-project-2
```
#### 6.2 注册自定义后端
字段说明:
```python
from sync_state_machine.common.persistence import register_persistence_backend
- `global_config`
- 全局公共节点要跑的单 pipeline 配置。
- `default_project_config`
- 每个项目默认使用的单 pipeline 配置。
- `project_bootstrap_node_types`
- 需要从 global scope 继承到项目 scope 的公共节点类型,例如 `company``supplier`
- `max_concurrency`
- 项目并发数上限,默认 `20`
- `projects`
- 项目映射清单。
register_persistence_backend("mysql", lambda config: MySQLPersistenceBackend(config))
```
### 9.2 项目项支持单独切配置
然后在配置里选择这个名字
如果某个项目需要特殊策略,可以在项目项里覆盖 `config_path`
```yaml
persist:
backend: mysql
db_path: ${PROJECT_ROOT}/test_results/sync_state_machine/demo.db
backend_options:
url: mysql+aiomysql://user:pass@127.0.0.1/depm_sync?charset=utf8mb4
pool_size: 5
projects:
- local_project_id: local-project-1
remote_project_id: remote-project-1
- local_project_id: local-project-2
remote_project_id: remote-project-2
config_path: run_profiles/special_project.pipeline.yaml
```
后端实现会收到整份 `PersistConfig`,自行决定是读取 `db_path``backend_options`,还是额外扩展别的字段
- `logging`
- 运行日志输出位置与级别。
此时该项目不会再用 `default_project_config`,而是改用自己的单 pipeline 配置文件
## 3. 数据源字段(DataSource
### 9.3 当前多项目 profile 的实际风格
### 3.1 JSONL 数据源
以当前 backend 两项目推送链路为例,常见组合是:
- `type=jsonl`:使用本地 JSONL 文件作为数据源。
- `jsonl_dir`JSONL 目录路径。
- `read_only=false`:允许该侧执行 create/update 回写;若为 true,通常用于只读加载。
- `handler_configs.project.data_id_filter=[]`:项目白名单;为空时不进行项目筛选。
- `handler_configs`:按节点类型下发给各业务 handler 的扩展参数。
- `local_datasource.type: depm_db`
- `remote_datasource.type: api`
- `persist.backend: sqlalchemy`
- `persist.url: mysql+aiomysql://...`
- `strategies.project.config.pre_bind_data_id` 明确写每个项目的 local/remote 映射
### 3.2 API 数据源
这类 profile 本质上仍然是“多项目编排文件 + 两份单 pipeline 文件”的组合,不要把它理解成第三种配置格式。
- `type=api`:对接 HTTP API。
- `api_base_url` / `api_uid` / `api_secret`:接口访问参数。
- `api_debug`:开启后输出更多调试信息。
- `poll_max_retries` / `poll_interval`:异步任务轮询次数与间隔。
- `handler_configs.project.data_id_filter`:建议显式配置的项目白名单;至少需要指定一个项目ID,以避免加载过多远程数据。
- `handler_configs`:业务 handler 扩展参数。
## 10. 当前推荐的写法习惯
## 4. 策略字段(Strategy
### 10.1 只写必要覆盖项
每个 node_type 的 **Resolved Full Config** 都会包含以下字段,但输入配置不需要全部显式写出。
推荐:
### 4.1 `update_direction`
- 只写与你当前环境、项目过滤、持久化目标、策略差异直接相关的字段。
- `push`:本地变更推送到远端。
- `pull`:远端变更拉取到本地。
- `none`:关闭 update 阶段。
不推荐:
### 4.2 `local_orphan_action` / `remote_orphan_action`
- 把默认值全部展开复制一遍。
- `create_remote`:本地孤儿创建到远端。
- `create_local`:远端孤儿创建到本地。
- `none`:不自动创建。
- `delete_local` / `delete_remote`:当前主流程未作为默认路径,谨慎使用。
原因很直接:
### 4.3 `auto_bind` / `auto_bind_fields` / `pre_bind_data_id`
- 默认值一旦调整,你的大段本地 YAML 会悄悄过时。
- 排障时也更难看出你到底改了什么。
- `auto_bind=true`:在 `S02(MISSING)` 上尝试自动匹配。
- `auto_bind=false`:不会做自动匹配,也不会走自动创建识别路径;通常会进入阻断态等待人工处理。
- `auto_bind_fields`:自动匹配使用的业务键字段。
- `pre_bind_data_id`:显式 data_id 预绑定清单(通用能力),格式为:
- `- local_data_id: "..."`
` remote_data_id: "..."`
- 规则:若两侧数据源都存在这对 data_id 对应节点,且当前无绑定记录,则直接绑定。
- 不依赖 `auto_bind/auto_bind_fields`,即使 `auto_bind=false` 也会执行。
- 不做业务键一对一匹配检查,按你给定的映射对优先执行。
### 10.2 handler 特有参数放 datasource 里
### 4.4 `depend_fields`
- 定义依赖字段到依赖 node_type 的映射,例如:`company_id -> company`
- 依赖节点状态非 `NORMAL` 时,会在 bind 的依赖检查阶段进入 `DEPENDENCY_ERROR`
### 4.5 `skip_sync`
- 运行配置中已不建议使用 `skip_sync`
- 推荐使用显式组合:
- `local_orphan_action: none`
- `remote_orphan_action: none`
- `update_direction: none`
- `skip_sync=true` 的当前语义是:
- 仍会执行该 node_type 的 load / bind
- 只跳过 create / update 写入阶段
- 适合“保留绑定与依赖可见性,但临时禁止写入”的场景。
- `none/none/none` 是更推荐的输入风格,因为它表达的是“该类型不做创建、不做补本地、不做更新”。
- 这组配置不会绕过前置 load / bind / 依赖检查;它只会把该类型变成无写入策略。
### 4.6 Handler 级参数(`handler_configs`
- 建议将 handler 特有参数放在 datasource 的 `handler_configs.<node_type>` 下。
- 示例:
- `remote_datasource.handler_configs.supplier.load_mode: persisted_only`
- `persisted_only` 表示只使用已持久化数据,不主动从外部拉新数据(具体以对应 handler 实现为准)。
### 4.7 `domain_option`
- `domain_option` 用于承载 domain 自己识别的运行时扩展参数。
- 通用策略行为仍然放在 `config` 下;只有领域特例才放到 `domain_option`
示例 1`project` 先从远端拉回两个特殊投产日期字段,再执行默认 push
比如:
```yaml
project:
domain_option:
pull_group_plan_production_date: true
config:
update_direction: push
remote_datasource:
type: api
handler_configs:
contract_settlement_detail:
load_method: aggregate
```
示例 2`project_detail_data` 用单个开关启用固定业务规则,把集团保供/爬坡/挑战产能计划按 pull 处理
这类参数应该写在 `datasource.handler_configs.<node_type>`,不要错写到 strategy 里。
### 10.3 项目过滤显式下沉到 handler
例如:
```yaml
project_detail_data:
domain_option:
pull_group_production_plans: true
config:
update_direction: push
handler_configs:
project:
data_id_filter:
- project-id-1
- project-id-2
```
含义:
- `project.domain_option.pull_group_plan_production_date=true` 时,`ic_plan_first_production_date``ic_plan_full_production_date` 会先 remote -> local,再执行剩余字段的常规更新。
- `project_detail_data.domain_option.pull_group_production_plans=true` 时,固定的 `ensure_production``climb_production``challenge_production` 会先 remote -> local;其余 key 继续沿用 `config.update_direction`
不要在 pipeline 外层再做一层临时过滤逻辑。项目范围应尽量体现在配置里,并由对应 handler 解释。
## 5. 阶段行为与配置关系
### 10.4 看到报错先修配置,不要做降级
- bind 阶段
- `depend_fields` 决定是否做依赖校验。
- `pre_bind_data_id` 先按显式 local/remote data_id 映射做预绑定。
- `auto_bind/auto_bind_fields` 决定是否继续进行业务键自动绑定。
- create 阶段
- `local_orphan_action` / `remote_orphan_action` 决定孤儿是否创建。
- update 阶段
- `update_direction` 决定是否和向哪侧更新。
- 写入后 reloadPipeline 内置)
- 对 API / 需要重新拉取权威状态的数据源,create 或 update 成功后仍会按 node_type reload。
- 对纯 JSONL -> JSONL pipelinereload 会跳过,直接复用当前内存状态。
- 目的:避免对离线 JSONL 数据做无意义重复扫描,同时保留 API 数据源的权威状态刷新。
- 同步后一致性检查(Pipeline 内置)
- 每个 node_type 在本轮同步后会基于绑定对执行数据一致性校验(比较时忽略 `id/_id` 字段)。
- 若发现差异,会在日志中打印差异样本,并在汇总表新增 `Consistent` 列展示 `Y` / `N(差异数)`
- 全局跳过
- `local_orphan_action=none` + `remote_orphan_action=none` + `update_direction=none`
可将该类型收敛为“只做 load/bind,不做 create/update 写入”的只读同步策略。
当前系统对配置保持 fail-fast:
## 6. 覆盖配置建议
- datasource type 未注册会报错
- legacy strategy key 会报错
- `persist` 缺失目标会报错
- profile 结构不符合 schema 会报错
- 先在 default 跑通一轮,再做 local 覆盖
- 每次只改一类问题相关字段(例如只改 `project` 的依赖与自动绑定)。
- 覆盖后优先看 `sync_log` 是否出现预期状态迁移。
这属于预期行为。优先修正配置,而不是在文档或调用侧加 fallback
## 7. 持久化与日志字段
## 11. 参考文件
### `persist`
如果你想直接对照真实样例,优先看这些文件:
- `enable`:是否开启持久化。
- `db_path`SQLite 路径。
- `wipe_on_start`:启动是否清理历史持久化。
- `run_profiles/jsonl_to_api.yaml`
- `run_profiles/jsonl_to_jsonl.multi_project.yaml`
- backend 中的 `local_tests/sync_system_tests/depm_to_ecm_bootstrap_pull_then_project_push.two_projects.sqlalchemy.pipeline.yaml`
### `logging`
如果你想看配置模型定义,直接看:
- `file_dir`:日志目录。
- `file_name_pattern`:文件命名模板。
- `level`:日志级别。
- `sync_state_machine/config/pipeline_config.py`
- `sync_state_machine/config/datasource_config.py`
- `sync_state_machine/config/persist_config.py`
- `sync_state_machine/config/multi_project_config.py`
- `sync_state_machine/config/strategy_config.py`
排障建议:运行失败时优先看日志中的 `状态机流转``reason=`,再对照 `docs/state_machine.md``docs/node_state_definition.md` 理解含义。
排障时优先顺序:
1. 看输入 YAML。
2. 看启动日志里的 `Resolved Full Config`
3. 看对应 datasource type 是否真的已经注册。
4. 再看运行日志和状态机结果。