feat: reconcile feedback fail closed
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
package com.ft546.wcs.shadow.feedback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ExecutionFeedback {
|
||||
private final String plcBootId;
|
||||
private final long plannerEpoch;
|
||||
private final long planVersion;
|
||||
private final long segmentSeq;
|
||||
private final long snapshotSeq;
|
||||
private final String currentSite;
|
||||
private final List<String> remainingPath;
|
||||
private final boolean connected;
|
||||
private final boolean commitAckKnown;
|
||||
|
||||
public ExecutionFeedback(String plcBootId, long plannerEpoch, long planVersion, long segmentSeq,
|
||||
long snapshotSeq, String currentSite, List<String> remainingPath,
|
||||
boolean connected, boolean commitAckKnown) {
|
||||
this.plcBootId = plcBootId;
|
||||
this.plannerEpoch = plannerEpoch;
|
||||
this.planVersion = planVersion;
|
||||
this.segmentSeq = segmentSeq;
|
||||
this.snapshotSeq = snapshotSeq;
|
||||
this.currentSite = currentSite;
|
||||
this.remainingPath = Collections.unmodifiableList(new ArrayList<String>(remainingPath));
|
||||
this.connected = connected;
|
||||
this.commitAckKnown = commitAckKnown;
|
||||
}
|
||||
|
||||
public String getPlcBootId() { return plcBootId; }
|
||||
public long getPlannerEpoch() { return plannerEpoch; }
|
||||
public long getPlanVersion() { return planVersion; }
|
||||
public long getSegmentSeq() { return segmentSeq; }
|
||||
public long getSnapshotSeq() { return snapshotSeq; }
|
||||
public String getCurrentSite() { return currentSite; }
|
||||
public List<String> getRemainingPath() { return remainingPath; }
|
||||
public boolean isConnected() { return connected; }
|
||||
public boolean isCommitAckKnown() { return commitAckKnown; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ft546.wcs.shadow.feedback;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
import com.ft546.wcs.shadow.segment.SegmentEvent;
|
||||
import com.ft546.wcs.shadow.segment.TrafficSegment;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FeedbackReconciler {
|
||||
public FeedbackResult reconcile(TrafficSegment segment, ExecutionFeedback feedback) {
|
||||
if (!feedback.isConnected()) return quarantine(segment, "COMMUNICATION_LOST");
|
||||
if (!feedback.isCommitAckKnown()) return quarantine(segment, "COMMIT_ACK_UNKNOWN");
|
||||
if (!segment.getPlcBootId().equals(feedback.getPlcBootId())) {
|
||||
return quarantine(segment, "PLC_RESTARTED");
|
||||
}
|
||||
if (segment.getPlannerEpoch() != feedback.getPlannerEpoch()
|
||||
|| segment.getPlanVersion() != feedback.getPlanVersion()
|
||||
|| segment.getSegmentSeq() != feedback.getSegmentSeq()) {
|
||||
return quarantine(segment, "VERSION_MISMATCH");
|
||||
}
|
||||
if (feedback.getSnapshotSeq() <= segment.getLastSnapshotSeq()) {
|
||||
return new FeedbackResult(false, "DUPLICATE_SNAPSHOT",
|
||||
Collections.<ResourceKey>emptySet(), allResources(segment.getPath()), segment);
|
||||
}
|
||||
int currentIndex = segment.getPath().indexOf(feedback.getCurrentSite());
|
||||
if (currentIndex < 0
|
||||
|| !segment.getPath().subList(currentIndex, segment.getPath().size())
|
||||
.equals(feedback.getRemainingPath())) {
|
||||
return quarantine(segment, "PATH_MISMATCH");
|
||||
}
|
||||
Set<ResourceKey> released = new LinkedHashSet<ResourceKey>();
|
||||
Set<ResourceKey> retained = new LinkedHashSet<ResourceKey>();
|
||||
List<String> path = segment.getPath();
|
||||
for (int i = 0; i < path.size(); i++) {
|
||||
(i < currentIndex ? released : retained).add(ResourceKey.node(path.get(i)));
|
||||
if (i < path.size() - 1) {
|
||||
(i < currentIndex ? released : retained).add(ResourceKey.edge(path.get(i), path.get(i + 1)));
|
||||
}
|
||||
}
|
||||
return new FeedbackResult(false, "MATCHED_PROGRESS", released, retained,
|
||||
segment.withLastSnapshotSeq(feedback.getSnapshotSeq()));
|
||||
}
|
||||
|
||||
private FeedbackResult quarantine(TrafficSegment segment, String reason) {
|
||||
return new FeedbackResult(true, reason, Collections.<ResourceKey>emptySet(),
|
||||
allResources(segment.getPath()), segment.transition(SegmentEvent.QUARANTINE));
|
||||
}
|
||||
|
||||
private Set<ResourceKey> allResources(List<String> path) {
|
||||
Set<ResourceKey> resources = new LinkedHashSet<ResourceKey>();
|
||||
for (int i = 0; i < path.size(); i++) {
|
||||
resources.add(ResourceKey.node(path.get(i)));
|
||||
if (i < path.size() - 1) resources.add(ResourceKey.edge(path.get(i), path.get(i + 1)));
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ft546.wcs.shadow.feedback;
|
||||
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
import com.ft546.wcs.shadow.segment.TrafficSegment;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FeedbackResult {
|
||||
private final boolean quarantined;
|
||||
private final String reasonCode;
|
||||
private final Set<ResourceKey> releasedResources;
|
||||
private final Set<ResourceKey> retainedResources;
|
||||
private final TrafficSegment updatedSegment;
|
||||
|
||||
FeedbackResult(boolean quarantined, String reasonCode, Set<ResourceKey> releasedResources,
|
||||
Set<ResourceKey> retainedResources, TrafficSegment updatedSegment) {
|
||||
this.quarantined = quarantined;
|
||||
this.reasonCode = reasonCode;
|
||||
this.releasedResources = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<ResourceKey>(releasedResources));
|
||||
this.retainedResources = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<ResourceKey>(retainedResources));
|
||||
this.updatedSegment = updatedSegment;
|
||||
}
|
||||
|
||||
public boolean isQuarantined() { return quarantined; }
|
||||
public String getReasonCode() { return reasonCode; }
|
||||
public Set<ResourceKey> getReleasedResources() { return releasedResources; }
|
||||
public Set<ResourceKey> getRetainedResources() { return retainedResources; }
|
||||
public TrafficSegment getUpdatedSegment() { return updatedSegment; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ft546.wcs.shadow;
|
||||
|
||||
import com.ft546.wcs.shadow.feedback.ExecutionFeedback;
|
||||
import com.ft546.wcs.shadow.feedback.FeedbackReconciler;
|
||||
import com.ft546.wcs.shadow.feedback.FeedbackResult;
|
||||
import com.ft546.wcs.shadow.resource.ResourceKey;
|
||||
import com.ft546.wcs.shadow.segment.ReservationState;
|
||||
import com.ft546.wcs.shadow.segment.SegmentEvent;
|
||||
import com.ft546.wcs.shadow.segment.TrafficSegment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FeedbackReconcilerTest {
|
||||
private final FeedbackReconciler reconciler = new FeedbackReconciler();
|
||||
|
||||
@Test
|
||||
void releasesOnlyPassedNodeAndEdgeAndKeepsDestinationProtected() {
|
||||
FeedbackResult result = reconciler.reconcile(committed(), feedback(
|
||||
"BOOT-1", 7L, 2L, 1L, 10L, "B", Arrays.asList("B", "C"), true, true));
|
||||
|
||||
assertFalse(result.isQuarantined());
|
||||
assertEquals("MATCHED_PROGRESS", result.getReasonCode());
|
||||
assertTrue(result.getReleasedResources().contains(ResourceKey.node("A")));
|
||||
assertTrue(result.getReleasedResources().contains(ResourceKey.edge("A", "B")));
|
||||
assertTrue(result.getRetainedResources().contains(ResourceKey.node("B")));
|
||||
assertTrue(result.getRetainedResources().contains(ResourceKey.node("C")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateSnapshotIsIdempotentAndEtaNeverReleases() {
|
||||
TrafficSegment processed = committed().withLastSnapshotSeq(10L);
|
||||
FeedbackResult duplicate = reconciler.reconcile(processed, feedback(
|
||||
"BOOT-1", 7L, 2L, 1L, 10L, "C", Collections.singletonList("C"), true, true));
|
||||
|
||||
assertEquals("DUPLICATE_SNAPSHOT", duplicate.getReasonCode());
|
||||
assertTrue(duplicate.getReleasedResources().isEmpty());
|
||||
assertEquals(ReservationState.HARD, duplicate.getUpdatedSegment().getReservationState());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mismatchRestartDisconnectAndAckLossFailClosed() {
|
||||
assertQuarantined("VERSION_MISMATCH", feedback(
|
||||
"BOOT-1", 7L, 3L, 1L, 10L, "B", Arrays.asList("B", "C"), true, true));
|
||||
assertQuarantined("PLC_RESTARTED", feedback(
|
||||
"BOOT-2", 7L, 2L, 1L, 10L, "B", Arrays.asList("B", "C"), true, true));
|
||||
assertQuarantined("COMMUNICATION_LOST", feedback(
|
||||
"BOOT-1", 7L, 2L, 1L, 10L, "B", Arrays.asList("B", "C"), false, true));
|
||||
assertQuarantined("COMMIT_ACK_UNKNOWN", feedback(
|
||||
"BOOT-1", 7L, 2L, 1L, 10L, "B", Arrays.asList("B", "C"), true, false));
|
||||
assertQuarantined("PATH_MISMATCH", feedback(
|
||||
"BOOT-1", 7L, 2L, 1L, 10L, "C", Arrays.asList("C", "B"), true, true));
|
||||
}
|
||||
|
||||
private void assertQuarantined(String reason, ExecutionFeedback feedback) {
|
||||
FeedbackResult result = reconciler.reconcile(committed(), feedback);
|
||||
assertTrue(result.isQuarantined());
|
||||
assertEquals(reason, result.getReasonCode());
|
||||
assertTrue(result.getReleasedResources().isEmpty());
|
||||
assertEquals(ReservationState.UNKNOWN_HARD, result.getUpdatedSegment().getReservationState());
|
||||
}
|
||||
|
||||
private TrafficSegment committed() {
|
||||
return TrafficSegment.planned("SEG-1", "V1", "BOOT-1", 7L, 2L, 1L,
|
||||
Arrays.asList("A", "B", "C"))
|
||||
.transition(SegmentEvent.PREPARE)
|
||||
.transition(SegmentEvent.PREPARED)
|
||||
.transition(SegmentEvent.BEGIN_COMMIT)
|
||||
.transition(SegmentEvent.COMMITTED);
|
||||
}
|
||||
|
||||
private ExecutionFeedback feedback(String bootId, long epoch, long version, long segmentSeq,
|
||||
long snapshotSeq, String current, java.util.List<String> remaining,
|
||||
boolean connected, boolean ackKnown) {
|
||||
return new ExecutionFeedback(bootId, epoch, version, segmentSeq, snapshotSeq,
|
||||
current, remaining, connected, ackKnown);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user