feat: 新增 G4 热表独立双写能力
- 新增配置项以支持旧/新明细表的独立写入开关及目标表名。 - 重构 DatabaseManager,抽象通用批量 COPY 写入内核,支持不同目标表的复用。 - 新增双明细写入编排器,支持旧/新表独立执行、重试及 fallback。 - 调整 HeartbeatProcessor.processBatch(),确保 room_status 独立执行。 - 错误表仅记录新表写入失败,旧表失败不再写入错误表。 - 重新定义消费暂停策略,基于当前启用的关键 sink 判断。 - 补充按 sink 维度的统计项与启动日志。 新增 G4 热表相关的数据库规范与处理逻辑,确保系统在双写模式下的稳定性与可扩展性。
This commit is contained in:
419
.github/prompts/plan-finalExecutableIndependentDualWriteNewHotTable.prompt.md
vendored
Normal file
419
.github/prompts/plan-finalExecutableIndependentDualWriteNewHotTable.prompt.md
vendored
Normal file
@@ -0,0 +1,419 @@
|
||||
## 最终可执行的实施清单
|
||||
|
||||
以下清单基于你已经确认的最终约束整理,可直接作为实施顺序使用。
|
||||
|
||||
---
|
||||
|
||||
## 一、实施目标
|
||||
|
||||
在**不改变数据来源**、**不改变现有批次处理节奏**、**不改变 `room_status` 写法与目标** 的前提下,实现:
|
||||
|
||||
- 旧明细表 `heartbeat.heartbeat_events` 可独立开关
|
||||
- 新明细表 `heartbeat.heartbeat_events_g4_hot` 可独立开关
|
||||
- 新明细表继续使用**批量 `COPY`**
|
||||
- 旧/新明细写入**完全独立**
|
||||
- `room_status` 始终独立执行,不受明细写入开关影响
|
||||
- 错误表 `heartbeat.heartbeat_events_errors` 只保留一份,**仅记录新表写入失败**
|
||||
- 支持未来**关闭旧表,仅保留新表**
|
||||
|
||||
---
|
||||
|
||||
## 二、最终行为规则
|
||||
|
||||
### 1. 明细写入规则
|
||||
- `legacyHeartbeatEnabled=true` 时,写旧表 `heartbeat.heartbeat_events`
|
||||
- `g4HotHeartbeatEnabled=true` 时,写新表 `heartbeat.heartbeat_events_g4_hot`
|
||||
- 两者可同时开启,也可单独开启,也可同时关闭
|
||||
|
||||
### 2. `room_status` 规则
|
||||
- 始终执行现有 `upsertRoomStatus()`
|
||||
- 目标表不变
|
||||
- 写法不变
|
||||
- 不依赖旧明细是否开启
|
||||
- 不依赖新明细是否开启
|
||||
- 即使旧/新明细都关闭,仍然写 `room_status`
|
||||
|
||||
### 3. 错误表规则
|
||||
- 继续使用现有 `heartbeat.heartbeat_events_errors`
|
||||
- 字段完全不变
|
||||
- 只记录**新表写入失败**
|
||||
- 旧表写入失败**不再写错误表**,只记录日志/统计
|
||||
|
||||
### 4. 消费暂停规则
|
||||
- 不能因为“两路明细都关闭”而暂停消费
|
||||
- 不能因为“新表表级错误”直接当成全局数据库离线
|
||||
- 只有在**当前启用且关键的写入路径**发生连接级不可恢复故障时,才触发暂停消费
|
||||
|
||||
---
|
||||
|
||||
## 三、代码改造清单
|
||||
|
||||
### Task 1:扩展配置项
|
||||
**目标文件**
|
||||
- `src/config/config.js`
|
||||
- `src/config/config.example.js`
|
||||
- `README.md` 或部署文档
|
||||
|
||||
**新增配置建议**
|
||||
- `DB_LEGACY_HEARTBEAT_ENABLED`
|
||||
- `DB_G4_HOT_HEARTBEAT_ENABLED`
|
||||
- `DB_ROOM_STATUS_ENABLED`
|
||||
- `DB_G4_HOT_TABLE`
|
||||
- `DB_LEGACY_TABLE`
|
||||
|
||||
**建议默认值**
|
||||
- `DB_LEGACY_HEARTBEAT_ENABLED=true`
|
||||
- `DB_G4_HOT_HEARTBEAT_ENABLED=false`
|
||||
- `DB_ROOM_STATUS_ENABLED=true`
|
||||
- `DB_LEGACY_TABLE=heartbeat.heartbeat_events`
|
||||
- `DB_G4_HOT_TABLE=heartbeat.heartbeat_events_g4_hot`
|
||||
|
||||
**实施要求**
|
||||
- 保持与当前 `config.js` 风格一致
|
||||
- 布尔值走现有 `parseBoolean`
|
||||
- 表名配置单独放在 `db` 配置节点下
|
||||
|
||||
**完成标准**
|
||||
- 应用启动时可从环境变量读取旧/新明细开关
|
||||
- `room_status` 开关独立可控
|
||||
- 新旧目标表名可配置
|
||||
|
||||
---
|
||||
|
||||
### Task 2:抽象通用明细写入内核
|
||||
**目标文件**
|
||||
- `src/db/databaseManager.js`
|
||||
|
||||
**当前基础**
|
||||
- 现有 `insertHeartbeatEvents()` 已实现:
|
||||
- 分区预创建
|
||||
- `COPY ... FROM STDIN`
|
||||
- 缺分区补建重试
|
||||
- fallback 逐条 `INSERT`
|
||||
|
||||
**要做的事**
|
||||
把现有写旧表的强绑定逻辑抽象为通用方法,例如:
|
||||
- `_insertHeartbeatEventsToTarget(events, targetConfig)`
|
||||
- `_buildHeartbeatCopySql(targetTable, columns)`
|
||||
- `_buildHeartbeatInsertSql(targetTable, columns)`
|
||||
|
||||
**目标参数至少包含**
|
||||
- `tableName`
|
||||
- `columns`
|
||||
- `logPrefix`
|
||||
- `enablePartitionEnsure`
|
||||
- 缺分区识别规则
|
||||
|
||||
**实施要求**
|
||||
- 不能复制两份几乎相同的 `COPY` 代码
|
||||
- 保证旧表和新表复用同一套批量写入能力
|
||||
- 保证新表也走 `COPY`
|
||||
|
||||
**完成标准**
|
||||
- 写旧表和写新表都可通过同一个通用写入内核完成
|
||||
- 旧逻辑行为不变
|
||||
- 新表能力复用旧逻辑
|
||||
|
||||
---
|
||||
|
||||
### Task 3:实现双明细独立编排
|
||||
**目标文件**
|
||||
- `src/db/databaseManager.js`
|
||||
|
||||
**新增编排方法建议**
|
||||
- `writeHeartbeatDetails(events)`
|
||||
- 或 `insertHeartbeatEventsDual(events)`
|
||||
|
||||
**它需要负责**
|
||||
- 判断旧表是否开启
|
||||
- 判断新表是否开启
|
||||
- 分别调用:
|
||||
- `legacy writer`
|
||||
- `g4Hot writer`
|
||||
- 聚合结果并返回结构化结果
|
||||
|
||||
**返回结果建议**
|
||||
至少包含:
|
||||
- `legacy.enabled`
|
||||
- `legacy.success`
|
||||
- `legacy.insertedCount`
|
||||
- `legacy.failedRecords`
|
||||
- `legacy.error`
|
||||
- `g4Hot.enabled`
|
||||
- `g4Hot.success`
|
||||
- `g4Hot.insertedCount`
|
||||
- `g4Hot.failedRecords`
|
||||
- `g4Hot.error`
|
||||
|
||||
**实施要求**
|
||||
- 两路执行逻辑互不影响
|
||||
- 一路失败不能吞掉另一路结果
|
||||
- 两路都关闭时返回“跳过写入”的明确结果,不报错
|
||||
|
||||
**完成标准**
|
||||
- 系统能正确识别四种模式:
|
||||
- 仅旧
|
||||
- 仅新
|
||||
- 双写
|
||||
- 双关
|
||||
|
||||
---
|
||||
|
||||
### Task 4:调整 `HeartbeatProcessor.processBatch()` 的主流程
|
||||
**目标文件**
|
||||
- `src/processor/heartbeatProcessor.js`
|
||||
|
||||
**当前逻辑问题**
|
||||
当前流程中:
|
||||
- 先写 `insertHeartbeatEvents(batchData)`
|
||||
- 只有成功后才 best-effort 写 `room_status`
|
||||
|
||||
这与当前最终要求不一致。
|
||||
|
||||
**改造目标**
|
||||
把流程改成三段独立逻辑:
|
||||
|
||||
#### A. 明细写入
|
||||
调用新的双写编排方法,获取旧/新明细写入结果
|
||||
|
||||
#### B. `room_status` 写入
|
||||
无论旧/新明细写入开关状态如何,都执行:
|
||||
- `upsertRoomStatus(batchData)`
|
||||
|
||||
注意:
|
||||
- 只要 `DB_ROOM_STATUS_ENABLED=true`
|
||||
- 就执行当前逻辑
|
||||
- 不依赖明细成功与否
|
||||
|
||||
#### C. 错误表写入
|
||||
仅把**新表失败记录**送入 `insertHeartbeatEventsErrors()`
|
||||
|
||||
**完成标准**
|
||||
- `room_status` 从“依赖旧表成功”变成“独立执行”
|
||||
- 错误表只接新表失败
|
||||
- 旧表失败不进错误表
|
||||
|
||||
---
|
||||
|
||||
### Task 5:重新定义错误表调用策略
|
||||
**目标文件**
|
||||
- `src/db/databaseManager.js`
|
||||
- `src/processor/heartbeatProcessor.js`
|
||||
|
||||
**要求**
|
||||
- 保留 `insertHeartbeatEventsErrors()` 表结构和 SQL
|
||||
- 仅调整调用来源:
|
||||
- 新表失败 → 写错误表
|
||||
- 旧表失败 → 不写错误表
|
||||
|
||||
**实施要求**
|
||||
建议在双写结果聚合后,只提取:
|
||||
- `g4Hot.failedRecords`
|
||||
|
||||
转换成当前错误表 payload 后调用 `insertHeartbeatEventsErrors()`
|
||||
|
||||
**完成标准**
|
||||
- 错误表字段、表结构、SQL 全不变
|
||||
- 旧表失败不会污染错误表
|
||||
- 新表失败可追踪
|
||||
|
||||
---
|
||||
|
||||
### Task 6:调整连接状态与暂停消费策略
|
||||
**目标文件**
|
||||
- `src/processor/heartbeatProcessor.js`
|
||||
- `src/db/databaseManager.js`
|
||||
|
||||
**当前问题**
|
||||
目前 `_isConnectionError()` 和 `_scheduleDbCheck()` 基本围绕单一 DB 写入路径设计。
|
||||
|
||||
**改造要求**
|
||||
把“是否需要暂停 Kafka 消费”改成基于**启用中的关键 sink**判断。
|
||||
|
||||
**建议规则**
|
||||
- 仅旧表开启:旧表连接故障才可能触发暂停
|
||||
- 仅新表开启:新表连接故障才可能触发暂停
|
||||
- 双开:如果两路都因连接级错误不可写,可触发暂停
|
||||
- 双关但 `room_status` 开启:只要 `room_status` 还能正常写,就不应因明细关闭而暂停
|
||||
|
||||
**重要区分**
|
||||
以下不能都算成“数据库离线”:
|
||||
- 连接失败
|
||||
- 缺分区
|
||||
- 表不存在
|
||||
- 表字段不匹配
|
||||
- 权限不足
|
||||
|
||||
其中真正应触发离线处理的优先是:
|
||||
- `08006`
|
||||
- `08001`
|
||||
- `08003`
|
||||
- `08004`
|
||||
- `08007`
|
||||
- `57P03`
|
||||
- 以及明显的网络连接错误
|
||||
|
||||
**完成标准**
|
||||
- 不会因为新表单独异常而误停整个消费
|
||||
- 不会因为两路明细关闭而误停消费
|
||||
|
||||
---
|
||||
|
||||
### Task 7:补充启动日志与配置摘要
|
||||
**目标文件**
|
||||
- `src/index.js`
|
||||
|
||||
**新增日志建议**
|
||||
启动时输出:
|
||||
- 旧明细写入是否开启
|
||||
- 新明细写入是否开启
|
||||
- `room_status` 是否开启
|
||||
- 旧表目标
|
||||
- 新表目标
|
||||
|
||||
**完成标准**
|
||||
- 运行日志中能直接看出当前处于哪种模式
|
||||
- 运维排障时不用翻配置文件
|
||||
|
||||
---
|
||||
|
||||
### Task 8:补充统计项
|
||||
**目标文件**
|
||||
- `src/stats/statsManager.js`
|
||||
- 如有 Redis 控制台输出,也同步补充
|
||||
|
||||
**新增统计建议**
|
||||
- legacy detail success count
|
||||
- legacy detail failed count
|
||||
- g4Hot detail success count
|
||||
- g4Hot detail failed count
|
||||
- room_status success count
|
||||
- room_status failed count
|
||||
- g4Hot error-table inserted count
|
||||
|
||||
**完成标准**
|
||||
- 双写观察期可以快速对比旧表/新表健康状态
|
||||
- 能单独看出新表失败是否升高
|
||||
|
||||
---
|
||||
|
||||
## 四、测试清单
|
||||
|
||||
### Task 9:补充单元测试
|
||||
**目标文件**
|
||||
- `test/smoke.test.js`
|
||||
- 如有必要新增专门的 `databaseManager` 测试文件
|
||||
|
||||
**必须覆盖的场景**
|
||||
1. 仅旧表开启
|
||||
2. 仅新表开启
|
||||
3. 旧新双开
|
||||
4. 旧新双关,但 `room_status` 开启
|
||||
5. 旧成功、新失败
|
||||
6. 旧失败、新成功
|
||||
7. 双失败
|
||||
8. 新表失败时写错误表
|
||||
9. 旧表失败时不写错误表
|
||||
10. `room_status` 始终执行
|
||||
11. `COPY` 失败时新表降级逐条 `INSERT`
|
||||
12. 缺分区时自动补分区重试
|
||||
|
||||
**完成标准**
|
||||
- `npm test` 可覆盖新旧双写与独立逻辑
|
||||
- `room_status` 独立性有明确断言
|
||||
|
||||
---
|
||||
|
||||
### Task 10:补充数据库 smoke 验证
|
||||
**目标文件**
|
||||
- `scripts/db/smokeTest.js`
|
||||
- 或新增 `scripts/db/smokeG4Hot.js`
|
||||
|
||||
**验证项**
|
||||
- 新表 `heartbeat.heartbeat_events_g4_hot` 能写入
|
||||
- 新表分区能正确附着
|
||||
- 新表唯一键/索引存在
|
||||
- 新表可按主查询维度检索
|
||||
- 旧表关闭、新表开启时仍正常
|
||||
- 旧新都关但 `room_status` 开启时系统不报配置错误
|
||||
|
||||
**完成标准**
|
||||
- 有一套上线前可重复执行的 smoke 脚本
|
||||
- 可快速确认新表接入正确
|
||||
|
||||
---
|
||||
|
||||
## 五、上线实施顺序
|
||||
|
||||
### Step 1:代码部署,保持现状
|
||||
配置:
|
||||
- 旧表开启
|
||||
- 新表关闭
|
||||
- `room_status` 开启
|
||||
|
||||
目标:
|
||||
- 确认改造后代码在“旧模式”下行为不变
|
||||
|
||||
### Step 2:开启新表双写
|
||||
配置:
|
||||
- 旧表开启
|
||||
- 新表开启
|
||||
- `room_status` 开启
|
||||
|
||||
目标:
|
||||
- 观察新表写入是否稳定
|
||||
- 检查新表失败是否进入错误表
|
||||
- 比对旧/新明细数据量
|
||||
|
||||
### Step 3:观察稳定性
|
||||
重点观察:
|
||||
- 新表 `COPY` 成功率
|
||||
- 新表 fallback 频率
|
||||
- 错误表记录量
|
||||
- `room_status` 是否持续正常
|
||||
|
||||
### Step 4:关闭旧表
|
||||
配置:
|
||||
- 旧表关闭
|
||||
- 新表开启
|
||||
- `room_status` 开启
|
||||
|
||||
目标:
|
||||
- 验证“仅保留新表”时系统稳定运行
|
||||
|
||||
### Step 5:保留过渡观察期
|
||||
目标:
|
||||
- 持续观察新表、错误表、`room_status`
|
||||
|
||||
---
|
||||
|
||||
## 六、验收标准
|
||||
|
||||
满足以下全部条件即可验收:
|
||||
|
||||
1. 新表 `heartbeat.heartbeat_events_g4_hot` 接入成功
|
||||
2. 新表仍使用批量 `COPY`
|
||||
3. 旧/新明细可独立开关
|
||||
4. `room_status` 始终独立执行
|
||||
5. 两路明细都关闭时,系统仍允许继续写 `room_status`
|
||||
6. 错误表仅记录新表失败
|
||||
7. 旧表失败不会写错误表
|
||||
8. 不修改 Kafka 数据来源
|
||||
9. 不修改现有数据转换来源
|
||||
10. 可支持后续关闭旧表,仅保留新表
|
||||
|
||||
---
|
||||
|
||||
## 七、建议的实施顺序编号版
|
||||
|
||||
1. 扩展 `config.js` 配置项
|
||||
2. 重构 `databaseManager.js`,抽通用 `COPY` writer
|
||||
3. 在 `databaseManager.js` 增加旧/新明细双写编排
|
||||
4. 调整 `heartbeatProcessor.js`,让 `room_status` 独立执行
|
||||
5. 调整错误表调用,仅接新表失败
|
||||
6. 重构连接异常与暂停消费判定
|
||||
7. 增加启动配置摘要日志
|
||||
8. 增加 stats 指标
|
||||
9. 补测试
|
||||
10. 补 smoke 验证脚本
|
||||
11. 按“旧开新关 → 双开 → 关旧留新”上线
|
||||
Reference in New Issue
Block a user