feat(wcs): persist and query execution recovery state
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class DeviceOperationsView {
|
||||||
|
private final String deviceId;
|
||||||
|
private final String deviceType;
|
||||||
|
private final String currentCommandKey;
|
||||||
|
private final String commandState;
|
||||||
|
private final String currentSite;
|
||||||
|
private final boolean connected;
|
||||||
|
private final String activeAlarmCode;
|
||||||
|
private final String recoveryCaseId;
|
||||||
|
private final Instant updatedAt;
|
||||||
|
|
||||||
|
public DeviceOperationsView(String deviceId, String deviceType,
|
||||||
|
String currentCommandKey, String commandState,
|
||||||
|
String currentSite, boolean connected,
|
||||||
|
String activeAlarmCode, String recoveryCaseId,
|
||||||
|
Instant updatedAt) {
|
||||||
|
this.deviceId = requireText(deviceId, "deviceId");
|
||||||
|
this.deviceType = requireText(deviceType, "deviceType");
|
||||||
|
this.currentCommandKey = optional(currentCommandKey);
|
||||||
|
this.commandState = optional(commandState);
|
||||||
|
this.currentSite = optional(currentSite);
|
||||||
|
this.connected = connected;
|
||||||
|
this.activeAlarmCode = optional(activeAlarmCode);
|
||||||
|
this.recoveryCaseId = optional(recoveryCaseId);
|
||||||
|
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDeviceId() { return deviceId; }
|
||||||
|
public String getDeviceType() { return deviceType; }
|
||||||
|
public String getCurrentCommandKey() { return currentCommandKey; }
|
||||||
|
public String getCommandState() { return commandState; }
|
||||||
|
public String getCurrentSite() { return currentSite; }
|
||||||
|
public boolean isConnected() { return connected; }
|
||||||
|
public String getActiveAlarmCode() { return activeAlarmCode; }
|
||||||
|
public String getRecoveryCaseId() { return recoveryCaseId; }
|
||||||
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
|
|
||||||
|
private static String optional(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
String text = optional(value);
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class OrderExecutionView {
|
||||||
|
private final String orderId;
|
||||||
|
private final String workflowId;
|
||||||
|
private final String stepId;
|
||||||
|
private final String deviceId;
|
||||||
|
private final String currentCommandKey;
|
||||||
|
private final String commandState;
|
||||||
|
private final String recoveryCaseId;
|
||||||
|
private final Instant updatedAt;
|
||||||
|
|
||||||
|
public OrderExecutionView(String orderId, String workflowId, String stepId,
|
||||||
|
String deviceId, String currentCommandKey,
|
||||||
|
String commandState, String recoveryCaseId,
|
||||||
|
Instant updatedAt) {
|
||||||
|
this.orderId = requireText(orderId, "orderId");
|
||||||
|
this.workflowId = optional(workflowId);
|
||||||
|
this.stepId = optional(stepId);
|
||||||
|
this.deviceId = optional(deviceId);
|
||||||
|
this.currentCommandKey = optional(currentCommandKey);
|
||||||
|
this.commandState = optional(commandState);
|
||||||
|
this.recoveryCaseId = optional(recoveryCaseId);
|
||||||
|
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOrderId() { return orderId; }
|
||||||
|
public String getWorkflowId() { return workflowId; }
|
||||||
|
public String getStepId() { return stepId; }
|
||||||
|
public String getDeviceId() { return deviceId; }
|
||||||
|
public String getCurrentCommandKey() { return currentCommandKey; }
|
||||||
|
public String getCommandState() { return commandState; }
|
||||||
|
public String getRecoveryCaseId() { return recoveryCaseId; }
|
||||||
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
|
|
||||||
|
private static String optional(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
String text = optional(value);
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class RecoveryCaseView {
|
||||||
|
private final String caseId;
|
||||||
|
private final String deviceId;
|
||||||
|
private final String commandKey;
|
||||||
|
private final String state;
|
||||||
|
private final String reason;
|
||||||
|
private final Instant createdAt;
|
||||||
|
private final String resolvedBy;
|
||||||
|
private final Instant resolvedAt;
|
||||||
|
|
||||||
|
public RecoveryCaseView(String caseId, String deviceId, String commandKey,
|
||||||
|
String state, String reason, Instant createdAt,
|
||||||
|
String resolvedBy, Instant resolvedAt) {
|
||||||
|
this.caseId = requireText(caseId, "caseId");
|
||||||
|
this.deviceId = requireText(deviceId, "deviceId");
|
||||||
|
this.commandKey = requireText(commandKey, "commandKey");
|
||||||
|
this.state = requireText(state, "state");
|
||||||
|
this.reason = requireText(reason, "reason");
|
||||||
|
this.createdAt = Objects.requireNonNull(createdAt, "createdAt");
|
||||||
|
this.resolvedBy = optional(resolvedBy);
|
||||||
|
this.resolvedAt = resolvedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCaseId() { return caseId; }
|
||||||
|
public String getDeviceId() { return deviceId; }
|
||||||
|
public String getCommandKey() { return commandKey; }
|
||||||
|
public String getState() { return state; }
|
||||||
|
public String getReason() { return reason; }
|
||||||
|
public Instant getCreatedAt() { return createdAt; }
|
||||||
|
public String getResolvedBy() { return resolvedBy; }
|
||||||
|
public Instant getResolvedAt() { return resolvedAt; }
|
||||||
|
|
||||||
|
private static String optional(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
String text = optional(value);
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class ResourceOperationsView {
|
||||||
|
private final String resourceKey;
|
||||||
|
private final String reservationState;
|
||||||
|
private final String orderId;
|
||||||
|
private final String stepId;
|
||||||
|
private final String vehicleId;
|
||||||
|
private final long fencingToken;
|
||||||
|
private final String releaseEvidenceId;
|
||||||
|
private final Instant updatedAt;
|
||||||
|
|
||||||
|
public ResourceOperationsView(String resourceKey, String reservationState,
|
||||||
|
String orderId, String stepId,
|
||||||
|
String vehicleId, long fencingToken,
|
||||||
|
String releaseEvidenceId, Instant updatedAt) {
|
||||||
|
this.resourceKey = requireText(resourceKey, "resourceKey");
|
||||||
|
this.reservationState = requireText(reservationState,
|
||||||
|
"reservationState");
|
||||||
|
this.orderId = requireText(orderId, "orderId");
|
||||||
|
this.stepId = requireText(stepId, "stepId");
|
||||||
|
this.vehicleId = requireText(vehicleId, "vehicleId");
|
||||||
|
if (fencingToken <= 0L) {
|
||||||
|
throw new IllegalArgumentException("fencingToken must be positive");
|
||||||
|
}
|
||||||
|
this.fencingToken = fencingToken;
|
||||||
|
this.releaseEvidenceId = optional(releaseEvidenceId);
|
||||||
|
this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResourceKey() { return resourceKey; }
|
||||||
|
public String getReservationState() { return reservationState; }
|
||||||
|
public String getOrderId() { return orderId; }
|
||||||
|
public String getStepId() { return stepId; }
|
||||||
|
public String getVehicleId() { return vehicleId; }
|
||||||
|
public long getFencingToken() { return fencingToken; }
|
||||||
|
public String getReleaseEvidenceId() { return releaseEvidenceId; }
|
||||||
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
|
|
||||||
|
private static String optional(String value) {
|
||||||
|
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
String text = optional(value);
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class WcsOperationsQueryService {
|
||||||
|
private static final int MAX_PAGE_SIZE = 500;
|
||||||
|
|
||||||
|
private final WcsOperationsReadRepository repository;
|
||||||
|
|
||||||
|
public WcsOperationsQueryService(WcsOperationsReadRepository repository) {
|
||||||
|
this.repository = Objects.requireNonNull(repository, "repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrderExecutionView order(String orderId) {
|
||||||
|
String id = requireText(orderId, "orderId");
|
||||||
|
return requireResult(repository.findOrder(id), "order", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrderExecutionView command(String commandKey) {
|
||||||
|
String key = requireText(commandKey, "commandKey");
|
||||||
|
return requireResult(repository.findCommand(key), "command", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceOperationsView device(String deviceId) {
|
||||||
|
String id = requireText(deviceId, "deviceId");
|
||||||
|
return requireResult(repository.findDevice(id), "device", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceOperationsView resource(String resourceKey) {
|
||||||
|
String key = requireText(resourceKey, "resourceKey");
|
||||||
|
return requireResult(repository.findResource(key), "resource", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecoveryCaseView recoveryCase(String caseId) {
|
||||||
|
String id = requireText(caseId, "caseId");
|
||||||
|
return requireResult(repository.findRecoveryCase(id),
|
||||||
|
"recovery case", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RecoveryCaseView> recoveryCases(String state, int offset,
|
||||||
|
int limit) {
|
||||||
|
String recoveryState = requireText(state, "state");
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IllegalArgumentException("offset must not be negative");
|
||||||
|
}
|
||||||
|
if (limit <= 0 || limit > MAX_PAGE_SIZE) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"limit must be between 1 and " + MAX_PAGE_SIZE);
|
||||||
|
}
|
||||||
|
List<RecoveryCaseView> views = Objects.requireNonNull(
|
||||||
|
repository.findRecoveryCases(recoveryState, offset, limit),
|
||||||
|
"recovery case page");
|
||||||
|
return Collections.unmodifiableList(
|
||||||
|
new ArrayList<RecoveryCaseView>(views));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T requireResult(T value, String type, String key) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new DomainRuleViolation(type + " not found: " + key);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String requireText(String value, String field) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(field + " must not be blank");
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface WcsOperationsReadRepository {
|
||||||
|
OrderExecutionView findOrder(String orderId);
|
||||||
|
|
||||||
|
OrderExecutionView findCommand(String commandKey);
|
||||||
|
|
||||||
|
DeviceOperationsView findDevice(String deviceId);
|
||||||
|
|
||||||
|
ResourceOperationsView findResource(String resourceKey);
|
||||||
|
|
||||||
|
RecoveryCaseView findRecoveryCase(String caseId);
|
||||||
|
|
||||||
|
List<RecoveryCaseView> findRecoveryCases(String state, int offset,
|
||||||
|
int limit);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ruoyi.wcs.infrastructure.persistence.recovery;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class ExecutionRecoveryMigrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packagesExecutionRecoveryTablesAndIdempotencyIndexes() throws Exception {
|
||||||
|
String migration = readSql("V003__wcs_execution_and_recovery.sql");
|
||||||
|
|
||||||
|
assertContains(migration, "wcs_device_command");
|
||||||
|
assertContains(migration, "wcs_device_execution");
|
||||||
|
assertContains(migration, "wcs_command_outbox");
|
||||||
|
assertContains(migration, "wcs_wms_notification_outbox");
|
||||||
|
assertContains(migration, "wcs_device_snapshot");
|
||||||
|
assertContains(migration, "wcs_device_event");
|
||||||
|
assertContains(migration, "wcs_alarm");
|
||||||
|
assertContains(migration, "wcs_recovery_case");
|
||||||
|
assertContains(migration, "wcs_manual_action_audit");
|
||||||
|
assertContains(migration, "wcs_control_lease");
|
||||||
|
assertContains(migration, "uk_wcs_recovery_case_open");
|
||||||
|
assertContains(migration, "WHERE recovery_state = 'OPEN'");
|
||||||
|
assertContains(migration, "uk_wcs_wms_outbox_event");
|
||||||
|
assertContains(migration, "ix_wcs_device_command_order_step");
|
||||||
|
assertContains(migration, "ix_wcs_resource_reservation_resource_state");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void packagesTheExecutionRecoveryVerificationScript() throws Exception {
|
||||||
|
String verification = readSql(
|
||||||
|
"verify_V003__wcs_execution_and_recovery.sql");
|
||||||
|
|
||||||
|
assertContains(verification, "wcs_device_command");
|
||||||
|
assertContains(verification, "uk_wcs_recovery_case_open");
|
||||||
|
assertContains(verification, "uk_wcs_wms_outbox_event");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertContains(String sql, String token) {
|
||||||
|
assertTrue(sql.contains(token), "missing SQL token: " + token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readSql(String filename) throws Exception {
|
||||||
|
Path current = Paths.get(System.getProperty(
|
||||||
|
"maven.multiModuleProjectDirectory", "."))
|
||||||
|
.toAbsolutePath().normalize();
|
||||||
|
while (current != null) {
|
||||||
|
Path file = current.resolve(Paths.get("sql", "wcs", filename));
|
||||||
|
if (Files.isRegularFile(file)) {
|
||||||
|
return new String(Files.readAllBytes(file),
|
||||||
|
StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
current = current.getParent();
|
||||||
|
}
|
||||||
|
throw new java.nio.file.NoSuchFileException(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.ruoyi.wcs.query;
|
||||||
|
|
||||||
|
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class WcsOperationsQueryServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsCommandAndRecoveryCorrelationForAnOrder() {
|
||||||
|
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||||
|
new FakeReadRepository());
|
||||||
|
|
||||||
|
OrderExecutionView view = queries.order("ORDER-1");
|
||||||
|
|
||||||
|
assertEquals("CMD-1", view.getCurrentCommandKey());
|
||||||
|
assertEquals("RC-1", view.getRecoveryCaseId());
|
||||||
|
assertEquals("UNKNOWN", view.getCommandState());
|
||||||
|
assertEquals("VEH-1", view.getDeviceId());
|
||||||
|
assertEquals("ORDER-1", queries.command("CMD-1").getOrderId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesStableDeviceResourceAndRecoveryViews() {
|
||||||
|
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||||
|
new FakeReadRepository());
|
||||||
|
|
||||||
|
assertEquals("ALARM-1", queries.device("VEH-1").getActiveAlarmCode());
|
||||||
|
assertEquals(77L, queries.resource("NODE:A").getFencingToken());
|
||||||
|
assertEquals("TASK_NUMBER_MISMATCH",
|
||||||
|
queries.recoveryCase("RC-1").getReason());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pagesRecoveryCasesByExplicitStateWithABoundedLimit() {
|
||||||
|
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||||
|
new FakeReadRepository());
|
||||||
|
|
||||||
|
List<RecoveryCaseView> page = queries.recoveryCases(
|
||||||
|
"OPEN", 0, 50);
|
||||||
|
|
||||||
|
assertEquals(Arrays.asList("RC-1", "RC-2"), Arrays.asList(
|
||||||
|
page.get(0).getCaseId(), page.get(1).getCaseId()));
|
||||||
|
assertThrows(UnsupportedOperationException.class,
|
||||||
|
() -> page.add(page.get(0)));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> queries.recoveryCases("OPEN", 0, 501));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingOrBlankOperationsLookupFailsExplicitly() {
|
||||||
|
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||||
|
new FakeReadRepository());
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> queries.order(" "));
|
||||||
|
assertThrows(DomainRuleViolation.class,
|
||||||
|
() -> queries.order("ORDER-MISSING"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FakeReadRepository
|
||||||
|
implements WcsOperationsReadRepository {
|
||||||
|
@Override
|
||||||
|
public OrderExecutionView findOrder(String orderId) {
|
||||||
|
if (!"ORDER-1".equals(orderId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new OrderExecutionView("ORDER-1", "WF-1", "STEP-1",
|
||||||
|
"VEH-1", "CMD-1", "UNKNOWN", "RC-1", Instant.EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OrderExecutionView findCommand(String commandKey) {
|
||||||
|
return "CMD-1".equals(commandKey) ? findOrder("ORDER-1") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceOperationsView findDevice(String deviceId) {
|
||||||
|
return new DeviceOperationsView("VEH-1", "RGV", "CMD-1",
|
||||||
|
"UNKNOWN", "NODE:A", true, "ALARM-1", "RC-1",
|
||||||
|
Instant.EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceOperationsView findResource(String resourceKey) {
|
||||||
|
return new ResourceOperationsView("NODE:A", "OCCUPIED",
|
||||||
|
"ORDER-1", "STEP-1", "VEH-1", 77L, null,
|
||||||
|
Instant.EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecoveryCaseView findRecoveryCase(String caseId) {
|
||||||
|
return recovery(caseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<RecoveryCaseView> findRecoveryCases(String state,
|
||||||
|
int offset,
|
||||||
|
int limit) {
|
||||||
|
return Arrays.asList(recovery("RC-1"), recovery("RC-2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RecoveryCaseView recovery(String caseId) {
|
||||||
|
return new RecoveryCaseView(caseId, "VEH-1", "CMD-1", "OPEN",
|
||||||
|
"TASK_NUMBER_MISMATCH", Instant.EPOCH, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
341
sql/wcs/V003__wcs_execution_and_recovery.sql
Normal file
341
sql/wcs/V003__wcs_execution_and_recovery.sql
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
SET XACT_ABORT ON;
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_control_lease', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_control_lease (
|
||||||
|
lease_name varchar(64) NOT NULL PRIMARY KEY,
|
||||||
|
owner_id varchar(128) NULL,
|
||||||
|
fencing_token bigint NOT NULL,
|
||||||
|
lease_until datetime2(3) NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT ck_wcs_control_lease_fencing CHECK (fencing_token >= 0),
|
||||||
|
CONSTRAINT ck_wcs_control_lease_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_device_command', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_device_command (
|
||||||
|
command_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
order_id varchar(64) NULL,
|
||||||
|
step_id varchar(128) NULL,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
command_type varchar(64) NOT NULL,
|
||||||
|
payload_json nvarchar(max) NOT NULL,
|
||||||
|
transport_attempt int NOT NULL,
|
||||||
|
command_state varchar(32) NOT NULL,
|
||||||
|
send_receipt_id varchar(128) NULL,
|
||||||
|
acknowledge_evidence_id varchar(128) NULL,
|
||||||
|
completion_evidence_id varchar(128) NULL,
|
||||||
|
status_reason nvarchar(512) NULL,
|
||||||
|
created_time datetime2(3) NOT NULL,
|
||||||
|
updated_time datetime2(3) NOT NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT uk_wcs_device_command_key UNIQUE (command_key),
|
||||||
|
CONSTRAINT ck_wcs_device_command_attempt CHECK (transport_attempt >= 0),
|
||||||
|
CONSTRAINT ck_wcs_device_command_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_device_command')
|
||||||
|
AND name = 'ix_wcs_device_command_device_state')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_device_command_device_state
|
||||||
|
ON dbo.wcs_device_command(device_id, command_state);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_device_command')
|
||||||
|
AND name = 'ix_wcs_device_command_order_step')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_device_command_order_step
|
||||||
|
ON dbo.wcs_device_command(order_id, step_id);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_device_execution', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_device_execution (
|
||||||
|
command_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
task_number varchar(128) NOT NULL,
|
||||||
|
route_nodes_json nvarchar(max) NOT NULL,
|
||||||
|
route_resources_json nvarchar(max) NOT NULL,
|
||||||
|
topology_version bigint NOT NULL,
|
||||||
|
fencing_token bigint NOT NULL,
|
||||||
|
expected_boot_id varchar(128) NOT NULL,
|
||||||
|
current_node_index int NOT NULL,
|
||||||
|
released_resource_count int NOT NULL,
|
||||||
|
last_device_sequence bigint NOT NULL,
|
||||||
|
active_flag bit NOT NULL
|
||||||
|
CONSTRAINT df_wcs_device_execution_active DEFAULT (1),
|
||||||
|
updated_time datetime2(3) NOT NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT fk_wcs_device_execution_command FOREIGN KEY (command_id)
|
||||||
|
REFERENCES dbo.wcs_device_command(command_id),
|
||||||
|
CONSTRAINT uk_wcs_device_execution_key UNIQUE (command_key),
|
||||||
|
CONSTRAINT ck_wcs_device_execution_topology CHECK (topology_version > 0),
|
||||||
|
CONSTRAINT ck_wcs_device_execution_fencing CHECK (fencing_token > 0),
|
||||||
|
CONSTRAINT ck_wcs_device_execution_indexes CHECK (
|
||||||
|
current_node_index >= 0 AND released_resource_count >= 0),
|
||||||
|
CONSTRAINT ck_wcs_device_execution_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_device_execution')
|
||||||
|
AND name = 'uk_wcs_device_execution_active_device')
|
||||||
|
BEGIN
|
||||||
|
CREATE UNIQUE INDEX uk_wcs_device_execution_active_device
|
||||||
|
ON dbo.wcs_device_execution(device_id)
|
||||||
|
WHERE active_flag = 1;
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_command_outbox', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_command_outbox (
|
||||||
|
command_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
payload_json nvarchar(max) NOT NULL,
|
||||||
|
topology_version bigint NOT NULL,
|
||||||
|
fencing_token bigint NOT NULL,
|
||||||
|
delivery_state varchar(32) NOT NULL,
|
||||||
|
attempt_count int NOT NULL,
|
||||||
|
next_attempt_time datetime2(3) NOT NULL,
|
||||||
|
claimed_by varchar(128) NULL,
|
||||||
|
claimed_time datetime2(3) NULL,
|
||||||
|
last_error nvarchar(1024) NULL,
|
||||||
|
delivered_time datetime2(3) NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT fk_wcs_command_outbox_command FOREIGN KEY (command_id)
|
||||||
|
REFERENCES dbo.wcs_device_command(command_id),
|
||||||
|
CONSTRAINT uk_wcs_command_outbox_key UNIQUE (command_key),
|
||||||
|
CONSTRAINT ck_wcs_command_outbox_attempt CHECK (attempt_count >= 0),
|
||||||
|
CONSTRAINT ck_wcs_command_outbox_topology CHECK (topology_version > 0),
|
||||||
|
CONSTRAINT ck_wcs_command_outbox_fencing CHECK (fencing_token > 0),
|
||||||
|
CONSTRAINT ck_wcs_command_outbox_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_command_outbox')
|
||||||
|
AND name = 'ix_wcs_command_outbox_due')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_command_outbox_due
|
||||||
|
ON dbo.wcs_command_outbox(delivery_state, next_attempt_time);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_wms_notification_outbox', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_wms_notification_outbox (
|
||||||
|
outbox_id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
|
||||||
|
event_id varchar(128) NOT NULL,
|
||||||
|
notification_type varchar(64) NOT NULL,
|
||||||
|
payload_json nvarchar(max) NOT NULL,
|
||||||
|
delivery_state varchar(32) NOT NULL,
|
||||||
|
attempt_count int NOT NULL,
|
||||||
|
next_attempt_time datetime2(3) NOT NULL,
|
||||||
|
claimed_by varchar(128) NULL,
|
||||||
|
claimed_time datetime2(3) NULL,
|
||||||
|
external_receipt varchar(256) NULL,
|
||||||
|
last_error nvarchar(1024) NULL,
|
||||||
|
created_time datetime2(3) NOT NULL,
|
||||||
|
delivered_time datetime2(3) NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT uk_wcs_wms_outbox_event UNIQUE (event_id),
|
||||||
|
CONSTRAINT ck_wcs_wms_outbox_attempt CHECK (attempt_count >= 0),
|
||||||
|
CONSTRAINT ck_wcs_wms_outbox_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_wms_notification_outbox')
|
||||||
|
AND name = 'ix_wcs_wms_outbox_due')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_wms_outbox_due
|
||||||
|
ON dbo.wcs_wms_notification_outbox(
|
||||||
|
delivery_state, next_attempt_time);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_device_snapshot', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_device_snapshot (
|
||||||
|
snapshot_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
task_number varchar(128) NOT NULL,
|
||||||
|
device_sequence bigint NOT NULL,
|
||||||
|
observed_time datetime2(3) NOT NULL,
|
||||||
|
connected_flag bit NOT NULL,
|
||||||
|
device_boot_id varchar(128) NOT NULL,
|
||||||
|
topology_version bigint NOT NULL,
|
||||||
|
fencing_token bigint NOT NULL,
|
||||||
|
current_node_id varchar(128) NOT NULL,
|
||||||
|
remaining_route_json nvarchar(max) NOT NULL,
|
||||||
|
command_completed_flag bit NOT NULL,
|
||||||
|
facts_json nvarchar(max) NULL,
|
||||||
|
received_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT ck_wcs_device_snapshot_sequence CHECK (device_sequence >= 0),
|
||||||
|
CONSTRAINT ck_wcs_device_snapshot_topology CHECK (topology_version > 0),
|
||||||
|
CONSTRAINT ck_wcs_device_snapshot_fencing CHECK (fencing_token > 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_device_snapshot')
|
||||||
|
AND name = 'ix_wcs_device_snapshot_device_time')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_device_snapshot_device_time
|
||||||
|
ON dbo.wcs_device_snapshot(device_id, observed_time DESC);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_device_event', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_device_event (
|
||||||
|
event_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
task_number varchar(128) NOT NULL,
|
||||||
|
device_sequence bigint NOT NULL,
|
||||||
|
event_type varchar(32) NOT NULL,
|
||||||
|
current_node_id varchar(128) NULL,
|
||||||
|
remaining_route_json nvarchar(max) NULL,
|
||||||
|
facts_json nvarchar(max) NULL,
|
||||||
|
device_time datetime2(3) NOT NULL,
|
||||||
|
received_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT ck_wcs_device_event_sequence CHECK (device_sequence >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_device_event')
|
||||||
|
AND name = 'ix_wcs_device_event_device_sequence')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_device_event_device_sequence
|
||||||
|
ON dbo.wcs_device_event(device_id, device_sequence);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_alarm', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_alarm (
|
||||||
|
alarm_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
device_id varchar(64) NULL,
|
||||||
|
command_key varchar(128) NULL,
|
||||||
|
recovery_case_id varchar(128) NULL,
|
||||||
|
alarm_code varchar(128) NOT NULL,
|
||||||
|
severity varchar(16) NOT NULL,
|
||||||
|
alarm_state varchar(32) NOT NULL,
|
||||||
|
detail nvarchar(1024) NULL,
|
||||||
|
source_event_id varchar(128) NULL,
|
||||||
|
opened_time datetime2(3) NOT NULL,
|
||||||
|
acknowledged_by varchar(128) NULL,
|
||||||
|
acknowledged_time datetime2(3) NULL,
|
||||||
|
closed_time datetime2(3) NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT ck_wcs_alarm_version CHECK (state_version >= 0)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_alarm')
|
||||||
|
AND name = 'ix_wcs_alarm_device_state')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_alarm_device_state
|
||||||
|
ON dbo.wcs_alarm(device_id, alarm_state, opened_time DESC);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_recovery_case', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_recovery_case (
|
||||||
|
case_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
device_id varchar(64) NOT NULL,
|
||||||
|
command_key varchar(128) NOT NULL,
|
||||||
|
reason_code varchar(128) NOT NULL,
|
||||||
|
evidence_ids_json nvarchar(max) NOT NULL,
|
||||||
|
expected_task_number varchar(128) NULL,
|
||||||
|
expected_site varchar(128) NULL,
|
||||||
|
expected_load_state varchar(32) NULL,
|
||||||
|
recovery_state varchar(32) NOT NULL,
|
||||||
|
created_time datetime2(3) NOT NULL,
|
||||||
|
resolved_by varchar(128) NULL,
|
||||||
|
resolved_time datetime2(3) NULL,
|
||||||
|
resolution_evidence_json nvarchar(max) NULL,
|
||||||
|
resolution_reason nvarchar(512) NULL,
|
||||||
|
state_version bigint NOT NULL,
|
||||||
|
CONSTRAINT ck_wcs_recovery_case_version CHECK (state_version >= 0),
|
||||||
|
CONSTRAINT ck_wcs_recovery_case_expected_facts CHECK (
|
||||||
|
(expected_task_number IS NULL AND expected_site IS NULL
|
||||||
|
AND expected_load_state IS NULL)
|
||||||
|
OR
|
||||||
|
(expected_task_number IS NOT NULL AND expected_site IS NOT NULL
|
||||||
|
AND expected_load_state IS NOT NULL))
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_recovery_case')
|
||||||
|
AND name = 'uk_wcs_recovery_case_open')
|
||||||
|
BEGIN
|
||||||
|
CREATE UNIQUE INDEX uk_wcs_recovery_case_open
|
||||||
|
ON dbo.wcs_recovery_case(device_id, command_key)
|
||||||
|
WHERE recovery_state = 'OPEN';
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_recovery_case')
|
||||||
|
AND name = 'ix_wcs_recovery_case_state_created')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_recovery_case_state_created
|
||||||
|
ON dbo.wcs_recovery_case(recovery_state, created_time);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_manual_action_audit', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.wcs_manual_action_audit (
|
||||||
|
audit_id varchar(128) NOT NULL PRIMARY KEY,
|
||||||
|
case_id varchar(128) NOT NULL,
|
||||||
|
action_type varchar(32) NOT NULL,
|
||||||
|
operator_id varchar(128) NOT NULL,
|
||||||
|
action_reason nvarchar(512) NOT NULL,
|
||||||
|
before_state varchar(32) NOT NULL,
|
||||||
|
after_state varchar(32) NOT NULL,
|
||||||
|
evidence_json nvarchar(max) NULL,
|
||||||
|
occurred_time datetime2(3) NOT NULL,
|
||||||
|
CONSTRAINT fk_wcs_manual_action_case FOREIGN KEY (case_id)
|
||||||
|
REFERENCES dbo.wcs_recovery_case(case_id)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_manual_action_audit')
|
||||||
|
AND name = 'ix_wcs_manual_action_case_time')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_manual_action_case_time
|
||||||
|
ON dbo.wcs_manual_action_audit(case_id, occurred_time);
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF OBJECT_ID('dbo.wcs_resource_reservation', 'U') IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys.indexes
|
||||||
|
WHERE object_id = OBJECT_ID('dbo.wcs_resource_reservation')
|
||||||
|
AND name = 'ix_wcs_resource_reservation_resource_state')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX ix_wcs_resource_reservation_resource_state
|
||||||
|
ON dbo.wcs_resource_reservation(resource_key, reservation_state);
|
||||||
|
END;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
40
sql/wcs/verify_V003__wcs_execution_and_recovery.sql
Normal file
40
sql/wcs/verify_V003__wcs_execution_and_recovery.sql
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT name AS table_name
|
||||||
|
FROM sys.tables
|
||||||
|
WHERE name IN (
|
||||||
|
'wcs_control_lease',
|
||||||
|
'wcs_device_command',
|
||||||
|
'wcs_device_execution',
|
||||||
|
'wcs_command_outbox',
|
||||||
|
'wcs_wms_notification_outbox',
|
||||||
|
'wcs_device_snapshot',
|
||||||
|
'wcs_device_event',
|
||||||
|
'wcs_alarm',
|
||||||
|
'wcs_recovery_case',
|
||||||
|
'wcs_manual_action_audit'
|
||||||
|
)
|
||||||
|
ORDER BY name;
|
||||||
|
|
||||||
|
SELECT i.name AS index_name, t.name AS table_name, i.is_unique
|
||||||
|
FROM sys.indexes i
|
||||||
|
JOIN sys.tables t ON t.object_id = i.object_id
|
||||||
|
WHERE i.name IN (
|
||||||
|
'uk_wcs_recovery_case_open',
|
||||||
|
'uk_wcs_wms_outbox_event',
|
||||||
|
'ix_wcs_device_command_device_state',
|
||||||
|
'ix_wcs_device_command_order_step',
|
||||||
|
'ix_wcs_resource_reservation_resource_state',
|
||||||
|
'ix_wcs_recovery_case_state_created'
|
||||||
|
)
|
||||||
|
ORDER BY i.name;
|
||||||
|
|
||||||
|
SELECT c.name AS constraint_name
|
||||||
|
FROM sys.check_constraints c
|
||||||
|
WHERE c.name IN (
|
||||||
|
'ck_wcs_control_lease_fencing',
|
||||||
|
'ck_wcs_device_execution_fencing',
|
||||||
|
'ck_wcs_command_outbox_fencing',
|
||||||
|
'ck_wcs_recovery_case_expected_facts'
|
||||||
|
)
|
||||||
|
ORDER BY c.name;
|
||||||
Reference in New Issue
Block a user