fix: verify shadow SQL package ownership

This commit is contained in:
zgw
2026-08-12 09:26:18 +08:00
parent bb3b93a79f
commit 548359d07f
5 changed files with 153 additions and 39 deletions

View File

@@ -16,8 +16,9 @@ origin/main = 916ac6bda32a5b102a3181cc8c5dbd5319c5656d
- 默认配置 `FORTUCKY_SHADOW_ENABLED=false`,未显式开启时不执行影子规划。
- 启用时只允许 `FORTUCKY_ALGORITHM_MODE=SHADOW`;误设 `ACTIVE` 或 `LEGACY` 会导致应用拒绝启动。
- 影子事务使用 `REQUIRES_NEW`、`READ_UNCOMMITTED` 和 3 秒超时。
- 单个 JVM 同时最多执行一个影子规划;并发到达的其他观察会立即跳过,不排队、不等待,也不延迟 Legacy 分配。
- 影子事务使用 `REQUIRES_NEW`、`READ_UNCOMMITTED` 和 3 秒超时,并且只在名为 `ft546-shadow-observer` 的 daemon 工作线程执行。
- 专用执行器只有一个线程、队列容量为 0,拒绝策略不会回落到 Legacy 请求线程;忙时其他观察立即跳过。
- Legacy 线程仅提交任务和通知内存结果,不等待影子规划或比较,也不执行任何影子数据库操作。
- 影子异常、超时、输入缺失和比较日志写入失败都会 fail-open,不跳过 Legacy 流程。
- 安装包不在 `dbo.bas_cargo_space`、`dbo.bas_stock`、`dbo.mydo_job` 或其他 Legacy 表上创建触发器。
- 安装包不对 Legacy 业务表执行 `INSERT`、`UPDATE`、`DELETE` 或 `MERGE`。
@@ -105,7 +106,7 @@ WHERE object.is_ms_shipped = 0
ORDER BY schema_name(object.schema_id), object.type_desc, object.name;
```
预期 `alg_schema_id` 为 `NULL`。如果现场已经存在 `alg` schema 或同名对象,停止安装并先确认所有权;不要覆盖,也不要执行本分支卸载脚本。
预期 `alg_schema_id` 为 `NULL`。安装脚本发现任何既有 `alg` schema 会以错误 51000 拒绝执行。如果现场已经存在 `alg` schema 或同名对象,停止安装并先确认所有权;不要覆盖,也不要执行本分支卸载脚本。
把两份查询结果连同执行时间保存为安装前基线。现场可能已有 Legacy 自身的触发器,安装后的全库 DML 触发器集合必须与安装前完全一致;用户对象清单只能新增本包定义的 `alg.*` 对象。
@@ -148,6 +149,15 @@ GROUP BY warehouse_id;
本次新增对象只能位于 `alg` schema;`lane_state` 应存在目标仓库的数据。如果安装部分失败,不要启动影子程序,也不要自动执行卸载脚本。保留完整输出和安装前后对象清单,由 DBA 决定从备份恢复,或在确认安装前 `alg` 不存在且失败期间无人创建其他 `alg` 对象后,才执行本分支卸载脚本。
确认包标记唯一且版本准确:
```sql
SELECT package_id, package_version, installed_at
FROM alg.shadow_package_manifest;
```
必须只有一行:`FT546WMS_SHADOW_ONLY` / `2026-08-12-1`。不一致时不要启动影子程序,也不要手工修改标记。
## 7. IDEA 启用影子模式
在 `FT546WMS-SHADOW` 的 Environment variables 中设置:
@@ -173,20 +183,24 @@ FORTUCKY_ALGORITHM_LEGACY_FALLBACK_ON_TECHNICAL_ERROR=false
4. 如果出现 `Shadow observation requires ...`,不要修改保护条件,直接回滚到稳定实例。
5. 先做只读登录和查询冒烟检查,再由现场人员执行一笔受控入库和一笔受控出库。
6. 确认 Legacy 返回码、库位和现场设备动作与稳定流程一致。
7. 确认 `alg.decision_log` 出现 `INBOUND`/`OUTBOUND` 及相应的 `*_COMPARE` 记录。
7. 确认 `alg.decision_log` 出现 `INBOUND`/`OUTBOUND` 记录,并至少有受控业务出现相应的 `*_COMPARE` 记录。
并发业务触发单 JVM 规划门时,被跳过的请求不会调用算法,也可能没有对应的计划/比较决策日志;Legacy 存储过程仍按原流程立即执行。这是保护现场数据库负载的预期行为,不要据此重放业务请求。
影子任务在 Legacy 前提交,但 Legacy 不等待后台线程。线程调度和数据库时序可能导致影子读取与 Legacy 写入重叠,甚至在 Legacy 写入后读取;影子结果不是严格的“Legacy 前快照”,分析时必须结合时间和业务结果解释。
单线程零队列执行器忙时,被跳过的请求不会调用算法,也没有对应的计划/比较决策日志;如果规划或 Legacy 超过观察期限,也可能只有计划日志而没有比较日志。Legacy 存储过程仍按原流程立即执行。这是保护业务延迟和数据库负载的预期行为,不要据此重放业务请求。
## 8. 观察与停止条件
应用日志中的以下文本表示影子已失败开放,Legacy 仍继续执行:
应用日志中的以下文本表示后台影子已失败开放,Legacy 仍继续执行:
```text
Shadow inbound planning failed open
Shadow outbound planning failed open
Shadow exact-stock planning failed open
Shadow inbound comparison failed open
Shadow outbound comparison failed open
Shadow inbound observation failed open
Shadow outbound observation failed open
Shadow inbound observation interrupted
Shadow outbound observation interrupted
Inbound shadow submission failed open
Project outbound shadow submission failed open
Exact-stock shadow submission failed open
```
出现单条告警不等于 Legacy 失败,但应记录时间和业务单号。测试前由现场负责人和 DBA 根据稳定时段基线写下响应延迟、SQL CPU/I/O、锁等待、连接数和 Shadow 告警频率阈值;未约定阈值时,只允许少量受控业务,不进入连续观察。满足任一条件立即停止影子测试并执行第 10 节回滚:
@@ -198,7 +212,7 @@ Shadow outbound comparison failed open
- 同一 Legacy 存储过程疑似被重复调用;
- WMS、WCS、PDA 或现场操作出现异常。
实际进入影子规划的请求每次都会按仓库全量刷新读模型。应用在单个 JVM 内将并发规划上限限制为 1,其他并发观察立即跳过;这能限制重叠刷新,但 `READ_UNCOMMITTED` 和单并发都无法消除全表扫描带来的 CPU 和 I/O,所以必须从少量受控业务开始,不能直接长时间全量开放。若部署多个影子 JVM,该上限分别生效,现场仍只允许启动一个 WMS 实例。
实际进入影子规划的请求每次都会按仓库全量刷新读模型。应用在单个 JVM 内将观察周期上限限制为 1、队列容量为 0,其他并发观察立即跳过;这能限制重叠刷新,但 `READ_UNCOMMITTED` 和单并发都无法消除全表扫描带来的 CPU 和 I/O,所以必须从少量受控业务开始,不能直接长时间全量开放。若部署多个影子 JVM,该上限分别生效,现场仍只允许启动一个 WMS 实例。
## 9. 数据采集与交付
@@ -265,7 +279,7 @@ FORTUCKY_SHADOW_ENABLED=false
fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql
```
卸载脚本只删除本分支拥有的 `alg` 过程、视图、表和 schema,不修改 Legacy 业务行。执行后验证:
卸载脚本先校验包标记必须精确等于 `FT546WMS_SHADOW_ONLY` / `2026-08-12-1`,再校验 `alg` 顶层对象的名称和类型必须与本包白名单完全一致。发现未知对象、缺失对象、类型不符或标记不符时会在第一个 `DROP` 前拒绝执行,不删除任何对象。校验通过后才在事务内删除本分支拥有的 `alg` 过程、视图、表和 schema;失败会回滚,不修改 Legacy 业务行。执行后验证:
```sql
SELECT SCHEMA_ID(N'alg') AS alg_schema_id;
@@ -273,15 +287,16 @@ SELECT SCHEMA_ID(N'alg') AS alg_schema_id;
预期结果为 `NULL`。再次查询全库 DML 触发器集合,结果应与安装前保存的基线一致。
不要在影子应用运行时卸载。不要对包含未知对象的既有 `alg` schema 执行本卸载脚本。卸载后如需再次测试,必须重新执行完整安装和检查流程。
不要在影子应用运行时卸载。如果脚本报错 51011-51014,保存错误和对象清单,停止手工删除,由 DBA 判断恢复或清理方案。不要修改 manifest 来绕过保护。卸载后如需再次测试,必须重新执行完整安装和检查流程。
## 12. 已知残余风险
- 当前自动化验证是 Java 单元测试和 SQL 静态审计,不能替代真实 SQL Server 2016 副本演练。
- `READ_UNCOMMITTED` 可能读到业务事务的中间状态;该结果只用于影子比较,不参与 Legacy 决策。
- 零等待异步提交不保证严格的 Legacy 前置快照;影子读取可能与 Legacy 写入重叠或发生在其后。
- 3 秒事务超时会触发 fail-open,但 JDBC 取消和 SQL Server 回滚完成时间不保证严格等于 3 秒。
- 每次影子规划前的全量刷新可能增加数据库 CPU 和 I/O;现场必须监控并设置短测试窗口。
- JVM 内单并发门只避免同一实例的重叠刷新,不限制其他进程或数据库会话;禁止同时运行稳定版和影子版,也禁止运行多个影子实例。
- JVM 内单线程零队列执行器只避免同一实例的重叠刷新,不限制其他进程或数据库会话;禁止同时运行稳定版和影子版,也禁止运行多个影子实例。
- 应用级 fail-open 能保护 Legacy 控制流,但不能消除数据库、网络或主机资源争用。
这些残余风险是采用“零 Legacy 触发器、每次规划前全量刷新”的代价,也是现场必须先小流量验证、随时可切回稳定源码的原因。