refactor: remove WCS tidy task-state recovery

This commit is contained in:
zgw
2026-07-23 15:55:43 +08:00
parent 7b63e7863f
commit 41e412f214
7 changed files with 269 additions and 53 deletions

View File

@@ -0,0 +1,71 @@
package com.ruoyi.algorithm.integration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "fortucky.algorithm")
public class AlgorithmProperties {
private SchedulingMode mode = SchedulingMode.SHADOW;
private String warehouseId = "01";
private int pageSize = 100;
private boolean databaseWritesEnabled = true;
private boolean legacyFallbackOnTechnicalError;
private long tidyAutoScanMs = 600_000L;
private long tidyReconcileMs = 5_000L;
private int tidyClaimLeaseSeconds = 60;
private String tidyWcsSnapshotUrl = "http://localhost:8081/wcs2Wms/api/tidyVehicleSnapshot";
private String replayRunId = "";
private String replayDatabase = "";
public SchedulingMode getMode() {
return mode;
}
public void setMode(SchedulingMode mode) {
this.mode = mode;
}
public String getWarehouseId() {
return warehouseId;
}
public void setWarehouseId(String warehouseId) {
this.warehouseId = warehouseId;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public boolean isDatabaseWritesEnabled() {
return databaseWritesEnabled;
}
public void setDatabaseWritesEnabled(boolean databaseWritesEnabled) {
this.databaseWritesEnabled = databaseWritesEnabled;
}
public boolean isLegacyFallbackOnTechnicalError() {
return legacyFallbackOnTechnicalError;
}
public void setLegacyFallbackOnTechnicalError(boolean legacyFallbackOnTechnicalError) {
this.legacyFallbackOnTechnicalError = legacyFallbackOnTechnicalError;
}
public long getTidyAutoScanMs() { return tidyAutoScanMs; }
public void setTidyAutoScanMs(long value) { this.tidyAutoScanMs = value; }
public long getTidyReconcileMs() { return tidyReconcileMs; }
public void setTidyReconcileMs(long value) { this.tidyReconcileMs = value; }
public int getTidyClaimLeaseSeconds() { return tidyClaimLeaseSeconds; }
public void setTidyClaimLeaseSeconds(int value) { this.tidyClaimLeaseSeconds = value; }
public String getTidyWcsSnapshotUrl() { return tidyWcsSnapshotUrl; }
public void setTidyWcsSnapshotUrl(String value) { this.tidyWcsSnapshotUrl = value; }
public String getReplayRunId() { return replayRunId; }
public void setReplayRunId(String value) { this.replayRunId = value; }
public String getReplayDatabase() { return replayDatabase; }
public void setReplayDatabase(String value) { this.replayDatabase = value; }
}

View File

@@ -6,7 +6,6 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import com.ruoyi.algorithm.wms.tidy.TidyWorkflowState;
import com.ruoyi.algorithm.wms.tidy.TidyRecoveryEvidence;
@Mapper
@@ -59,7 +58,6 @@ public interface WmsAlgorithmMapper {
int pauseTidyWorkflow(@Param("workflowId") String workflowId, @Param("statusReason") String statusReason);
int pauseTidyStep(@Param("workflowId") String workflowId, @Param("stepNo") int stepNo,
@Param("statusReason") String statusReason);
List<TidyWorkflowState> selectActiveTidyWorkflows(@Param("warehouseId") String warehouseId);
List<TidyRecoveryEvidence> selectTidyRecoveryEvidence(@Param("warehouseId") String warehouseId);
TidyModeSnapshot selectTidyMode(@Param("warehouseId") String warehouseId);

View File

@@ -0,0 +1,80 @@
package com.ruoyi.algorithm.wms.tidy;
import com.ruoyi.algorithm.tidy.TidyCandidate;
import com.ruoyi.algorithm.tidy.TidyMove;
import com.ruoyi.algorithm.wms.WmsAlgorithmMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Service
public class TidyWorkflowTransaction implements TidyWorkflowStarter {
private final WmsAlgorithmMapper mapper;
private final ExecutionAdmissionService admission;
public TidyWorkflowTransaction(WmsAlgorithmMapper mapper, ExecutionAdmissionService admission) {
this.mapper = mapper;
this.admission = admission;
}
@Override
@Transactional(rollbackFor = Exception.class)
public UUID startAutomatic(TidyLaneClaim claim, TidyCandidate candidate) {
return start(TidyOrigin.AUTO_TIDY, claim.warehouseId(), candidate, claim);
}
@Transactional(rollbackFor = Exception.class)
public UUID start(TidyOrigin origin, String warehouseId, TidyCandidate candidate) {
return start(origin, warehouseId, candidate, null);
}
private UUID start(TidyOrigin origin, String warehouseId, TidyCandidate candidate, TidyLaneClaim claim) {
admission.assertAllowed(warehouseId, origin == TidyOrigin.AUTO_TIDY
? ExecutionWorkType.AUTO_TIDY : ExecutionWorkType.MANUAL_TIDY);
if (candidate == null || candidate.moves().isEmpty()) {
throw new IllegalArgumentException("TIDY_CANDIDATE_EMPTY");
}
UUID workflowId = UUID.randomUUID();
String laneCode = candidate.releasesLaneCode().orElse("");
String token = claim == null ? UUID.randomUUID().toString() : claim.claimToken().toString();
long dirtyVersion = claim == null ? 0 : claim.dirtyVersion();
mapper.insertTidyWorkflow(workflowId.toString(), warehouseId, laneCode, origin.name(),
candidate.type().name(), candidate.moves().get(0).fromSpaceId(),
candidate.moves().get(candidate.moves().size() - 1).toSpaceId(),
candidate.moves().get(0).trayNo(), "CLAIMED", dirtyVersion, token);
int stepNo = 0;
String dependency = null;
for (TidyMove move : candidate.moves()) {
String stepId = workflowId + "-" + (++stepNo);
mapper.insertTidyStep(stepId, workflowId.toString(), stepNo, move.fromSpaceId(),
move.toSpaceId(), move.trayNo(), "PENDING", dependency);
mapper.lockTidyResource(UUID.randomUUID(), workflowId.toString(), warehouseId,
"SPACE", move.fromSpaceId());
mapper.lockTidyResource(UUID.randomUUID(), workflowId.toString(), warehouseId,
"SPACE", move.toSpaceId());
dependency = stepId;
}
mapper.insertTidyMoveJob(workflowId.toString(), workflowId + "-1");
return workflowId;
}
@Transactional(rollbackFor = Exception.class)
public boolean onJobFinished(String jobId, String callbackStage) {
if (jobId == null || jobId.isBlank()) return false;
String workflowId = mapper.selectTidyWorkflowIdByJob(jobId);
if (workflowId == null) return false;
String stepId = mapper.selectTidyStepIdByJob(jobId);
if (mapper.insertTidyCallbackEventIfAbsent(UUID.randomUUID(), workflowId, stepId,
jobId, callbackStage, "SUCCESS") == 0) return false;
mapper.completeTidyStep(stepId);
String next = mapper.selectNextTidyStepId(workflowId);
if (next == null) {
mapper.completeTidyWorkflow(workflowId);
mapper.releaseTidyResources(workflowId);
} else {
mapper.insertTidyMoveJob(workflowId, next);
}
return true;
}
}

View File

@@ -159,46 +159,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and status in ('PENDING', 'DISPATCHED', 'RUNNING')
</update>
<resultMap id="tidyWorkflowStateMap" type="com.ruoyi.algorithm.wms.tidy.TidyWorkflowState">
<constructor>
<arg column="workflowId" javaType="java.lang.String" />
<arg column="origin" javaType="com.ruoyi.algorithm.wms.tidy.TidyOrigin" />
<arg column="status" javaType="java.lang.String" />
<arg column="laneCode" javaType="java.lang.String" />
<arg column="sourceSpaceId" javaType="java.lang.String" />
<arg column="targetSpaceId" javaType="java.lang.String" />
<arg column="trayCode" javaType="java.lang.String" />
<arg column="wcsTaskId" javaType="java.lang.String" />
<arg column="activeStep" javaType="_int" />
</constructor>
</resultMap>
<select id="selectActiveTidyWorkflows" resultMap="tidyWorkflowStateMap">
select workflow.workflow_id as workflowId,
workflow.origin as origin,
workflow.status as status,
workflow.lane_code as laneCode,
workflow.source_space_id as sourceSpaceId,
workflow.target_space_id as targetSpaceId,
workflow.tray_code as trayCode,
coalesce(workflow.wcs_task_id, job.task_no) as wcsTaskId,
active_step.step_no as activeStep
from alg.tidy_workflow workflow
cross apply (
select top (1) step.step_no, step.step_id
from alg.tidy_step step
where step.workflow_id = workflow.workflow_id
and step.status in ('PENDING', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY')
order by step.step_no
) active_step
left join dbo.mydo_job job
on job.job_id = concat('TDY-', workflow.workflow_id, '-', active_step.step_id)
where workflow.warehouse_id = #{warehouseId}
and workflow.status in (
'CLAIMED', 'WAITING_DISPATCH', 'DISPATCHED', 'RUNNING', 'PAUSED_RECOVERY'
)
</select>
<sql id="tidyModeProjection">
mode.warehouse_id as warehouseId,
mode.requested_enabled as requestedEnabled,

View File

@@ -161,17 +161,6 @@ class WmsAlgorithmMapperWorkflowSqlTest {
.doesNotContain("foreach");
}
@Test
void activeTidyWorkflowUsesAnExplicitDatabaseShapedConstructorMapping() throws IOException {
assertThat(statement("resultmap", "tidyWorkflowStateMap"))
.contains("<constructor>")
.contains("column=\"workflowid\" javatype=\"java.lang.string\"")
.contains("column=\"origin\" javatype=\"com.ruoyi.algorithm.wms.tidy.tidyorigin\"")
.contains("column=\"activestep\" javatype=\"_int\"");
assertThat(statement("select", "selectActiveTidyWorkflows"))
.contains("resultmap=\"tidyworkflowstatemap\"");
}
@Test
void tidyWorkflowCompletionDoesNotRewriteTerminalRows() throws IOException {
assertThat(statement("update", "completeTidyWorkflow"))

View File

@@ -0,0 +1,83 @@
package com.ruoyi.algorithm.wms.tidy;
import com.ruoyi.algorithm.tidy.TidyCandidate;
import com.ruoyi.algorithm.tidy.TidyCandidateType;
import com.ruoyi.algorithm.tidy.TidyMove;
import com.ruoyi.algorithm.wms.WmsAlgorithmMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TidyWorkflowTransactionTest {
@Test
void startsOrderedMovesAsSingleWorkflowWithFirstMoveJob() {
WmsAlgorithmMapper mapper = mock(WmsAlgorithmMapper.class);
ExecutionAdmissionService admission = mock(ExecutionAdmissionService.class);
TidyWorkflowTransaction tx = new TidyWorkflowTransaction(mapper, admission);
UUID workflow = tx.start(TidyOrigin.MANUAL_TIDY, "01", candidate());
assertThat(workflow).isNotNull();
verify(admission).assertAllowed("01", ExecutionWorkType.MANUAL_TIDY);
verify(mapper).insertTidyWorkflow(any(), eq("01"), any(), eq("MANUAL_TIDY"),
eq("CONSOLIDATE_INCUBATION"), eq("SPACE-A"), eq("SPACE-C"), eq("TRAY-1"),
eq("CLAIMED"), eq(0L), anyString());
verify(mapper).insertTidyStep(any(), any(), eq(1), eq("SPACE-A"), eq("SPACE-B"),
eq("TRAY-1"), eq("PENDING"), eq(null));
verify(mapper).insertTidyStep(any(), any(), eq(2), eq("SPACE-B"), eq("SPACE-C"),
eq("TRAY-2"), eq("PENDING"), any());
verify(mapper).insertTidyMoveJob(any(), any());
}
@Test
void duplicateCallbackDoesNotCompleteOrDispatchAgain() {
WmsAlgorithmMapper mapper = mock(WmsAlgorithmMapper.class);
ExecutionAdmissionService admission = mock(ExecutionAdmissionService.class);
TidyWorkflowTransaction tx = new TidyWorkflowTransaction(mapper, admission);
when(mapper.selectTidyWorkflowIdByJob("JOB-1")).thenReturn("WF-1");
when(mapper.selectTidyStepIdByJob("JOB-1")).thenReturn("STEP-1");
when(mapper.insertTidyCallbackEventIfAbsent(any(), eq("WF-1"), eq("STEP-1"),
eq("JOB-1"), eq("FINISHED"), eq("SUCCESS"))).thenReturn(0);
assertThat(tx.onJobFinished("JOB-1", "FINISHED")).isFalse();
verify(mapper, never()).completeTidyStep(any());
verify(mapper, never()).insertTidyMoveJob(any(), any());
}
@Test
void recoveryFinishedUsesTheNormalIdempotentAdvancementPath() {
WmsAlgorithmMapper mapper = mock(WmsAlgorithmMapper.class);
ExecutionAdmissionService admission = mock(ExecutionAdmissionService.class);
TidyWorkflowTransaction tx = new TidyWorkflowTransaction(mapper, admission);
when(mapper.selectTidyWorkflowIdByJob("JOB-1")).thenReturn("WF-1");
when(mapper.selectTidyStepIdByJob("JOB-1")).thenReturn("STEP-1");
when(mapper.insertTidyCallbackEventIfAbsent(any(), eq("WF-1"), eq("STEP-1"),
eq("JOB-1"), eq("RECOVERY_FINISHED"), eq("SUCCESS"))).thenReturn(1);
when(mapper.selectNextTidyStepId("WF-1")).thenReturn(null);
assertThat(tx.onJobFinished("JOB-1", "RECOVERY_FINISHED")).isTrue();
verify(mapper).completeTidyStep("STEP-1");
verify(mapper).completeTidyWorkflow("WF-1");
verify(mapper).releaseTidyResources("WF-1");
verify(mapper, never()).pauseTidyWorkflow(anyString(), anyString());
}
private TidyCandidate candidate() {
return new TidyCandidate(TidyCandidateType.CONSOLIDATE_INCUBATION, "P-1",
List.of(new TidyMove("TRAY-1", "SPACE-A", "SPACE-B"),
new TidyMove("TRAY-2", "SPACE-B", "SPACE-C")),
Optional.of("LANE-1"), "test");
}
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.algorithm.wms.tidy;
import com.ruoyi.algorithm.integration.AlgorithmProperties;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class WcsTaskStateDependencyRemovalTest {
@Test
void taskStateClassesPropertiesAndConfigurationAreAbsent() throws Exception {
assertThatThrownBy(() -> Class.forName(
"com.ruoyi.algorithm.wms.tidy.WcsTaskStateClient"))
.isInstanceOf(ClassNotFoundException.class);
assertThatThrownBy(() -> Class.forName(
"com.ruoyi.algorithm.wms.tidy.WcsTaskState"))
.isInstanceOf(ClassNotFoundException.class);
assertThat(Arrays.stream(AlgorithmProperties.class.getMethods())
.map(Method::getName))
.noneMatch(name -> name.contains("TidyWcsTaskState"));
try (InputStream stream = getClass().getResourceAsStream("/application.yml")) {
assertThat(stream).isNotNull();
String yaml = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
assertThat(yaml).doesNotContain("tidy-wcs-task-state");
assertThat(yaml).contains("tidy-wcs-snapshot-url");
}
}
}