docs: add IDEA shadow deployment and rollback guide
This commit is contained in:
283
doc/algorithm-shadow/README.md
Normal file
283
doc/algorithm-shadow/README.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# FT546WMS 现场影子测试操作手册
|
||||
|
||||
## 1. 适用范围
|
||||
|
||||
本手册用于分支 `test/ft546wms-shadow-from-main-20260811` 的现场测试。该分支基于远端稳定版本:
|
||||
|
||||
```text
|
||||
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 秒超时。
|
||||
- 影子异常、超时、输入缺失和比较日志写入失败都会 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. 交付文件
|
||||
|
||||
```text
|
||||
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` 的提交号,再从远端提交准备只读稳定工作区。
|
||||
|
||||
在仓库主目录执行。目标目录必须不存在;如果已存在,先人工确认它的来源,不要覆盖或删除:
|
||||
|
||||
```powershell
|
||||
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 窗口分别打开:
|
||||
|
||||
```text
|
||||
稳定版: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 对现场数据库做可恢复的完整备份,并记录备份文件、时间和恢复负责人。随后执行:
|
||||
|
||||
```sql
|
||||
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 或同名对象,停止安装并先确认所有权;不要覆盖,也不要执行本分支卸载脚本。
|
||||
|
||||
把两份查询结果连同执行时间保存为安装前基线。现场可能已有 Legacy 自身的触发器,安装后的全库 DML 触发器集合必须与安装前完全一致;用户对象清单只能新增本包定义的 `alg.*` 对象。
|
||||
|
||||
### 6.2 执行安装
|
||||
|
||||
在低业务量时段使用 SQL Server Management Studio 打开并执行:
|
||||
|
||||
```text
|
||||
fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql
|
||||
```
|
||||
|
||||
脚本使用 `GO` 分批执行并在当前会话设置 `READ_UNCOMMITTED`。首次安装会全量建立读模型,可能扫描较多现场数据;如果副本演练时间超出可接受窗口,现场不要继续。
|
||||
|
||||
也可以使用已配置 Windows 集成认证的 `sqlcmd`:
|
||||
|
||||
```powershell
|
||||
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.*` 对象。保存安装后结果和执行时间,再执行:
|
||||
|
||||
```sql
|
||||
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` 应存在目标仓库的数据。如果安装部分失败,不要启动影子程序,也不要自动执行卸载脚本。保留完整输出和安装前后对象清单,由 DBA 决定从备份恢复,或在确认安装前 `alg` 不存在且失败期间无人创建其他 `alg` 对象后,才执行本分支卸载脚本。
|
||||
|
||||
## 7. IDEA 启用影子模式
|
||||
|
||||
在 `FT546WMS-SHADOW` 的 Environment variables 中设置:
|
||||
|
||||
```text
|
||||
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` 记录。
|
||||
|
||||
## 8. 观察与停止条件
|
||||
|
||||
应用日志中的以下文本表示影子已失败开放,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
|
||||
```
|
||||
|
||||
出现单条告警不等于 Legacy 失败,但应记录时间和业务单号。测试前由现场负责人和 DBA 根据稳定时段基线写下响应延迟、SQL CPU/I/O、锁等待、连接数和 Shadow 告警频率阈值;未约定阈值时,只允许少量受控业务,不进入连续观察。满足任一条件立即停止影子测试并执行第 10 节回滚:
|
||||
|
||||
- Legacy 入库或出库结果、返回码、消息或设备动作与稳定版不同;
|
||||
- 正常接口达到预先约定的延迟阈值,或同类 3 秒超时告警重复出现;
|
||||
- SQL Server CPU、I/O、锁等待或连接数出现不可接受增长;
|
||||
- `alg` 之外出现任何由本次测试新增的表、过程、触发器或数据修改;
|
||||
- 同一 Legacy 存储过程疑似被重复调用;
|
||||
- WMS、WCS、PDA 或现场操作出现异常。
|
||||
|
||||
影子规划每次都会按仓库全量刷新读模型。`READ_UNCOMMITTED` 降低了对 Legacy 写事务的锁干扰,但无法消除全表扫描带来的 CPU 和 I/O,所以必须从少量受控业务开始,不能直接长时间全量开放。
|
||||
|
||||
## 9. 数据采集与交付
|
||||
|
||||
测试开始和结束时各执行一次:
|
||||
|
||||
```text
|
||||
fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql
|
||||
```
|
||||
|
||||
在 SSMS 中选择“结果输出到文件”,保留完整结果。该脚本只读,包含对象清单、读模型统计、Legacy 表行数/校验和、当前锁和决策日志。Legacy 表校验和会扫描数据,只在低业务量时执行,不要高频轮询。
|
||||
|
||||
测试结束、卸载之前单独导出原始决策记录:
|
||||
|
||||
```sql
|
||||
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 中设置:
|
||||
|
||||
```text
|
||||
FORTUCKY_SHADOW_ENABLED=false
|
||||
```
|
||||
|
||||
然后完整重启应用。该方式保留影子分支代码,但所有影子入口会直接返回空观察结果。它不替代稳定版回滚,只用于临时恢复。
|
||||
|
||||
## 11. 完整卸载
|
||||
|
||||
仅在影子应用已停止、`alg.decision_log` 已导出且稳定版已恢复后执行。
|
||||
|
||||
使用 SSMS 打开并执行:
|
||||
|
||||
```text
|
||||
fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql
|
||||
```
|
||||
|
||||
卸载脚本只删除本分支拥有的 `alg` 过程、视图、表和 schema,不修改 Legacy 业务行。执行后验证:
|
||||
|
||||
```sql
|
||||
SELECT SCHEMA_ID(N'alg') AS alg_schema_id;
|
||||
```
|
||||
|
||||
预期结果为 `NULL`。再次查询全库 DML 触发器集合,结果应与安装前保存的基线一致。
|
||||
|
||||
不要在影子应用运行时卸载。不要对包含未知对象的既有 `alg` schema 执行本卸载脚本。卸载后如需再次测试,必须重新执行完整安装和检查流程。
|
||||
|
||||
## 12. 已知残余风险
|
||||
|
||||
- 当前自动化验证是 Java 单元测试和 SQL 静态审计,不能替代真实 SQL Server 2016 副本演练。
|
||||
- `READ_UNCOMMITTED` 可能读到业务事务的中间状态;该结果只用于影子比较,不参与 Legacy 决策。
|
||||
- 3 秒事务超时会触发 fail-open,但 JDBC 取消和 SQL Server 回滚完成时间不保证严格等于 3 秒。
|
||||
- 每次影子规划前的全量刷新可能增加数据库 CPU 和 I/O;现场必须监控并设置短测试窗口。
|
||||
- 应用级 fail-open 能保护 Legacy 控制流,但不能消除数据库、网络或主机资源争用。
|
||||
|
||||
这些残余风险是采用“零 Legacy 触发器、每次规划前全量刷新”的代价,也是现场必须先小流量验证、随时可切回稳定源码的原因。
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Build a clean `origin/main`-based branch that observes inbound and outbound allocation with the Java algorithm while preserving every Legacy decision and business write.
|
||||
|
||||
**Architecture:** Reuse the existing `fortucky-algorithm` planner as an internal module, but expose it to WMS only through a fail-open shadow gateway. The gateway plans in a separate timed transaction; the original Legacy procedures remain unchanged and authoritative. A reduced SQL package installs only read-model and diagnostic objects.
|
||||
**Architecture:** Reuse the existing `fortucky-algorithm` planner as an internal module, but expose it to WMS only through a fail-open shadow gateway. The gateway plans in a separate timed `READ_UNCOMMITTED` transaction; the original Legacy procedures remain unchanged and authoritative. A reduced SQL package installs only read-model and diagnostic objects, creates no Legacy-table triggers, and refreshes its read model in full before planning.
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot, Spring JDBC transactions, MyBatis, SQL Server 2016, JUnit 5, Mockito, AssertJ, Maven 3.9.11.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- Legacy procedures execute exactly once and their current response construction remains authoritative.
|
||||
- Shadow observation is disabled by default and fails open on every runtime error.
|
||||
- Shadow database scripts never insert, update, or delete WMS business rows.
|
||||
- Shadow database scripts never create triggers on WMS business tables.
|
||||
|
||||
---
|
||||
|
||||
@@ -113,10 +114,10 @@
|
||||
- Test: `fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: read-model tables/views/procedures, decision log, dirty-lane queue, and three enqueue-only business-table triggers.
|
||||
- Produces: read-model tables/views/procedures and decision log; planning performs a full warehouse-scoped read-model refresh.
|
||||
- Excludes: ACTIVE mutation procedures and WMS business-table DML.
|
||||
|
||||
- [ ] Write static SQL tests with an allow-list for tables, views, procedures, and triggers; reject tidy/workflow/replay/reservation mutation names and business-table DML.
|
||||
- [ ] Write static SQL tests with an allow-list for tables, views, and procedures; reject all triggers, dirty queues, tidy/workflow/replay/reservation mutation names, and business-table DML.
|
||||
- [ ] Add an install/uninstall symmetry test and a test requiring initial synchronization plus diagnostic queries.
|
||||
- [ ] Run focused tests and confirm RED because scripts are absent.
|
||||
- [ ] Build the reduced scripts from the reviewed SQL Server 2016-compatible read-model portions.
|
||||
|
||||
@@ -8,7 +8,7 @@ Create a clean field-test branch directly from `origin/main` that runs the Java
|
||||
|
||||
1. The branch is based on `origin/main` and contains no license, UI, TLS, tidy, replay, PDA, WCS callback, or ACTIVE workflow integration changes.
|
||||
2. Legacy stored procedures are called exactly once with the same inputs and their return values remain authoritative.
|
||||
3. Shadow planning runs before the Legacy allocation so it observes the pre-decision state, but it runs in a separate `REQUIRES_NEW` transaction with a configurable timeout.
|
||||
3. Shadow planning runs before the Legacy allocation so it observes the pre-decision state, but it runs in a separate `REQUIRES_NEW`, `READ_UNCOMMITTED` transaction with a configurable timeout.
|
||||
4. A shadow exception, timeout, missing optional input, missing algorithm object, or comparison logging failure is caught and logged; it cannot skip, replace, roll back, or rewrite the Legacy result.
|
||||
5. No ACTIVE entry point is exposed by `ruoyi-admin`. Startup rejects any algorithm mode other than `SHADOW` when shadow observation is enabled.
|
||||
6. Shadow mode writes only `alg.*` read-model and diagnostic objects. It never calls reservation, tidy, deep-workflow, replay, move-completion, or other procedures that write WMS business tables.
|
||||
@@ -45,7 +45,7 @@ There are no algorithm controllers, ACTIVE schedulers, workflow callbacks, reser
|
||||
|
||||
## Transaction and failure isolation
|
||||
|
||||
Shadow planning uses `TransactionTemplate` with `PROPAGATION_REQUIRES_NEW`, read-model writes enabled, and a short timeout. The transaction completes or rolls back before Legacy allocation starts, so algorithm locks and rollback state cannot leak into the Legacy transaction. The adapter catches runtime failures at its public boundary and returns an empty observation.
|
||||
Shadow planning uses `TransactionTemplate` with `PROPAGATION_REQUIRES_NEW`, `ISOLATION_READ_UNCOMMITTED`, read-model writes enabled, and a short timeout. The transaction completes or rolls back before Legacy allocation starts, so algorithm locks and rollback state cannot leak into the Legacy transaction. The adapter catches runtime failures at its public boundary and returns an empty observation.
|
||||
|
||||
The integration never queries `alg.tidy_mode` and never uses an update lock for admission control.
|
||||
|
||||
@@ -55,13 +55,13 @@ The integration never queries `alg.tidy_mode` and never uses an update lock for
|
||||
|
||||
- policy/read-model/diagnostic tables required by inbound and outbound planning;
|
||||
- read-only views over WMS business data;
|
||||
- topology, snapshot, lane-state, dirty-lane refresh, and decision-log support;
|
||||
- three narrowly scoped `dbo` triggers that only enqueue affected lanes into `alg.dirty_lane_queue`;
|
||||
- topology, snapshot, lane-state, full-refresh, and decision-log support;
|
||||
- no triggers on any Legacy WMS table and no dirty-lane queue;
|
||||
- initial read-model synchronization.
|
||||
|
||||
It excludes tidy, replay, reservation mutation procedures, deep workflow, outbound workflow, WCS completion, PDA callback, and any stored procedure that inserts, updates, or deletes WMS business rows.
|
||||
|
||||
`uninstall-shadow.sql` first removes the three `dbo` triggers, then removes only shadow-owned `alg` objects and the `alg` schema. It never modifies WMS business rows. Operators must export `alg.decision_log` before uninstalling.
|
||||
`uninstall-shadow.sql` removes only shadow-owned `alg` objects and the `alg` schema. It never modifies WMS business rows. Operators must export `alg.decision_log` before uninstalling.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -85,7 +85,7 @@ Automated tests prove:
|
||||
- missing project/stock input still invokes Legacy exactly once;
|
||||
- Legacy result and response structure remain authoritative;
|
||||
- SHADOW configuration rejects ACTIVE mode;
|
||||
- shadow SQL contains only allow-listed objects and no WMS business-table DML;
|
||||
- shadow SQL contains only allow-listed `alg` objects, no triggers, and no WMS business-table DML;
|
||||
- install/uninstall object lists are symmetrical;
|
||||
- existing project tests and packaging still succeed.
|
||||
|
||||
|
||||
@@ -178,3 +178,15 @@ xss:
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# 影子算法默认关闭;现场测试仅通过 IDEA 环境变量显式开启
|
||||
fortucky:
|
||||
shadow:
|
||||
enabled: ${FORTUCKY_SHADOW_ENABLED:false}
|
||||
warehouse-id: ${FORTUCKY_SHADOW_WAREHOUSE_ID:}
|
||||
timeout-seconds: ${FORTUCKY_SHADOW_TIMEOUT_SECONDS:3}
|
||||
algorithm:
|
||||
mode: ${FORTUCKY_ALGORITHM_MODE:SHADOW}
|
||||
warehouse-id: ${FORTUCKY_ALGORITHM_WAREHOUSE_ID:01}
|
||||
database-writes-enabled: ${FORTUCKY_ALGORITHM_DATABASE_WRITES_ENABLED:true}
|
||||
legacy-fallback-on-technical-error: ${FORTUCKY_ALGORITHM_LEGACY_FALLBACK_ON_TECHNICAL_ERROR:false}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.integration.AlgorithmProperties;
|
||||
import com.ruoyi.algorithm.integration.SchedulingMode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowConfigurationTest {
|
||||
|
||||
@Test
|
||||
void applicationDefaultsKeepShadowDisabledAndReadyForExplicitShadowEnablement() throws IOException {
|
||||
MockEnvironment environment = new MockEnvironment();
|
||||
Binder binder = applicationBinder(environment);
|
||||
|
||||
ShadowObservationProperties shadow = binder
|
||||
.bind("fortucky.shadow", Bindable.of(ShadowObservationProperties.class))
|
||||
.orElseThrow(() -> new IllegalStateException("fortucky.shadow configuration is missing"));
|
||||
AlgorithmProperties algorithm = binder
|
||||
.bind("fortucky.algorithm", Bindable.of(AlgorithmProperties.class))
|
||||
.orElseThrow(() -> new IllegalStateException("fortucky.algorithm configuration is missing"));
|
||||
|
||||
assertThat(shadow.isEnabled()).isFalse();
|
||||
assertThat(shadow.getWarehouseId()).isEmpty();
|
||||
assertThat(shadow.getTimeoutSeconds()).isEqualTo(3);
|
||||
assertThat(algorithm.getMode()).isEqualTo(SchedulingMode.SHADOW);
|
||||
assertThat(algorithm.isDatabaseWritesEnabled()).isTrue();
|
||||
assertThat(algorithm.getWarehouseId()).isEqualTo("01");
|
||||
}
|
||||
|
||||
@Test
|
||||
void standardLegacyFallbackEnvironmentVariableOverridesTheSafeDefault() throws IOException {
|
||||
MockEnvironment environment = new MockEnvironment()
|
||||
.withProperty("FORTUCKY_ALGORITHM_LEGACY_FALLBACK_ON_TECHNICAL_ERROR", "true");
|
||||
|
||||
AlgorithmProperties algorithm = applicationBinder(environment)
|
||||
.bind("fortucky.algorithm", Bindable.of(AlgorithmProperties.class))
|
||||
.orElseThrow(() -> new IllegalStateException("fortucky.algorithm configuration is missing"));
|
||||
|
||||
assertThat(algorithm.isLegacyFallbackOnTechnicalError()).isTrue();
|
||||
}
|
||||
|
||||
private Binder applicationBinder(MockEnvironment environment) throws IOException {
|
||||
MutablePropertySources propertySources = environment.getPropertySources();
|
||||
List<PropertySource<?>> yaml = new YamlPropertySourceLoader()
|
||||
.load("application", new ClassPathResource("application.yml"));
|
||||
yaml.forEach(propertySources::addLast);
|
||||
return Binder.get(environment);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user