Files
FT546MWS/doc/algorithm-shadow

FT546WMS 现场影子测试操作手册

1. 适用范围

本手册用于分支 test/ft546wms-shadow-from-main-20260811 的现场测试。该分支基于远端稳定版本:

origin/main = 916ac6bda32a5b102a3181cc8c5dbd5319c5656d

影子算法只提出候选结果并记录比较数据。原有 Legacy 存储过程仍执行一次且仅执行一次,Legacy 的库位分配、返回码和响应内容始终是最终结果。

影子模式仍会读取 Legacy 表,并会在 alg schema 中刷新读模型和写入决策日志。因此“只观察”表示不改变 Legacy 业务数据和决策权,不表示完全没有数据库 CPU、I/O 或执行时间开销。

2. 安全边界

  • 默认配置 FORTUCKY_SHADOW_ENABLED=false,未显式开启时不执行影子规划。
  • 启用时只允许 FORTUCKY_ALGORITHM_MODE=SHADOW;误设 ACTIVE 或 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。
  • 影子模式不调用预留、深位锁、理库、回放、移库完成、WCS 完成或 PDA 回调写入过程。
  • alg.reservation 和 alg.deep_workflow_lock 是规划器读取兼容表;影子包不安装修改它们的过程,SHADOW 模式也禁止进入这些写入口。

3. 交付文件

fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql
fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql
fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql

只使用名称带 -shadow 的脚本。该分支已移除原算法通用的 install.sql、diagnostics.sql 和 uninstall.sql,避免现场误装 ACTIVE 数据库对象。

4. 上现场前必须完成的演练

  1. 在现场数据库的最近备份副本上启动远端 origin/main,完成一次入库和一次出库基线测试。
  2. 在备份副本执行 install-shadow.sql,确认脚本全部成功。
  3. 用影子分支完成同样的入库和出库,确认业务结果仍来自 Legacy。
  4. 导出 alg.decision_log,然后停止影子程序并执行 uninstall-shadow.sql。
  5. 再次启动远端 origin/main,重复基线测试。

未完成数据库副本演练,不要直接在现场生产库安装。

5. 准备两个独立 IDEA 工作区

当前开发机的本地 main 已与 origin/main 分叉,不能作为稳定版回滚源。先执行 git fetch origin,确认 origin/main 的提交号,再从远端提交准备只读稳定工作区。

在仓库主目录执行。目标目录必须不存在;如果已存在,先人工确认它的来源,不要覆盖或删除:

git fetch origin
$ExpectedStableCommit = "916ac6bda32a5b102a3181cc8c5dbd5319c5656d"
$RemoteMainCommit = git rev-parse origin/main
if ($RemoteMainCommit -ne $ExpectedStableCommit) { throw "origin/main 已变化,停止部署并重新合并验证" }

$StableWorktree = "D:\workSpace\PROJECT_SPACE\FT546\FT546MWS\worktrees\stable-origin-main-20260811"
if (Test-Path -LiteralPath $StableWorktree) { throw "稳定版 worktree 目标目录已存在,请人工核对" }
git worktree add --detach $StableWorktree origin/main

$CheckedOutCommit = git -C $StableWorktree rev-parse HEAD
if ($CheckedOutCommit -ne $ExpectedStableCommit) { throw "稳定版 worktree 提交校验失败" }
if (git -C $StableWorktree status --porcelain) { throw "稳定版 worktree 不是干净状态" }

两个 IDEA 窗口分别打开:

稳定版:D:\workSpace\PROJECT_SPACE\FT546\FT546MWS\worktrees\stable-origin-main-20260811
影子版:D:\workSpace\PROJECT_SPACE\FT546\FT546MWS\worktrees\shadow-from-main-20260811

为两个窗口分别创建 Run Configuration:

  • FT546WMS-STABLE-origin-main:完全沿用当前稳定现场参数,不增加任何 FORTUCKY_* 环境变量。
  • FT546WMS-SHADOW:复制稳定配置的 JDK、profile、端口、数据库和 Redis 参数,只增加下一节列出的环境变量。

禁止同时运行两个实例。即使使用不同 HTTP 端口,两个 WMS 实例也可能重复执行定时任务、消费任务或发送外部回调。切换时必须先停止当前实例,确认进程退出和端口释放,再启动另一个实例。

6. 安装影子数据库包

6.1 安装前检查

由 DBA 对现场数据库做可恢复的完整备份,并记录备份文件、时间和恢复负责人。随后执行:

SELECT SCHEMA_ID(N'alg') AS alg_schema_id;

SELECT trigger_object.name AS trigger_name,
       schema_name(trigger_object.schema_id) AS trigger_schema,
       schema_name(parent_object.schema_id) AS parent_schema,
       parent_object.name AS parent_table
FROM sys.triggers trigger_object
JOIN sys.objects parent_object ON parent_object.object_id = trigger_object.parent_id
WHERE trigger_object.is_ms_shipped = 0
ORDER BY parent_schema, parent_object.name, trigger_schema, trigger_object.name;

SELECT schema_name(object.schema_id) AS schema_name,
       object.name AS object_name,
       object.type_desc
FROM sys.objects object
WHERE object.is_ms_shipped = 0
ORDER BY schema_name(object.schema_id), object.type_desc, object.name;

预期 alg_schema_id 为 NULL。安装脚本发现任何既有 alg schema 会以错误 51000 拒绝执行。如果现场已经存在 alg schema 或同名对象,停止安装并先确认所有权;不要覆盖,也不要执行本分支卸载脚本。

把两份查询结果连同执行时间保存为安装前基线。现场可能已有 Legacy 自身的触发器,安装后的全库 DML 触发器集合必须与安装前完全一致;用户对象清单只能新增本包定义的 alg.* 对象。

6.2 执行安装

在低业务量时段使用 SQL Server Management Studio 打开并执行:

fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql

脚本使用 GO 分批执行并在当前会话设置 READ_UNCOMMITTED。首次安装会全量建立读模型,可能扫描较多现场数据;如果副本演练时间超出可接受窗口,现场不要继续。

也可以使用已配置 Windows 集成认证的 sqlcmd:

sqlcmd -S "<SQL Server>" -d "<数据库>" -E -b -r1 -i "<仓库>\fortucky-algorithm\src\main\resources\db\sqlserver\install-shadow.sql"

不要把数据库密码写进脚本、IDEA Run Configuration 截图或命令历史。

6.3 安装后检查

重新执行安装前的两份元数据查询。全库 DML 触发器结果必须完全一致,用户对象清单只能新增预期的 alg.* 对象。保存安装后结果和执行时间,再执行:

SELECT schema_name(object.schema_id) AS schema_name,
       object.name,
       object.type_desc
FROM sys.objects object
WHERE schema_name(object.schema_id) = N'alg'
ORDER BY object.type_desc, object.name;

SELECT warehouse_id,
       COUNT(*) AS lane_count,
       MAX(rebuilt_at) AS latest_rebuild_at
FROM alg.lane_state
GROUP BY warehouse_id;

本次新增对象只能位于 alg schema;lane_state 应存在目标仓库的数据。如果安装部分失败,不要启动影子程序,也不要执行本分支卸载脚本:拒绝式卸载会因对象缺失而报错 51014。保留完整输出和安装前后对象清单,由 DBA 决定从备份恢复,或依据清单人工清理本次未完成安装。不要补写 manifest、补建空对象或修改卸载脚本来绕过保护;清理完成且 SCHEMA_ID(N'alg') 再次为 NULL 后,才能重新完整安装。

确认包标记唯一且版本准确:

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 中设置:

FORTUCKY_SHADOW_ENABLED=true
FORTUCKY_ALGORITHM_MODE=SHADOW
FORTUCKY_ALGORITHM_DATABASE_WRITES_ENABLED=true
FORTUCKY_ALGORITHM_WAREHOUSE_ID=01
FORTUCKY_SHADOW_TIMEOUT_SECONDS=3
FORTUCKY_ALGORITHM_LEGACY_FALLBACK_ON_TECHNICAL_ERROR=false

将 01 替换为现场实际仓库号。保持 FORTUCKY_SHADOW_WAREHOUSE_ID 未设置,使影子入口统一回退到 FORTUCKY_ALGORITHM_WAREHOUSE_ID。不要设置 replay、tidy、ACTIVE 或 WCS callback 相关变量。

FORTUCKY_ALGORITHM_DATABASE_WRITES_ENABLED=true 是影子读模型与 alg.decision_log 所必需的应用开关;当前影子代码路径只执行已安装的 alg.* 写入,它不把 Legacy 决策权交给算法。该开关不是数据库权限隔离,安全性还依赖本分支的 SHADOW 启动保护、删减后的 SQL 包和现场变更审计。

切换步骤:

  1. 记录稳定实例停止时间,停止 FT546WMS-STABLE-origin-main。
  2. 确认 Java 进程退出、端口释放,且没有第二个 WMS 实例运行。
  3. 启动 FT546WMS-SHADOW。
  4. 如果出现 Shadow observation requires ...,不要修改保护条件,直接回滚到稳定实例。
  5. 先做只读登录和查询冒烟检查,再由现场人员执行一笔受控入库和一笔受控出库。
  6. 确认 Legacy 返回码、库位和现场设备动作与稳定流程一致。
  7. 确认 alg.decision_log 出现 INBOUND/OUTBOUND 记录,并至少有受控业务出现相应的 *_COMPARE 记录。

影子任务在 Legacy 前提交,但 Legacy 不等待后台线程。线程调度和数据库时序可能导致影子读取与 Legacy 写入重叠,甚至在 Legacy 写入后读取;影子结果不是严格的“Legacy 前快照”,分析时必须结合时间和业务结果解释。

单线程零队列执行器忙时,被跳过的请求不会调用算法,也没有对应的计划/比较决策日志;如果规划或 Legacy 超过观察期限,也可能只有计划日志而没有比较日志。Legacy 存储过程仍按原流程立即执行。这是保护业务延迟和数据库负载的预期行为,不要据此重放业务请求。

8. 观察与停止条件

应用日志中的以下文本表示后台影子已失败开放,Legacy 仍继续执行:

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 节回滚:

  • Legacy 入库或出库结果、返回码、消息或设备动作与稳定版不同;
  • 正常接口达到预先约定的延迟阈值,或同类 3 秒超时告警重复出现;
  • SQL Server CPU、I/O、锁等待或连接数出现不可接受增长;
  • alg 之外出现任何由本次测试新增的表、过程、触发器或数据修改;
  • 同一 Legacy 存储过程疑似被重复调用;
  • WMS、WCS、PDA 或现场操作出现异常。

实际进入影子规划的请求每次都会按仓库全量刷新读模型。应用在单个 JVM 内将观察周期上限限制为 1、队列容量为 0,其他并发观察立即跳过;这能限制重叠刷新,但 READ_UNCOMMITTED 和单并发都无法消除全表扫描带来的 CPU 和 I/O,所以必须从少量受控业务开始,不能直接长时间全量开放。若部署多个影子 JVM,该上限分别生效,现场仍只允许启动一个 WMS 实例。

9. 数据采集与交付

测试开始和结束时各执行一次:

fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql

在 SSMS 中选择“结果输出到文件”,保留完整结果。该脚本只读,包含对象清单、读模型统计、Legacy 表行数/校验和、当前锁和决策日志。Legacy 表校验和会扫描数据,只在低业务量时执行,不要高频轮询。

测试结束、卸载之前单独导出原始决策记录:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT decision_id, warehouse_id, decision_type, input_version,
       result_code, reason, created_at
FROM alg.decision_log
WHERE created_at >= '<测试开始时间>'
  AND created_at <  '<测试结束时间>'
ORDER BY created_at, decision_id;

交付分析的数据包至少包括:

  • 决策日志导出文件,保持原始列名和时间字段;
  • diagnostics-shadow.sql 开始和结束两份结果;
  • 应用日志中测试时间段的 Shadow 告警及其前后异常栈;
  • 测试开始/结束时间、时区、仓库号、入库次数、自动出库次数、手动出库次数;
  • 现场人员确认的 Legacy 最终库位、返回码及异常说明;
  • 当前分支提交号和构建 JAR 的 SHA-256。

不要提供数据库密码、访问令牌或与分析无关的个人信息。

10. 快速回滚

10.1 首选:切回远端稳定源码

  1. 停止 FT546WMS-SHADOW,记录停止时间。
  2. 确认进程退出和端口释放。
  3. 在独立稳定工作区启动 FT546WMS-STABLE-origin-main。
  4. 执行登录、查询、受控入库和受控出库冒烟检查。

稳定版不引用 alg.*,因此应用回滚时无需立即卸载数据库对象。先恢复生产服务,再在维护窗口导出数据并卸载。

10.2 备用:仅关闭影子开关

如果稳定工作区暂时无法启动,可在影子 Run Configuration 中设置:

FORTUCKY_SHADOW_ENABLED=false

然后完整重启应用。该方式保留影子分支代码,但所有影子入口会直接返回空观察结果。它不替代稳定版回滚,只用于临时恢复。

11. 完整卸载

仅在影子应用已停止、alg.decision_log 已导出且稳定版已恢复后执行。

使用 SSMS 打开并执行:

fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql

卸载脚本先校验包标记必须精确等于 FT546WMS_SHADOW_ONLY / 2026-08-12-1,再校验 alg 顶层对象的名称和类型必须与本包白名单完全一致。发现未知对象、缺失对象、类型不符或标记不符时会在第一个 DROP 前拒绝执行,不删除任何对象。校验通过后才在事务内删除本分支拥有的 alg 过程、视图、表和 schema;失败会回滚,不修改 Legacy 业务行。执行后验证:

SELECT SCHEMA_ID(N'alg') AS alg_schema_id;

预期结果为 NULL。再次查询全库 DML 触发器集合,结果应与安装前保存的基线一致。

不要在影子应用运行时卸载。如果脚本报错 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 内单线程零队列执行器只避免同一实例的重叠刷新,不限制其他进程或数据库会话;禁止同时运行稳定版和影子版,也禁止运行多个影子实例。
  • 应用级 fail-open 能保护 Legacy 控制流,但不能消除数据库、网络或主机资源争用。

这些残余风险是采用“零 Legacy 触发器、每次规划前全量刷新”的代价,也是现场必须先小流量验证、随时可切回稳定源码的原因。