feat: validate shadow segment snapshots
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package com.ft546.wcs.shadow.segment;
|
||||
|
||||
public enum ReservationState {
|
||||
SOFT, COMMIT_GUARD, HARD, UNKNOWN_HARD, RELEASED
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.ft546.wcs.shadow.segment;
|
||||
|
||||
public enum SegmentEvent {
|
||||
PREPARE, PREPARED, BEGIN_COMMIT, COMMITTED, EXECUTING, COMPLETED,
|
||||
COMMIT_UNKNOWN, FAULT, QUARANTINE
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.ft546.wcs.shadow.segment;
|
||||
|
||||
public enum SegmentState {
|
||||
SOFT_PLANNED, PREPARING, PREPARED, COMMITTING, COMMITTED, EXECUTING,
|
||||
COMPLETED, COMMIT_UNCERTAIN, FAULT_LOCKED, QUARANTINED
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.ft546.wcs.shadow.segment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class TrafficSegment {
|
||||
private final String segmentId;
|
||||
private final String vehicleId;
|
||||
private final String plcBootId;
|
||||
private final long plannerEpoch;
|
||||
private final long planVersion;
|
||||
private final long segmentSeq;
|
||||
private final List<String> path;
|
||||
private final SegmentState state;
|
||||
private final ReservationState reservationState;
|
||||
private final long lastSnapshotSeq;
|
||||
|
||||
private TrafficSegment(String segmentId, String vehicleId, String plcBootId, long plannerEpoch,
|
||||
long planVersion, long segmentSeq, List<String> path, SegmentState state,
|
||||
ReservationState reservationState, long lastSnapshotSeq) {
|
||||
this.segmentId = segmentId;
|
||||
this.vehicleId = vehicleId;
|
||||
this.plcBootId = plcBootId;
|
||||
this.plannerEpoch = plannerEpoch;
|
||||
this.planVersion = planVersion;
|
||||
this.segmentSeq = segmentSeq;
|
||||
this.path = Collections.unmodifiableList(new ArrayList<String>(path));
|
||||
this.state = state;
|
||||
this.reservationState = reservationState;
|
||||
this.lastSnapshotSeq = lastSnapshotSeq;
|
||||
}
|
||||
|
||||
public static TrafficSegment planned(String segmentId, String vehicleId, String plcBootId,
|
||||
long plannerEpoch, long planVersion, long segmentSeq,
|
||||
List<String> path) {
|
||||
if (path == null || path.size() < 2) {
|
||||
throw new IllegalArgumentException("segment path requires at least two sites");
|
||||
}
|
||||
return new TrafficSegment(segmentId, vehicleId, plcBootId, plannerEpoch, planVersion,
|
||||
segmentSeq, path, SegmentState.SOFT_PLANNED, ReservationState.SOFT, -1L);
|
||||
}
|
||||
|
||||
public TrafficSegment transition(SegmentEvent event) {
|
||||
SegmentState next;
|
||||
ReservationState reservation = reservationState;
|
||||
switch (event) {
|
||||
case PREPARE:
|
||||
require(SegmentState.SOFT_PLANNED); next = SegmentState.PREPARING; break;
|
||||
case PREPARED:
|
||||
require(SegmentState.PREPARING); next = SegmentState.PREPARED; break;
|
||||
case BEGIN_COMMIT:
|
||||
require(SegmentState.PREPARED); next = SegmentState.COMMITTING;
|
||||
reservation = ReservationState.COMMIT_GUARD; break;
|
||||
case COMMITTED:
|
||||
require(SegmentState.COMMITTING); next = SegmentState.COMMITTED;
|
||||
reservation = ReservationState.HARD; break;
|
||||
case EXECUTING:
|
||||
require(SegmentState.COMMITTED); next = SegmentState.EXECUTING; break;
|
||||
case COMPLETED:
|
||||
require(SegmentState.EXECUTING); next = SegmentState.COMPLETED;
|
||||
reservation = ReservationState.RELEASED; break;
|
||||
case COMMIT_UNKNOWN:
|
||||
require(SegmentState.COMMITTING); next = SegmentState.COMMIT_UNCERTAIN;
|
||||
reservation = ReservationState.UNKNOWN_HARD; break;
|
||||
case FAULT:
|
||||
if (state != SegmentState.COMMITTED && state != SegmentState.EXECUTING) {
|
||||
throw new IllegalStateException("FAULT requires committed segment");
|
||||
}
|
||||
next = SegmentState.FAULT_LOCKED; reservation = ReservationState.UNKNOWN_HARD; break;
|
||||
case QUARANTINE:
|
||||
next = SegmentState.QUARANTINED; reservation = ReservationState.UNKNOWN_HARD; break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported event " + event);
|
||||
}
|
||||
return new TrafficSegment(segmentId, vehicleId, plcBootId, plannerEpoch, planVersion,
|
||||
segmentSeq, path, next, reservation, lastSnapshotSeq);
|
||||
}
|
||||
|
||||
public TrafficSegment withLastSnapshotSeq(long snapshotSeq) {
|
||||
return new TrafficSegment(segmentId, vehicleId, plcBootId, plannerEpoch, planVersion,
|
||||
segmentSeq, path, state, reservationState, snapshotSeq);
|
||||
}
|
||||
|
||||
private void require(SegmentState required) {
|
||||
if (state != required) {
|
||||
throw new IllegalStateException(state + " cannot accept transition requiring " + required);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSegmentId() { return segmentId; }
|
||||
public String getVehicleId() { return vehicleId; }
|
||||
public String getPlcBootId() { return plcBootId; }
|
||||
public long getPlannerEpoch() { return plannerEpoch; }
|
||||
public long getPlanVersion() { return planVersion; }
|
||||
public long getSegmentSeq() { return segmentSeq; }
|
||||
public List<String> getPath() { return path; }
|
||||
public SegmentState getState() { return state; }
|
||||
public ReservationState getReservationState() { return reservationState; }
|
||||
public long getLastSnapshotSeq() { return lastSnapshotSeq; }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ft546.wcs.shadow.snapshot;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
public final class SnapshotFreshnessPolicy {
|
||||
private final Duration maximumAge;
|
||||
|
||||
public SnapshotFreshnessPolicy(Duration maximumAge) {
|
||||
this.maximumAge = maximumAge;
|
||||
}
|
||||
|
||||
public SnapshotHealth validate(TrafficVehicleSnapshot previous,
|
||||
TrafficVehicleSnapshot current, Instant decisionTime) {
|
||||
if (!current.isConnected()) return SnapshotHealth.unhealthy("COMMUNICATION_LOST");
|
||||
if (current.isFaulted()) return SnapshotHealth.unhealthy("VEHICLE_FAULT");
|
||||
if (current.isManual()) return SnapshotHealth.unhealthy("MANUAL_MODE");
|
||||
if (Duration.between(current.getReceivedAt(), decisionTime).compareTo(maximumAge) > 0) {
|
||||
return SnapshotHealth.unhealthy("STALE_SNAPSHOT");
|
||||
}
|
||||
if (previous != null && !previous.getPlcBootId().equals(current.getPlcBootId())) {
|
||||
return SnapshotHealth.unhealthy("PLC_RESTARTED");
|
||||
}
|
||||
if (previous != null && (current.getSnapshotSeq() <= previous.getSnapshotSeq()
|
||||
|| current.getHeartbeatSeq() <= previous.getHeartbeatSeq())) {
|
||||
return SnapshotHealth.unhealthy("SNAPSHOT_SEQUENCE_ROLLBACK");
|
||||
}
|
||||
return SnapshotHealth.healthy();
|
||||
}
|
||||
|
||||
public static final class SnapshotHealth {
|
||||
private final boolean healthy;
|
||||
private final String reasonCode;
|
||||
|
||||
private SnapshotHealth(boolean healthy, String reasonCode) {
|
||||
this.healthy = healthy;
|
||||
this.reasonCode = reasonCode;
|
||||
}
|
||||
|
||||
static SnapshotHealth healthy() { return new SnapshotHealth(true, "OK"); }
|
||||
static SnapshotHealth unhealthy(String reason) { return new SnapshotHealth(false, reason); }
|
||||
public boolean isHealthy() { return healthy; }
|
||||
public String getReasonCode() { return reasonCode; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ft546.wcs.shadow.snapshot;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class TrafficSnapshot {
|
||||
private final long tickSeq;
|
||||
private final Instant decisionTime;
|
||||
private final String mapCode;
|
||||
private final long mapVersion;
|
||||
private final String policyVersion;
|
||||
private final List<TrafficVehicleSnapshot> vehicles;
|
||||
|
||||
public TrafficSnapshot(long tickSeq, Instant decisionTime, String mapCode, long mapVersion,
|
||||
String policyVersion, List<TrafficVehicleSnapshot> vehicles) {
|
||||
this.tickSeq = tickSeq;
|
||||
this.decisionTime = decisionTime;
|
||||
this.mapCode = mapCode;
|
||||
this.mapVersion = mapVersion;
|
||||
this.policyVersion = policyVersion;
|
||||
this.vehicles = Collections.unmodifiableList(new ArrayList<TrafficVehicleSnapshot>(vehicles));
|
||||
}
|
||||
|
||||
public long getTickSeq() { return tickSeq; }
|
||||
public Instant getDecisionTime() { return decisionTime; }
|
||||
public String getMapCode() { return mapCode; }
|
||||
public long getMapVersion() { return mapVersion; }
|
||||
public String getPolicyVersion() { return policyVersion; }
|
||||
public List<TrafficVehicleSnapshot> getVehicles() { return vehicles; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ft546.wcs.shadow.snapshot;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class TrafficVehicleSnapshot {
|
||||
private final String vehicleId;
|
||||
private final String plcBootId;
|
||||
private final long heartbeatSeq;
|
||||
private final long snapshotSeq;
|
||||
private final Instant receivedAt;
|
||||
private final String currentSite;
|
||||
private final String nextSite;
|
||||
private final List<String> remainingPath;
|
||||
private final boolean connected;
|
||||
private final boolean faulted;
|
||||
private final boolean manual;
|
||||
|
||||
public TrafficVehicleSnapshot(String vehicleId, String plcBootId, long heartbeatSeq,
|
||||
long snapshotSeq, Instant receivedAt, String currentSite,
|
||||
String nextSite, List<String> remainingPath, boolean connected,
|
||||
boolean faulted, boolean manual) {
|
||||
this.vehicleId = vehicleId;
|
||||
this.plcBootId = plcBootId;
|
||||
this.heartbeatSeq = heartbeatSeq;
|
||||
this.snapshotSeq = snapshotSeq;
|
||||
this.receivedAt = receivedAt;
|
||||
this.currentSite = currentSite;
|
||||
this.nextSite = nextSite;
|
||||
this.remainingPath = Collections.unmodifiableList(new ArrayList<String>(remainingPath));
|
||||
this.connected = connected;
|
||||
this.faulted = faulted;
|
||||
this.manual = manual;
|
||||
}
|
||||
|
||||
public String getVehicleId() { return vehicleId; }
|
||||
public String getPlcBootId() { return plcBootId; }
|
||||
public long getHeartbeatSeq() { return heartbeatSeq; }
|
||||
public long getSnapshotSeq() { return snapshotSeq; }
|
||||
public Instant getReceivedAt() { return receivedAt; }
|
||||
public String getCurrentSite() { return currentSite; }
|
||||
public String getNextSite() { return nextSite; }
|
||||
public List<String> getRemainingPath() { return remainingPath; }
|
||||
public boolean isConnected() { return connected; }
|
||||
public boolean isFaulted() { return faulted; }
|
||||
public boolean isManual() { return manual; }
|
||||
}
|
||||
Reference in New Issue
Block a user