docs: plan stable snapshot shadow v3
This commit is contained in:
975
docs/superpowers/plans/2026-09-03-ft546wms-shadow-v3.md
Normal file
975
docs/superpowers/plans/2026-09-03-ft546wms-shadow-v3.md
Normal file
@@ -0,0 +1,975 @@
|
||||
# FT546 WMS Shadow v3 Stable-Snapshot Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build an observation-only v3 shadow pipeline that compares inbound and outbound decisions against a proven pre-Legacy snapshot without ever making Legacy wait or changing its result.
|
||||
|
||||
**Architecture:** A lock-free JVM coordinator leases one already-published SQL read-model generation to at most one observation and marks concurrent samples invalid. The existing algorithm receives new explicit “published snapshot” planning entry points that skip live refresh, while one background worker compares the Legacy result and refreshes the next generation only after a quiet window. SQL Server owns atomic generation refresh/publication and immutable input capture; field scripts verify, upgrade, export, disable, and uninstall only the shadow package.
|
||||
|
||||
**Tech Stack:** Java 17, Spring Boot, Spring transactions, MyBatis, SQL Server 2016 RTM, JUnit 5, Mockito, AssertJ, Maven 3.9.11, Windows CMD, Node.js syntax checks.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-03-ft546wms-shadow-v3-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work only on branch `test/ft546wms-shadow-v3-20260903` based on v2 commit `3f7ed3718defef2c4ab37e1deb2534d2329747e4`; verified main commit `916ac6bda32a5b102a3181cc8c5dbd5319c5656d` must remain an ancestor.
|
||||
- Legacy allocation procedures execute exactly once with unchanged arguments, transaction ownership, result handling, response codes, and response fields.
|
||||
- A Legacy request performs no shadow database/file access, synchronous shadow log write, monitor wait, sleep, `Future.get`, or executor fallback work.
|
||||
- Caller-side shadow admission is fixed-size in-memory bookkeeping with a field p99 budget of 1 ms; queue rejection returns immediately.
|
||||
- Shadow failure, staleness, concurrency, timeout, bad configuration, or missing input always fails open to Legacy and fails closed for observation validity.
|
||||
- The supported field topology is exactly one FT546WMS JVM; a second process or recent different shadow-instance claim prevents v3 shadow enablement but never prevents Legacy startup.
|
||||
- `FORTUCKY_ALGORITHM_WAREHOUSE_ID=01` is authoritative. Numeric `1` equals `01`; raw `W02` is diagnostic only and does not override or by itself reject the `01` plan.
|
||||
- Snapshot maximum age defaults to 60 seconds and accepts 5–300 seconds. Quiet interval defaults to 1000 ms and accepts 100–10000 ms. Worker timeout defaults to 3 seconds and accepts 1–30 seconds.
|
||||
- SQL uses SQL Server 2016-compatible syntax, `READ UNCOMMITTED` source reads, shadow-owned transactions, and only `alg.*` writes. No trigger or Legacy business-table DML may be introduced.
|
||||
- v2 decision history is preserved during the recognized v2-to-v3 upgrade. Unknown `alg` objects, an unexpected manifest, or partial ownership must stop install/upgrade/uninstall.
|
||||
- No ACTIVE controller, routing flag, reservation, tidy, replay, WCS callback, PDA behavior, frontend behavior, or unrelated production code is added or changed.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Validated v3 Configuration and Warehouse Normalization
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/application.yml`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `int getSnapshotMaxAgeSeconds()`, `int getQuietIntervalMillis()`, and exact startup range validation on `ShadowObservationProperties`.
|
||||
- Produces: `WarehouseIdNormalizer.equivalent(String, String)` for numeric leading-zero equivalence and exact trimmed non-numeric comparison.
|
||||
- Preserves: `fortucky.shadow.enabled=false` default and current fallback from blank `fortucky.shadow.warehouse-id` to `SchedulingFacade.defaultWarehouseId()`.
|
||||
|
||||
- [ ] **Step 1: Add failing configuration and guard tests.**
|
||||
|
||||
```java
|
||||
assertThat(shadow.getSnapshotMaxAgeSeconds()).isEqualTo(60);
|
||||
assertThat(shadow.getQuietIntervalMillis()).isEqualTo(1000);
|
||||
|
||||
shadow.setSnapshotMaxAgeSeconds(4);
|
||||
assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.hasMessageContaining("between 5 and 300");
|
||||
|
||||
shadow.setSnapshotMaxAgeSeconds(60);
|
||||
shadow.setQuietIntervalMillis(10001);
|
||||
assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate())
|
||||
.hasMessageContaining("between 100 and 10000");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused tests and verify RED because the new properties do not exist.**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowConfigurationTest,ShadowSafetyGuardTest,WarehouseIdNormalizerTest test
|
||||
```
|
||||
|
||||
Expected: compilation failure for the missing getters/class.
|
||||
|
||||
- [ ] **Step 3: Implement the exact property defaults, YAML bindings, and validation.**
|
||||
|
||||
```java
|
||||
private int snapshotMaxAgeSeconds = 60;
|
||||
private int quietIntervalMillis = 1000;
|
||||
|
||||
if (shadowProperties.getTimeoutSeconds() < 1 || shadowProperties.getTimeoutSeconds() > 30) {
|
||||
throw new IllegalStateException("Shadow observation timeout must be between 1 and 30 seconds");
|
||||
}
|
||||
if (shadowProperties.getSnapshotMaxAgeSeconds() < 5
|
||||
|| shadowProperties.getSnapshotMaxAgeSeconds() > 300) {
|
||||
throw new IllegalStateException("Shadow snapshot maximum age must be between 5 and 300 seconds");
|
||||
}
|
||||
if (shadowProperties.getQuietIntervalMillis() < 100
|
||||
|| shadowProperties.getQuietIntervalMillis() > 10000) {
|
||||
throw new IllegalStateException("Shadow quiet interval must be between 100 and 10000 milliseconds");
|
||||
}
|
||||
```
|
||||
|
||||
Add these exact YAML keys:
|
||||
|
||||
```yaml
|
||||
snapshot-max-age-seconds: ${FORTUCKY_SHADOW_SNAPSHOT_MAX_AGE_SECONDS:60}
|
||||
quiet-interval-millis: ${FORTUCKY_SHADOW_QUIET_INTERVAL_MILLIS:1000}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add normalization tests and implement the utility.**
|
||||
|
||||
```java
|
||||
assertThat(WarehouseIdNormalizer.equivalent("1", "01")).isTrue();
|
||||
assertThat(WarehouseIdNormalizer.equivalent(" 01 ", "1")).isTrue();
|
||||
assertThat(WarehouseIdNormalizer.equivalent("W02", "01")).isFalse();
|
||||
assertThat(WarehouseIdNormalizer.equivalent(" W02 ", "W02")).isTrue();
|
||||
```
|
||||
|
||||
Implementation contract:
|
||||
|
||||
```java
|
||||
final class WarehouseIdNormalizer {
|
||||
static boolean equivalent(String first, String second) {
|
||||
if (first == null || second == null) {
|
||||
return false;
|
||||
}
|
||||
String left = first.trim();
|
||||
String right = second.trim();
|
||||
if (left.matches("[0-9]+") && right.matches("[0-9]+")) {
|
||||
return new java.math.BigInteger(left).equals(new java.math.BigInteger(right));
|
||||
}
|
||||
return left.equals(right);
|
||||
}
|
||||
private WarehouseIdNormalizer() { }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the focused tests and verify GREEN.**
|
||||
|
||||
Run the command from Step 2. Expected: all selected tests pass.
|
||||
|
||||
- [ ] **Step 6: Commit the validated configuration boundary.**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java ruoyi-admin/src/main/resources/application.yml ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java
|
||||
git commit -m "feat: validate shadow v3 snapshot settings"
|
||||
```
|
||||
|
||||
### Task 2: Build the Lock-Free Snapshot Coordinator
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/PublishedShadowSnapshot.java`
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSkipReason.java`
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSnapshotCoordinator.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSnapshotCoordinatorTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `PublishedShadowSnapshot(long generation, String warehouseId, Instant synchronizedAt, String sourceSignature)`.
|
||||
- Produces: `ShadowSnapshotCoordinator.Admission enter(Instant now)`, `void exit(Admission admission)`, and `void finishObservation(Admission admission)`; every enabled Legacy call exits activity exactly once while worker lease cleanup remains separately idempotent.
|
||||
- Produces: `Optional<RefreshTicket> tryBeginRefresh(Instant now)`, `boolean publish(RefreshTicket, PublishedShadowSnapshot, Instant)`, and `void failRefresh(RefreshTicket, String)`.
|
||||
- Guarantees: `Admission.accepted()` is true only for one fresh `READY` snapshot; any later Legacy entry changes the epoch and makes the admitted sample non-comparable.
|
||||
|
||||
- [ ] **Step 1: Write failing tests for startup, one lease, busy skipping, and exact-once exit.**
|
||||
|
||||
```java
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("2026-09-03T00:00:00Z");
|
||||
assertThat(coordinator.enter(now()).skipReason()).isEqualTo(ShadowSkipReason.NOT_READY);
|
||||
|
||||
publish(coordinator, snapshot(1, "01", now()));
|
||||
var first = coordinator.enter(now());
|
||||
var second = coordinator.enter(now());
|
||||
assertThat(first.accepted()).isTrue();
|
||||
assertThat(second.accepted()).isFalse();
|
||||
assertThat(second.skipReason()).isEqualTo(ShadowSkipReason.BUSY);
|
||||
coordinator.exit(second);
|
||||
coordinator.exit(first);
|
||||
assertThat(coordinator.activeLegacyCount()).isZero();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the validation-before-publication race test.**
|
||||
|
||||
```java
|
||||
var refresh = coordinator.tryBeginRefresh(now()).orElseThrow();
|
||||
var concurrent = coordinator.enter(now());
|
||||
assertThat(coordinator.publish(refresh, snapshot(2, "01", now()), now())).isFalse();
|
||||
assertThat(coordinator.phase()).isNotEqualTo(ShadowSnapshotCoordinator.Phase.READY);
|
||||
coordinator.exit(concurrent);
|
||||
```
|
||||
|
||||
Also assert that a second Legacy entry after an accepted admission makes `coordinator.isComparable(first)` false.
|
||||
|
||||
- [ ] **Step 3: Write stale and warehouse-mismatch tests.**
|
||||
|
||||
```java
|
||||
publish(coordinator, snapshot(3, "01", now().minusSeconds(61)));
|
||||
assertThat(coordinator.enter(now()).skipReason()).isEqualTo(ShadowSkipReason.STALE);
|
||||
|
||||
publish(coordinator, snapshot(4, "02", now()));
|
||||
assertThat(coordinator.enter(now()).skipReason()).isEqualTo(ShadowSkipReason.WAREHOUSE);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the coordinator test and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowSnapshotCoordinatorTest test
|
||||
```
|
||||
|
||||
Expected: compilation failure because coordinator types are absent.
|
||||
|
||||
- [ ] **Step 5: Implement one atomic stamped state and idempotent admissions.**
|
||||
|
||||
Use one `AtomicReference<State>` where `State` contains `Phase`, `epoch`, `activeLegacyCount`, the published snapshot, and quiet-since time. `enter` increments epoch/active before Legacy, `exit` uses an `AtomicBoolean` on the admission to decrement once, and `publish` uses compare-and-set against the refresh ticket's epoch/state stamp.
|
||||
|
||||
```java
|
||||
private record State(
|
||||
Phase phase,
|
||||
long epoch,
|
||||
int activeLegacyCount,
|
||||
PublishedShadowSnapshot published,
|
||||
Long leasedEpoch,
|
||||
Instant quietSince,
|
||||
String lastFailure) { }
|
||||
```
|
||||
|
||||
Implement the concrete API named in the Interfaces section with CAS loops over one `AtomicReference<State>`. `enter` must first advance epoch/active count in the same replacement state, then lease only a matching fresh `READY` snapshot. `exit` decrements only Legacy activity; `finishObservation` releases only the worker lease. `publish` must compare the ticket's full stamped state, not only its generation.
|
||||
|
||||
- [ ] **Step 6: Run the coordinator tests repeatedly and verify GREEN.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowSnapshotCoordinatorTest test
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowSnapshotCoordinatorTest test
|
||||
```
|
||||
|
||||
Expected: both runs pass without timing sleeps.
|
||||
|
||||
- [ ] **Step 7: Commit the isolated state machine.**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/PublishedShadowSnapshot.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSkipReason.java ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSnapshotCoordinator.java ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSnapshotCoordinatorTest.java
|
||||
git commit -m "feat: coordinate stable shadow snapshots"
|
||||
```
|
||||
|
||||
### Task 3: Add Explicit No-Refresh Algorithm Planning Entry Points
|
||||
|
||||
**Files:**
|
||||
- Modify: `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/service/WarehouseApplicationService.java`
|
||||
- Modify: `fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/SchedulingFacade.java`
|
||||
- Test: `fortucky-algorithm/src/test/java/com/ruoyi/algorithm/service/WarehouseApplicationServiceTest.java`
|
||||
- Create: `fortucky-algorithm/src/test/java/com/ruoyi/algorithm/integration/SchedulingFacadePublishedSnapshotTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `InboundPlanningResult planInboundFromPublishedSnapshot(String warehouseId, InboundRequest request)`.
|
||||
- Produces: `OutboundPlan planOutboundFromPublishedSnapshot(String warehouseId, OutboundRequest request)`.
|
||||
- Preserves: existing `planInbound` and `planOutbound` refresh-before-read behavior for every non-v3 consumer.
|
||||
|
||||
- [ ] **Step 1: Add failing service tests proving the new methods never refresh.**
|
||||
|
||||
```java
|
||||
service.planInboundFromPublishedSnapshot("W1", inbound("I-STABLE"));
|
||||
verify(repository, never()).refreshChangedLanes(any());
|
||||
verify(repository).loadPolicy("W1");
|
||||
|
||||
service.planOutboundFromPublishedSnapshot("W1", outboundRequest);
|
||||
verify(repository, never()).refreshChangedLanes(any());
|
||||
verify(repository).searchOutboundStock(eq("W1"), any());
|
||||
```
|
||||
|
||||
Keep the existing tests that verify `planInbound` and `planOutbound` call `refreshChangedLanes`.
|
||||
|
||||
- [ ] **Step 2: Add failing facade tests proving SHADOW can call only the published-snapshot methods and LEGACY cannot.**
|
||||
|
||||
```java
|
||||
facade.planInboundFromPublishedSnapshot("W1", request);
|
||||
verify(applicationService).planInboundFromPublishedSnapshot("W1", request);
|
||||
|
||||
properties.setMode(SchedulingMode.LEGACY);
|
||||
assertThatThrownBy(() -> facade.planOutboundFromPublishedSnapshot("W1", outboundRequest))
|
||||
.hasMessageContaining("disabled in LEGACY mode");
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the focused algorithm tests and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl fortucky-algorithm -Dtest=WarehouseApplicationServiceTest,SchedulingFacadePublishedSnapshotTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Extract private refresh-controlled implementations without changing planner logic.**
|
||||
|
||||
```java
|
||||
public InboundPlanningResult planInbound(String warehouseId, InboundRequest request) {
|
||||
return planInbound(warehouseId, request, true);
|
||||
}
|
||||
|
||||
public InboundPlanningResult planInboundFromPublishedSnapshot(String warehouseId, InboundRequest request) {
|
||||
return planInbound(warehouseId, request, false);
|
||||
}
|
||||
|
||||
private InboundPlanningResult planInbound(String warehouseId, InboundRequest request, boolean refresh) {
|
||||
if (refresh) {
|
||||
repository.refreshChangedLanes(warehouseId);
|
||||
}
|
||||
return planInboundFromCurrentReadModel(warehouseId, request);
|
||||
}
|
||||
```
|
||||
|
||||
Move the current body after `refreshChangedLanes` into `planInboundFromCurrentReadModel`; do not edit its statements. Apply the same named extraction to outbound with `planOutboundFromCurrentReadModel`. Do not change tidy, reservation, or workflow methods.
|
||||
|
||||
- [ ] **Step 5: Add facade delegates guarded by the existing `requireAlgorithmMode()`.**
|
||||
|
||||
```java
|
||||
public InboundPlanningResult planInboundFromPublishedSnapshot(String warehouseId, InboundRequest request) {
|
||||
requireAlgorithmMode();
|
||||
return applicationService.planInboundFromPublishedSnapshot(warehouseId, request);
|
||||
}
|
||||
```
|
||||
|
||||
Add the equivalent outbound delegate.
|
||||
|
||||
- [ ] **Step 6: Run all algorithm service/facade tests and verify GREEN.**
|
||||
|
||||
```bash
|
||||
mvn -pl fortucky-algorithm -Dtest=WarehouseApplicationServiceTest,SchedulingFacadePublishedSnapshotTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit the explicit planning seam.**
|
||||
|
||||
```bash
|
||||
git add fortucky-algorithm/src/main/java/com/ruoyi/algorithm/service/WarehouseApplicationService.java fortucky-algorithm/src/main/java/com/ruoyi/algorithm/integration/SchedulingFacade.java fortucky-algorithm/src/test/java/com/ruoyi/algorithm/service/WarehouseApplicationServiceTest.java fortucky-algorithm/src/test/java/com/ruoyi/algorithm/integration/SchedulingFacadePublishedSnapshotTest.java
|
||||
git commit -m "feat: plan from published read model"
|
||||
```
|
||||
|
||||
### Task 4: Upgrade the SQL Package to Atomic v3 Generations
|
||||
|
||||
**Files:**
|
||||
- Modify: `fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql`
|
||||
- Create: `fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v2-to-v3.sql`
|
||||
- Modify: `fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql`
|
||||
- Modify: `fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql`
|
||||
- Modify: `fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces manifest `FT546WMS_SHADOW_ONLY / 2026-09-03-3`.
|
||||
- Produces tables `alg.shadow_snapshot_state`, `alg.cargo_space_snapshot`, `alg.active_job_snapshot`, `alg.shadow_inbound_request_snapshot`, `alg.shadow_outbound_request_snapshot`, `alg.shadow_runtime_metric`, and `alg.shadow_instance_heartbeat`.
|
||||
- Produces procedures `alg.usp_claim_shadow_instance` and `alg.usp_refresh_shadow_snapshot`.
|
||||
- Preserves existing v2 `alg.decision_log` rows during the recognized upgrade.
|
||||
- Guarantees all current read-model rows and input rows carry one `snapshot_generation`, and v3 views expose only the published warehouse generation.
|
||||
|
||||
- [ ] **Step 1: Extend static migration tests with the exact v3 object allow-list and manifest.**
|
||||
|
||||
```java
|
||||
assertThat(createdTables).contains(
|
||||
"shadow_snapshot_state", "cargo_space_snapshot", "active_job_snapshot",
|
||||
"shadow_inbound_request_snapshot",
|
||||
"shadow_outbound_request_snapshot", "shadow_runtime_metric",
|
||||
"shadow_instance_heartbeat");
|
||||
assertThat(createdProcedures).contains("usp_claim_shadow_instance", "usp_refresh_shadow_snapshot");
|
||||
assertThat(installSql).contains("'FT546WMS_SHADOW_ONLY', '2026-09-03-3'");
|
||||
assertThat(upgradeSql).contains("'2026-08-12-1'").contains("'2026-09-03-3'");
|
||||
assertThat(uninstallSql).contains("'2026-09-03-3'");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add tests rejecting Legacy DML, triggers, mismatched allow-lists, and non-transactional publication.**
|
||||
|
||||
Require `usp_refresh_shadow_snapshot` to contain `SET XACT_ABORT ON`, `BEGIN TRANSACTION`, the component refreshes, a final `UPDATE alg.shadow_snapshot_state`, and `COMMIT TRANSACTION` in that order. Require the upgrade to validate the exact v2 object set before any `ALTER`, and the uninstaller to list every v3 object exactly once.
|
||||
|
||||
```java
|
||||
assertThat(normalizedSql).doesNotContain("insert into dbo.", "update dbo.", "delete from dbo.", "merge dbo.");
|
||||
assertThat(normalizedSql).doesNotContain("create trigger");
|
||||
assertThat(indexOfPublish).isGreaterThan(indexOfInputRefresh);
|
||||
assertThat(indexOfCommit).isGreaterThan(indexOfPublish);
|
||||
```
|
||||
|
||||
Extract the `CREATE VIEW alg.v_space_state` and `CREATE VIEW alg.v_active_job` bodies and assert they contain no `dbo.` reference. `dbo.*` reads are allowed only inside the controlled refresh/source-signature procedures and the initial input-copy statements.
|
||||
|
||||
- [ ] **Step 3: Run the migration test and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl fortucky-algorithm -Dtest=ShadowSqlServerMigrationScriptTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add generation metadata and immutable planning-input tables to clean install.**
|
||||
|
||||
Use warehouse-scoped primary keys and explicit generation columns:
|
||||
|
||||
```sql
|
||||
CREATE TABLE alg.shadow_snapshot_state (
|
||||
warehouse_id varchar(80) NOT NULL PRIMARY KEY,
|
||||
snapshot_generation bigint NOT NULL,
|
||||
snapshot_status varchar(20) NOT NULL,
|
||||
synchronized_at datetime2 NULL,
|
||||
source_signature varbinary(32) NULL,
|
||||
source_row_count bigint NULL,
|
||||
instance_id varchar(80) NULL,
|
||||
last_error varchar(1000) NULL
|
||||
);
|
||||
|
||||
CREATE TABLE alg.shadow_inbound_request_snapshot (
|
||||
warehouse_id varchar(80) NOT NULL,
|
||||
task_no varchar(80) NOT NULL,
|
||||
tray_no varchar(255) NOT NULL,
|
||||
request_id varchar(80) NULL,
|
||||
raw_warehouse_id varchar(80) NULL,
|
||||
project_no varchar(255) NULL,
|
||||
product_id varchar(255) NULL,
|
||||
lot_no varchar(80) NULL,
|
||||
snapshot_generation bigint NOT NULL,
|
||||
synchronized_at datetime2 NOT NULL,
|
||||
CONSTRAINT PK_alg_shadow_inbound_request_snapshot PRIMARY KEY (warehouse_id, task_no, tray_no)
|
||||
);
|
||||
```
|
||||
|
||||
Create the outbound input table with `stockout_detail_id`, `raw_warehouse_id`, `project_no`, `product_id`, `requested_quantity`, `snapshot_generation`, and `synchronized_at`. Add `snapshot_generation` to topology, inventory, inventory summary, and lane state. Do not run an uncontrolled initial refresh at install time; leave state `INITIALIZING` for the v3 worker.
|
||||
|
||||
Use these remaining snapshot columns:
|
||||
|
||||
```sql
|
||||
CREATE TABLE alg.cargo_space_snapshot (
|
||||
warehouse_id varchar(80) NOT NULL,
|
||||
space_id varchar(80) NOT NULL,
|
||||
lane_code varchar(80) NULL,
|
||||
cargo_area_id varchar(80) NULL,
|
||||
roadway_id varchar(80) NULL,
|
||||
display_name varchar(255) NULL,
|
||||
platoon int NULL,
|
||||
column_no int NULL,
|
||||
floor_no int NULL,
|
||||
depth_raw varchar(80) NULL,
|
||||
space_status varchar(20) NULL,
|
||||
locked varchar(20) NULL,
|
||||
tray_code varchar(255) NULL,
|
||||
snapshot_generation bigint NOT NULL,
|
||||
synchronized_at datetime2 NOT NULL,
|
||||
CONSTRAINT PK_alg_cargo_space_snapshot PRIMARY KEY (warehouse_id, space_id)
|
||||
);
|
||||
|
||||
CREATE TABLE alg.active_job_snapshot (
|
||||
warehouse_id varchar(80) NOT NULL,
|
||||
job_id varchar(80) NOT NULL,
|
||||
task_no varchar(80) NULL,
|
||||
tray_code varchar(255) NULL,
|
||||
source_space_id varchar(80) NULL,
|
||||
target_space_id varchar(80) NULL,
|
||||
status varchar(20) NULL,
|
||||
in_or_out varchar(20) NULL,
|
||||
create_time_raw varchar(80) NULL,
|
||||
snapshot_generation bigint NOT NULL,
|
||||
synchronized_at datetime2 NOT NULL,
|
||||
CONSTRAINT PK_alg_active_job_snapshot PRIMARY KEY (warehouse_id, job_id)
|
||||
);
|
||||
|
||||
CREATE TABLE alg.shadow_outbound_request_snapshot (
|
||||
warehouse_id varchar(80) NOT NULL,
|
||||
stockout_detail_id varchar(80) NOT NULL,
|
||||
raw_warehouse_id varchar(80) NULL,
|
||||
project_no varchar(255) NULL,
|
||||
product_id varchar(255) NULL,
|
||||
requested_quantity decimal(38,6) NULL,
|
||||
snapshot_generation bigint NOT NULL,
|
||||
synchronized_at datetime2 NOT NULL,
|
||||
CONSTRAINT PK_alg_shadow_outbound_request_snapshot PRIMARY KEY (warehouse_id, stockout_detail_id)
|
||||
);
|
||||
```
|
||||
|
||||
`alg.shadow_runtime_metric` uses `(warehouse_id, metric_name, bucket_upper_micros)` as its primary key plus `metric_count`, `metric_total_micros`, `metric_max_micros`, and `flushed_at`. `alg.shadow_instance_heartbeat` uses `warehouse_id` as primary key plus `instance_id`, `claimed_at`, and `last_seen_at`. Rewrite `v_space_state`, `v_active_job`, topology rebuild, and snapshot-mode lane fingerprinting to read only generation-qualified `alg` snapshots; no planner-facing view may read `dbo.*`.
|
||||
|
||||
- [ ] **Step 5: Implement background-only instance claim and atomic refresh procedures.**
|
||||
|
||||
`usp_claim_shadow_instance` uses `UPDLOCK, HOLDLOCK` only on `alg.shadow_instance_heartbeat`, accepts a 120-second stale threshold, and returns `claimed=0/1`. `usp_refresh_shadow_snapshot` accepts warehouse, next generation, and instance id; it computes pre/post row-count plus SHA2-256 signatures over the planning fields, copies cargo spaces, inventory, active jobs, inbound inputs, and outbound inputs before rebuilding topology/lane state in one transaction, throws on signature mismatch, and updates the publish marker last.
|
||||
|
||||
The final validation and publication block uses this exact form:
|
||||
|
||||
```sql
|
||||
IF @pre_source_count <> @post_source_count
|
||||
OR @pre_source_signature <> @post_source_signature
|
||||
THROW 51025, 'FT546 shadow refresh refused: source changed during refresh', 1;
|
||||
|
||||
MERGE alg.shadow_snapshot_state WITH (HOLDLOCK) AS target
|
||||
USING (SELECT @warehouse_id AS warehouse_id) AS source
|
||||
ON source.warehouse_id = target.warehouse_id
|
||||
WHEN MATCHED THEN UPDATE SET
|
||||
snapshot_generation = @snapshot_generation,
|
||||
snapshot_status = 'PUBLISHED',
|
||||
synchronized_at = SYSUTCDATETIME(),
|
||||
source_signature = @post_source_signature,
|
||||
source_row_count = @post_source_count,
|
||||
instance_id = @instance_id,
|
||||
last_error = NULL
|
||||
WHEN NOT MATCHED THEN INSERT (
|
||||
warehouse_id, snapshot_generation, snapshot_status, synchronized_at,
|
||||
source_signature, source_row_count, instance_id, last_error)
|
||||
VALUES (
|
||||
@warehouse_id, @snapshot_generation, 'PUBLISHED', SYSUTCDATETIME(),
|
||||
@post_source_signature, @post_source_count, @instance_id, NULL);
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
```
|
||||
|
||||
Before this block, copy the exact planning fields from `dbo.bas_cargo_space`, `dbo.bas_stock`, `dbo.mydo_job`, `dbo.mydo_job_detail`, `dbo.in_stockin_detail`, and `dbo.out_stockout_detail` into their generation-qualified `alg` snapshots, then run the topology and lane-state statements only over those copied rows. No dynamic SQL is permitted.
|
||||
|
||||
- [ ] **Step 6: Implement a strict v2-to-v3 upgrade and matching v3 uninstall.**
|
||||
|
||||
The upgrade validates package id/version `2026-08-12-1`, validates the complete v2 allow-list, applies DDL in a transaction, creates v3 procedures/views, updates the manifest last, and leaves existing `decision_log` rows intact. When the exact `2026-09-03-3` manifest and complete v3 allow-list are already present, rerunning the upgrade exits successfully without DDL. The uninstaller validates only `2026-09-03-3`, drops all v3 procedures/views/tables in dependency order, and refuses unknown/missing objects.
|
||||
|
||||
- [ ] **Step 7: Extend diagnostics for generation, heartbeat, metrics, input coverage, and blocking.**
|
||||
|
||||
Add read-only result sets for manifest, published generation age/status/signature, input row counts per generation, recent instance claim, admission histogram, comparison completeness, inventory parity, and sessions blocked by `alg.*` statements. Diagnostics must not update any object.
|
||||
|
||||
- [ ] **Step 8: Run migration tests and verify GREEN.**
|
||||
|
||||
```bash
|
||||
mvn -pl fortucky-algorithm -Dtest=ShadowSqlServerMigrationScriptTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Commit the v3 SQL package.**
|
||||
|
||||
```bash
|
||||
git add fortucky-algorithm/src/main/resources/db/sqlserver/install-shadow.sql fortucky-algorithm/src/main/resources/db/sqlserver/upgrade-shadow-v2-to-v3.sql fortucky-algorithm/src/main/resources/db/sqlserver/uninstall-shadow.sql fortucky-algorithm/src/main/resources/db/sqlserver/diagnostics-shadow.sql fortucky-algorithm/src/test/java/com/ruoyi/algorithm/repository/ShadowSqlServerMigrationScriptTest.java
|
||||
git commit -m "feat: publish atomic shadow snapshot generations"
|
||||
```
|
||||
|
||||
### Task 5: Read and Plan Exclusively From the Leased Generation
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSnapshotDescriptor.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowInboundReference.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowStockReference.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowObservation.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowObservation.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngine.java`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `boolean claimInstance(String warehouseId, String instanceId)`, `void refreshSnapshot(String warehouseId, long generation, String instanceId)`, and `ShadowSnapshotDescriptor selectPublishedSnapshot(String warehouseId)` mapper operations.
|
||||
- Produces engine methods `refreshSnapshot(long generation, String instanceId)`, `planInbound(PublishedShadowSnapshot, String, String)`, `planProjectOutbound(PublishedShadowSnapshot, String)`, and `planExactStockOutbound(PublishedShadowSnapshot, String, String)`.
|
||||
- Consumes: Task 3 facade methods that never call `refreshChangedLanes`.
|
||||
- Guarantees: input mapper planning selects read only `alg.shadow_*_request_snapshot` and `alg.inventory_item_snapshot`, while algorithm repository views read only `alg.cargo_space_snapshot`, `alg.active_job_snapshot`, and the other published read-model tables; every path is warehouse- and generation-qualified and raw warehouse remains diagnostic only.
|
||||
- Guarantees: the engine reads `selectPublishedSnapshot` before and after planning and rejects the observation unless both descriptors match the leased generation, warehouse, synchronization time, and source signature.
|
||||
|
||||
`ShadowSnapshotDescriptor` is a MyBatis JavaBean with `long snapshotGeneration`, `String warehouseId`, `String snapshotStatus`, `Instant synchronizedAt`, and uppercase hexadecimal `String sourceSignature`, with ordinary getters/setters. `refreshSnapshot` converts a validated descriptor into the immutable `PublishedShadowSnapshot` record from Task 2.
|
||||
|
||||
- [ ] **Step 1: Replace current engine tests with failing stable-generation tests while retaining transaction assertions.**
|
||||
|
||||
```java
|
||||
PublishedShadowSnapshot snapshot = new PublishedShadowSnapshot(
|
||||
42L, "01", Instant.parse("2026-09-03T00:00:00Z"), "ABC123");
|
||||
when(mapper.selectInboundInput("01", 42L, "TASK-1", "TRAY-1")).thenReturn(input);
|
||||
|
||||
InboundShadowObservation observation = engine.planInbound(snapshot, "TASK-1", "TRAY-1");
|
||||
|
||||
verify(facade).planInboundFromPublishedSnapshot(eq("01"), any());
|
||||
verify(mapper).selectInboundInput("01", 42L, "TASK-1", "TRAY-1");
|
||||
assertThat(observation.snapshotGeneration()).isEqualTo(42L);
|
||||
assertThat(observation.warehouseId()).isEqualTo("01");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add tests proving raw `W02` stays diagnostic while planning uses configured/published `01`.**
|
||||
|
||||
```java
|
||||
reference.setWarehouseId("W02");
|
||||
engine.planProjectOutbound(snapshot, "DETAIL-1");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), any());
|
||||
assertThat(observation.reason()).contains("rawWarehouse=W02");
|
||||
```
|
||||
|
||||
Also test raw `1` normalizes to `01`, exact-stock lookup uses `(warehouseId, generation, stockId)`, and no planning mapper query contains `from bas_stock`, `from mydo_job`, or `from out_stockout_detail`.
|
||||
|
||||
- [ ] **Step 3: Add a failing test for generation change during planning.**
|
||||
|
||||
```java
|
||||
when(mapper.selectPublishedSnapshot("01"))
|
||||
.thenReturn(descriptor(42L), descriptor(43L));
|
||||
|
||||
assertThatThrownBy(() -> engine.planInbound(snapshot, "TASK-1", "TRAY-1"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("published snapshot changed during planning");
|
||||
verify(mapper, never()).insertComparison(any(), any(), any(), any(), any(), any());
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add refresh/claim tests.**
|
||||
|
||||
```java
|
||||
when(mapper.claimInstance("01", "INSTANCE-1", 120)).thenReturn(true);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(43L));
|
||||
|
||||
PublishedShadowSnapshot refreshed = engine.refreshSnapshot(43L, "INSTANCE-1");
|
||||
|
||||
verify(mapper).refreshSnapshot("01", 43L, "INSTANCE-1");
|
||||
assertThat(refreshed.generation()).isEqualTo(43L);
|
||||
```
|
||||
|
||||
Reject null, non-`PUBLISHED`, wrong generation, or non-equivalent descriptor warehouse.
|
||||
|
||||
- [ ] **Step 5: Run planning tests and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -Dtest=ShadowPlanningEngineTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Implement fixed mapper signatures and generation-qualified SQL.**
|
||||
|
||||
```java
|
||||
ShadowInboundReference selectInboundInput(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("taskNo") String taskNo,
|
||||
@Param("trayNo") String trayNo);
|
||||
ShadowStockReference selectProjectOutboundReference(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("stockoutDetailId") String detailId);
|
||||
ShadowStockReference selectStockReference(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("stockId") String stockId);
|
||||
boolean claimInstance(@Param("warehouseId") String warehouseId,
|
||||
@Param("instanceId") String instanceId, @Param("staleSeconds") int staleSeconds);
|
||||
void refreshSnapshot(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("instanceId") String instanceId);
|
||||
ShadowSnapshotDescriptor selectPublishedSnapshot(@Param("warehouseId") String warehouseId);
|
||||
int insertComparison(@Param("decisionId") String decisionId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("decisionType") String decisionType,
|
||||
@Param("inputVersion") long inputVersion,
|
||||
@Param("resultCode") String resultCode,
|
||||
@Param("reason") String reason);
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Implement stable engine methods and remove per-plan refresh from the shadow path.**
|
||||
|
||||
Use `snapshot.warehouseId()` as the algorithm warehouse, preserve `reference.getWarehouseId()` as `rawWarehouse`, call only `plan*FromPublishedSnapshot`, and put `snapshotGeneration`, `snapshotSync`, `sourceSignature`, input fields, candidate counts, and raw warehouse in every comparison reason. Add `shadowCandidateCount` to `ShadowInboundReference`; populate it from the generation-qualified lane-state read model. Keep `shadowMatchCount` for outbound. Pass `snapshot.generation()` into `input_version` using `CONVERT(binary(8), #{inputVersion})`.
|
||||
|
||||
- [ ] **Step 8: Run planning tests and verify GREEN.**
|
||||
|
||||
Run the command from Step 5. Expected: all selected tests pass.
|
||||
|
||||
- [ ] **Step 9: Commit generation-qualified planning.**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowPlanningEngineTest.java
|
||||
git commit -m "feat: plan shadow decisions from leased generation"
|
||||
```
|
||||
|
||||
### Task 6: Integrate Coordinator, Completion Tokens, Metrics, and Recovery
|
||||
|
||||
**Files:**
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowRuntimeMetrics.java`
|
||||
- Create: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowFailureStage.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/InboundShadowCompletion.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/OutboundShadowCompletion.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationGateway.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/mapper/ShadowInputMapper.java`
|
||||
- Modify: `ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml`
|
||||
- Test: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowObservationGatewayTest.java`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowRuntimeMetricsTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Completion tokens retain `accepted()` and existing success completion methods, and add non-throwing `fail(Throwable)`; success/failure both close one coordinator admission exactly once.
|
||||
- `ShadowRuntimeMetrics.recordAdmission(ShadowSkipReason, long elapsedNanos)`, `recordLegacyDuration(long)`, `increment(ShadowSkipReason)`, and `recordError(ShadowFailureStage)` update fixed counters/histogram buckets only; `drain()` returns one bounded immutable batch for worker-side mapper flush.
|
||||
- Gateway startup performs asynchronous instance claim and initial refresh. One admitted task plans, awaits Legacy only on the worker, validates comparability, writes comparison, and refreshes before `READY` can return.
|
||||
- Worker-only warnings contain request id, configured warehouse, snapshot generation, fixed failure stage, exception class, and elapsed time; they exclude credentials, full payloads, and exception messages that may contain business data.
|
||||
|
||||
- [ ] **Step 1: Add failing completion tests for accepted, skipped, failed, and duplicate completion.**
|
||||
|
||||
```java
|
||||
completion.complete("10", "SPACE-1");
|
||||
completion.complete("10", "SPACE-2");
|
||||
verify(onLegacyExit, times(1)).run();
|
||||
|
||||
completion.fail(new IllegalStateException("legacy failed"));
|
||||
assertThatCode(() -> completion.fail(new IllegalStateException("again")))
|
||||
.doesNotThrowAnyException();
|
||||
verify(onLegacyExit, times(1)).run();
|
||||
```
|
||||
|
||||
Add equivalent outbound assertions.
|
||||
|
||||
- [ ] **Step 2: Add failing gateway tests for initial refresh and valid generation flow.**
|
||||
|
||||
```java
|
||||
gateway.start();
|
||||
verify(engine, timeout(1000)).refreshSnapshot(eq(1L), anyString());
|
||||
|
||||
InboundShadowCompletion token = gateway.observeInbound("TASK-1", "TRAY-1");
|
||||
assertThat(token.accepted()).isTrue();
|
||||
token.complete("10", "LEGACY-SPACE");
|
||||
verify(engine, timeout(1000)).planInbound(eq(snapshot), eq("TASK-1"), eq("TRAY-1"));
|
||||
verify(engine, timeout(1000)).compareInbound(any(), eq("10"), eq("LEGACY-SPACE"));
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add failing gateway tests for zero-wait skipping and invalid concurrent samples.**
|
||||
|
||||
Block the worker with latches, submit a second Legacy call, and assert the second token returns before a 1-second test bound, is not accepted, never calls a planner, closes activity on completion, and causes the first observation to be recorded as `SKIP_CONCURRENT` rather than a valid comparison. Test rejected execution, worker timeout, planning error, comparison error, refresh failure, three retries with 1/2/4-second injected backoff, and one-minute recovery coalescing using fake sleeper/clock collaborators rather than real sleeps.
|
||||
|
||||
- [ ] **Step 4: Add failing metric histogram tests.**
|
||||
|
||||
```java
|
||||
metrics.recordAdmission(ShadowSkipReason.BUSY, 750_000L);
|
||||
metrics.recordAdmission(null, 1_500_000L);
|
||||
var batch = metrics.drain();
|
||||
assertThat(batch.count("SKIP_BUSY")).isEqualTo(1);
|
||||
assertThat(batch.bucketCount("ADMISSION_US", 1000)).isEqualTo(1);
|
||||
assertThat(batch.bucketCount("ADMISSION_US", 2500)).isEqualTo(1);
|
||||
assertThat(metrics.drain().totalCount()).isZero();
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run gateway/metrics tests and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -Dtest=ShadowObservationGatewayTest,ShadowRuntimeMetricsTest,ShadowSnapshotCoordinatorTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Implement idempotent tracked completion tokens.**
|
||||
|
||||
```java
|
||||
public void fail(Throwable failure) {
|
||||
finish(new LegacyResult(null, null, failure));
|
||||
}
|
||||
|
||||
private void finish(LegacyResult result) {
|
||||
if (finished.compareAndSet(false, true)) {
|
||||
try { legacyResult.complete(result); }
|
||||
finally { onLegacyExit.run(); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rejected tokens used while shadow is fully disabled remain allocation-free singletons. Enabled-but-skipped calls receive tracked tokens so their Legacy activity exits in `complete`/`fail`.
|
||||
|
||||
- [ ] **Step 7: Implement bounded metrics and worker-only persistence.**
|
||||
|
||||
Use atomic counters and fixed admission/Legacy-duration microsecond buckets `100, 250, 500, 1000, 2500, 5000, 10000, overflow`, extending duration buckets to `25000, 50000, 100000, 250000, 500000, 1000000, overflow`. Add fixed mapper upserts into `alg.shadow_runtime_metric`; no mapper call may occur from `observe*`, `complete`, or `fail`. Completion records Legacy duration using its captured `System.nanoTime()` start without blocking.
|
||||
|
||||
- [ ] **Step 8: Rewrite gateway orchestration around the coordinator.**
|
||||
|
||||
Use a one-thread executor with capacity one and `AbortPolicy`; expose package-private constructor parameters for `Clock`, nested `Sleeper`, executor, and instance id so tests are deterministic. Define `@FunctionalInterface interface Sleeper { void sleep(Duration delay) throws InterruptedException; }` inside the gateway. `start()` only schedules work. `observe*` calls `coordinator.enter`, records elapsed nanos, immediately returns tracked skip on non-admission, and submits accepted work without logging on the caller.
|
||||
|
||||
Inbound worker control flow:
|
||||
|
||||
```java
|
||||
try {
|
||||
InboundShadowObservation observation =
|
||||
engine.planInbound(admission.snapshot(), taskNo, trayNo);
|
||||
InboundShadowCompletion.LegacyResult legacy = completion.awaitLegacyResult();
|
||||
if (coordinator.isComparable(admission)) {
|
||||
engine.compareInbound(observation, legacy.resultCode(), legacy.spaceId());
|
||||
} else {
|
||||
metrics.increment(ShadowSkipReason.CONCURRENT);
|
||||
}
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception shadowFailure) {
|
||||
metrics.recordError(stage(shadowFailure));
|
||||
} finally {
|
||||
coordinator.finishObservation(admission);
|
||||
refreshWithBoundedRecovery();
|
||||
}
|
||||
```
|
||||
|
||||
Use the same control flow for project and exact-stock outbound with `OutboundShadowObservation`, `completion.awaitLegacyResult()`, and `engine.compareOutbound(observation, legacyResult)`. `stage(Throwable)` returns one fixed enum value from the currently executing worker stage; it never includes payload text.
|
||||
|
||||
The actual implementation must keep admission exit owned by caller completion and worker lease cleanup separately idempotent, so worker timeout cannot falsely reduce the active Legacy count. Emit the required structured warning only from this worker catch block.
|
||||
|
||||
- [ ] **Step 9: Run gateway/metrics tests twice and verify GREEN.**
|
||||
|
||||
Run the Step 5 command twice. Expected: deterministic pass with no timing sleeps in tests.
|
||||
|
||||
- [ ] **Step 10: Commit the v3 asynchronous pipeline.**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow ruoyi-admin/src/main/resources/mapper/algorithm/ShadowInputMapper.xml ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow
|
||||
git commit -m "feat: run shadow v3 without legacy waiting"
|
||||
```
|
||||
|
||||
### Task 7: Guarantee Legacy Activity Exit on Every Service Path
|
||||
|
||||
**Files:**
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java`
|
||||
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java`
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java`
|
||||
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: idempotent `complete(...)` and `fail(Throwable)` completion tokens from Task 6.
|
||||
- Guarantees: stored-procedure success, warning, early return, runtime failure, and error paths all close shadow activity without swallowing or replacing Legacy behavior.
|
||||
|
||||
- [ ] **Step 1: Add failing inbound Legacy exception and response-branch tests.**
|
||||
|
||||
```java
|
||||
IllegalStateException legacyFailure = new IllegalStateException("legacy database failure");
|
||||
doThrow(legacyFailure).when(mydoJobMapper).allotInJobSpace(any());
|
||||
|
||||
assertThatThrownBy(() -> service.requestEnterByTaskNo(request)).isSameAs(legacyFailure);
|
||||
verify(shadowCompletion).fail(legacyFailure);
|
||||
verify(mydoJobMapper, times(1)).allotInJobSpace(any());
|
||||
```
|
||||
|
||||
Retain success and warning assertions and verify exactly one `complete` or `fail` call.
|
||||
|
||||
- [ ] **Step 2: Add failing automatic/manual outbound Legacy exception tests.**
|
||||
|
||||
For both `autoAllot` and `manualAllot`, throw the same mapper exception, assert the exact exception escapes unchanged, assert the stored procedure was called once for the affected item, and verify `shadowCompletion.fail(sameException)` exactly once.
|
||||
|
||||
- [ ] **Step 3: Run service tests and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -Dtest=ToWmsServiceImplShadowTest,OutStockoutServiceImplShadowTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Wrap each original Legacy call/result block with failure notification and unchanged rethrow.**
|
||||
|
||||
```java
|
||||
try {
|
||||
mydoJobMapper.allotInJobSpace(map);
|
||||
} catch (RuntimeException | Error legacyFailure) {
|
||||
shadowCompletion.fail(legacyFailure);
|
||||
throw legacyFailure;
|
||||
}
|
||||
```
|
||||
|
||||
Use the same shape around each `autoAllot` and `manualAllot`. Keep normal `complete` at the same post-Legacy point. Remove synchronous shadow warning logs from caller-side submission catches; they return rejected tokens and increment only in-memory metrics inside the gateway.
|
||||
|
||||
- [ ] **Step 5: Run service tests and verify GREEN, including original ordering.**
|
||||
|
||||
Run the Step 3 command. Verify existing order remains lookup → observe → Legacy procedure once → Legacy result/reload → completion.
|
||||
|
||||
- [ ] **Step 6: Commit exact-once Legacy exit handling.**
|
||||
|
||||
```bash
|
||||
git add ruoyi-admin/src/main/java/com/ruoyi/api/service/impl/ToWmsServiceImpl.java ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java ruoyi-admin/src/test/java/com/ruoyi/api/service/impl/ToWmsServiceImplShadowTest.java ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java
|
||||
git commit -m "fix: close shadow activity on legacy failures"
|
||||
```
|
||||
|
||||
### Task 8: Add CMD Deployment, Upgrade, Rollback, and Export Tooling
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/shadow-v3-preflight.cmd`
|
||||
- Create: `scripts/shadow-v3-install.cmd`
|
||||
- Create: `scripts/shadow-v3-uninstall.cmd`
|
||||
- Create: `scripts/shadow-v3-export.cmd`
|
||||
- Create: `scripts/shadow-v3-export.sql`
|
||||
- Modify: `doc/algorithm-shadow/README.md`
|
||||
- Create: `doc/algorithm-shadow/SHADOW_V3_SERVER_AI_EXPORT_PROMPT.md`
|
||||
- Create: `ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- `shadow-v3-preflight.cmd REPO_ROOT EXPECTED_COMMIT SQL_SERVER DATABASE` verifies branch, exact commit, clean worktree, main ancestry, one FT546WMS process maximum, SQL Server connectivity, manifest, and current snapshot state without changing data.
|
||||
- `shadow-v3-install.cmd SQL_SERVER DATABASE upgrade` runs only the recognized v2-to-v3 upgrade; mode `clean` runs the clean v3 installer. Both use Windows authentication, `sqlcmd -b -r1`, and stop on error.
|
||||
- `shadow-v3-uninstall.cmd SQL_SERVER DATABASE CONFIRM_UNINSTALL` requires the literal confirmation and calls only the verified v3 uninstaller.
|
||||
- `shadow-v3-export.cmd REPO_ROOT SQL_SERVER DATABASE START_UTC OUTPUT_DIR LOG_DIR` produces deployment, SQL diagnostics, comparison detail, metric, and warning-log files without database writes.
|
||||
|
||||
- [ ] **Step 1: Add failing operations-package tests for files, arguments, and safety guards.**
|
||||
|
||||
```java
|
||||
assertThat(preflight).contains("git status --porcelain", "git merge-base --is-ancestor", "jps -lv");
|
||||
assertThat(installer).contains("if /I \"%~3\"==\"upgrade\"", "sqlcmd", "-b", "-r1");
|
||||
assertThat(uninstaller).contains("CONFIRM_UNINSTALL");
|
||||
assertThat(exporter).contains("git rev-parse HEAD", "shadow-v3-export.sql");
|
||||
assertThat(exportSql.toLowerCase()).doesNotContain("insert ", "update ", "delete ", "merge ");
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the operations-package test and verify RED.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowV3OperationsPackageTest test
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implement preflight and install/uninstall CMD scripts with quoted paths and error propagation.**
|
||||
|
||||
Every script begins with `@echo off` and `setlocal`, validates non-empty positional arguments, uses `%~dp0` for repository-owned SQL paths, checks `errorlevel`, and exits nonzero on failure. Never use PowerShell. The uninstaller prints that `alg.*` evidence must be exported first and refuses every confirmation other than `CONFIRM_UNINSTALL`.
|
||||
|
||||
Installer command shape:
|
||||
|
||||
```cmd
|
||||
sqlcmd -S "%~1" -d "%~2" -E -b -r1 -i "%SQL_FILE%"
|
||||
if errorlevel 1 exit /b 1
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Implement one read-only SQL export and CMD collector.**
|
||||
|
||||
The SQL accepts sqlcmd variables `WarehouseId` and `StartUtc`, uses only `SELECT`, and outputs manifest, snapshot state/signature, heartbeat, input coverage, runtime metric buckets, valid/skip/error outcome counts, complete comparison rows, inventory parity, and blocking evidence. The CMD collector writes Git branch/commit/status, `jps -lv`, environment variable names with secrets excluded, diagnostics output, and matching application log lines into `OUTPUT_DIR`.
|
||||
|
||||
- [ ] **Step 5: Update the Chinese runbook and server-AI prompt with exact v3 order.**
|
||||
|
||||
Document: export v2 evidence → stop v2 IDEA run → `git fetch`/checkout exact v3 commit → preflight → install `upgrade` → set the two new environment variables → start IDEA once → read-only health checks → seven-day export → disable/restart rollback → optional confirmed uninstall. State explicitly that IDEA restart creates a short planned outage and that normal requests never wait after startup.
|
||||
|
||||
- [ ] **Step 6: Run operations tests and CMD parser checks.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -Dtest=ShadowV3OperationsPackageTest test
|
||||
cmd.exe /d /c "call scripts\shadow-v3-preflight.cmd 2>NUL & if not errorlevel 1 exit /b 9"
|
||||
cmd.exe /d /c "call scripts\shadow-v3-uninstall.cmd 2>NUL & if not errorlevel 1 exit /b 9"
|
||||
```
|
||||
|
||||
Expected: JUnit passes; both no-argument script calls reject input with a nonzero exit code and do not run SQL.
|
||||
|
||||
- [ ] **Step 7: Commit the field operations package.**
|
||||
|
||||
```bash
|
||||
git add scripts/shadow-v3-preflight.cmd scripts/shadow-v3-install.cmd scripts/shadow-v3-uninstall.cmd scripts/shadow-v3-export.cmd scripts/shadow-v3-export.sql doc/algorithm-shadow/README.md doc/algorithm-shadow/SHADOW_V3_SERVER_AI_EXPORT_PROMPT.md ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java
|
||||
git commit -m "docs: add shadow v3 field operations package"
|
||||
```
|
||||
|
||||
### Task 9: Full Verification, Independent Review, and Branch Publication
|
||||
|
||||
**Files:**
|
||||
- Modify only when a verification failure is directly caused by v3 changes; return to the owning task's test-first loop before editing.
|
||||
- Review: every file changed from `3f7ed3718defef2c4ab37e1deb2534d2329747e4` through `HEAD`.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: one clean, pushed branch with all automated evidence passing.
|
||||
- Does not produce: a formal ACTIVE cutover, production database execution, or field deployment.
|
||||
|
||||
- [ ] **Step 1: Verify the exact branch ancestry and worktree scope.**
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
git merge-base --is-ancestor 916ac6bda32a5b102a3181cc8c5dbd5319c5656d HEAD
|
||||
git status --short
|
||||
git diff --check 3f7ed3718defef2c4ab37e1deb2534d2329747e4...HEAD
|
||||
```
|
||||
|
||||
Expected: branch is `test/ft546wms-shadow-v3-20260903`, ancestry exits 0, status is clean, and diff check is empty.
|
||||
|
||||
- [ ] **Step 2: Run all algorithm tests.**
|
||||
|
||||
```bash
|
||||
mvn -pl fortucky-algorithm test
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESS with no failed/error tests.
|
||||
|
||||
- [ ] **Step 3: Run all admin shadow and Legacy integration tests.**
|
||||
|
||||
```bash
|
||||
mvn -pl ruoyi-admin -am -Dtest=ShadowConfigurationTest,ShadowSafetyGuardTest,WarehouseIdNormalizerTest,ShadowSnapshotCoordinatorTest,ShadowPlanningEngineTest,ShadowObservationGatewayTest,ShadowRuntimeMetricsTest,ShadowV3OperationsPackageTest,ToWmsServiceImplShadowTest,OutStockoutServiceImplShadowTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
Expected: BUILD SUCCESS with no failed/error tests.
|
||||
|
||||
- [ ] **Step 4: Run the full reactor tests and package.**
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
mvn -DskipTests package
|
||||
```
|
||||
|
||||
Expected: both commands report BUILD SUCCESS.
|
||||
|
||||
- [ ] **Step 5: Run script syntax/static checks.**
|
||||
|
||||
```bash
|
||||
node --check scripts/shadow-export-mcp.js
|
||||
node --check scripts/shadow-followup-mcp.js
|
||||
git diff --check 3f7ed3718defef2c4ab37e1deb2534d2329747e4...HEAD
|
||||
```
|
||||
|
||||
Expected: no output except normal command headers and exit code 0.
|
||||
|
||||
- [ ] **Step 6: Perform an independent code review against the spec.**
|
||||
|
||||
Invoke `superpowers:requesting-code-review` and require the reviewer to check: Legacy exact-once behavior; no caller-side blocking/I/O/logging; all completion exits; refresh/compare concurrency validity; SQL object/DML allow-list; v2 upgrade rollback; warehouse `01` authority; CMD quoting and destructive confirmation; and missing tests. Resolve every P0/P1 issue and rerun the owning focused tests.
|
||||
|
||||
- [ ] **Step 7: Record the final verification commit without amending earlier task commits.**
|
||||
|
||||
If review corrections were required, commit them with their precise scope. If no tracked files changed, do not create an empty commit.
|
||||
|
||||
```bash
|
||||
git status --short --branch
|
||||
git log --oneline --decorate 3f7ed3718defef2c4ab37e1deb2534d2329747e4..HEAD
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Push the v3 branch only after all evidence is green.**
|
||||
|
||||
```bash
|
||||
git push -u origin test/ft546wms-shadow-v3-20260903
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Hand off the exact field deployment values.**
|
||||
|
||||
Report the pushed commit hash, branch, local test counts, package result, SQL manifest version, and these environment values:
|
||||
|
||||
```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_SHADOW_SNAPSHOT_MAX_AGE_SECONDS=60
|
||||
FORTUCKY_SHADOW_QUIET_INTERVAL_MILLIS=1000
|
||||
```
|
||||
|
||||
State that field execution has not been performed locally, use the v2-to-v3 `upgrade` mode at the current site, and give the exact disable/restart rollback before any optional uninstall instruction.
|
||||
Reference in New Issue
Block a user