feat(wcs): add resource reservation ledger
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReservationDecision {
|
||||
private final String reservationId;
|
||||
private final boolean granted;
|
||||
private final long fencingToken;
|
||||
private final String reasonCode;
|
||||
private final List<String> conflictingResourceKeys;
|
||||
|
||||
private ReservationDecision(String reservationId, boolean granted,
|
||||
long fencingToken, String reasonCode,
|
||||
List<String> conflictingResourceKeys) {
|
||||
this.reservationId = reservationId;
|
||||
this.granted = granted;
|
||||
this.fencingToken = fencingToken;
|
||||
this.reasonCode = reasonCode;
|
||||
this.conflictingResourceKeys = Collections.unmodifiableList(
|
||||
new ArrayList<String>(conflictingResourceKeys));
|
||||
}
|
||||
|
||||
static ReservationDecision granted(ResourceReservation reservation) {
|
||||
return new ReservationDecision(reservation.getReservationId(), true,
|
||||
reservation.getFencingToken(), "GRANTED",
|
||||
Collections.<String>emptyList());
|
||||
}
|
||||
|
||||
static ReservationDecision rejected(String reservationId,
|
||||
List<String> conflicts,
|
||||
String reasonCode) {
|
||||
return new ReservationDecision(reservationId, false, 0L,
|
||||
reasonCode, conflicts);
|
||||
}
|
||||
|
||||
public String getReservationId() {
|
||||
return reservationId;
|
||||
}
|
||||
|
||||
public boolean isGranted() {
|
||||
return granted;
|
||||
}
|
||||
|
||||
public long getFencingToken() {
|
||||
return fencingToken;
|
||||
}
|
||||
|
||||
public String getReasonCode() {
|
||||
return reasonCode;
|
||||
}
|
||||
|
||||
public List<String> getConflictingResourceKeys() {
|
||||
return conflictingResourceKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ReservationRequest {
|
||||
private final String reservationId;
|
||||
private final String orderId;
|
||||
private final String stepId;
|
||||
private final String vehicleId;
|
||||
private final List<String> resourceKeys;
|
||||
|
||||
public ReservationRequest(String reservationId, String orderId, String stepId,
|
||||
String vehicleId, List<String> resourceKeys) {
|
||||
this.reservationId = requireText(reservationId, "reservationId");
|
||||
this.orderId = requireText(orderId, "orderId");
|
||||
this.stepId = requireText(stepId, "stepId");
|
||||
this.vehicleId = requireText(vehicleId, "vehicleId");
|
||||
if (resourceKeys == null || resourceKeys.isEmpty()) {
|
||||
throw new IllegalArgumentException("resourceKeys must not be empty");
|
||||
}
|
||||
Set<String> unique = new LinkedHashSet<String>();
|
||||
for (String resourceKey : resourceKeys) {
|
||||
unique.add(requireText(resourceKey, "resourceKey"));
|
||||
}
|
||||
if (unique.size() != resourceKeys.size()) {
|
||||
throw new IllegalArgumentException("resourceKeys must not contain duplicates");
|
||||
}
|
||||
List<String> sorted = new ArrayList<String>(unique);
|
||||
Collections.sort(sorted);
|
||||
this.resourceKeys = Collections.unmodifiableList(sorted);
|
||||
}
|
||||
|
||||
public String getReservationId() {
|
||||
return reservationId;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getStepId() {
|
||||
return stepId;
|
||||
}
|
||||
|
||||
public String getVehicleId() {
|
||||
return vehicleId;
|
||||
}
|
||||
|
||||
public List<String> getResourceKeys() {
|
||||
return resourceKeys;
|
||||
}
|
||||
|
||||
boolean sameAs(ReservationRequest other) {
|
||||
return other != null
|
||||
&& reservationId.equals(other.reservationId)
|
||||
&& orderId.equals(other.orderId)
|
||||
&& stepId.equals(other.stepId)
|
||||
&& vehicleId.equals(other.vehicleId)
|
||||
&& resourceKeys.equals(other.resourceKeys);
|
||||
}
|
||||
|
||||
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,11 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
public enum ReservationState {
|
||||
REQUESTED,
|
||||
GRANTED,
|
||||
OCCUPIED,
|
||||
RELEASED,
|
||||
REJECTED,
|
||||
EXPIRED,
|
||||
ABORTED
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
|
||||
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class ResourceLedger {
|
||||
private final Map<String, Integer> capacities;
|
||||
private final Map<String, Integer> usage;
|
||||
private final Map<String, ResourceReservation> reservations;
|
||||
private long nextFencingToken;
|
||||
|
||||
public ResourceLedger(Map<String, Integer> capacities) {
|
||||
if (capacities == null || capacities.isEmpty()) {
|
||||
throw new IllegalArgumentException("capacities must not be empty");
|
||||
}
|
||||
this.capacities = new LinkedHashMap<String, Integer>();
|
||||
this.usage = new LinkedHashMap<String, Integer>();
|
||||
for (Map.Entry<String, Integer> entry : capacities.entrySet()) {
|
||||
String key = requireText(entry.getKey(), "resourceKey");
|
||||
Integer capacity = entry.getValue();
|
||||
if (capacity == null || capacity <= 0) {
|
||||
throw new IllegalArgumentException("resource capacity must be positive");
|
||||
}
|
||||
this.capacities.put(key, capacity);
|
||||
this.usage.put(key, 0);
|
||||
}
|
||||
this.reservations = new LinkedHashMap<String, ResourceReservation>();
|
||||
this.nextFencingToken = 1L;
|
||||
}
|
||||
|
||||
public ResourceLedger(EffectiveTopology topology) {
|
||||
this(capacities(topology));
|
||||
}
|
||||
|
||||
private ResourceLedger(ResourceLedger source) {
|
||||
this.capacities = new LinkedHashMap<String, Integer>(source.capacities);
|
||||
this.usage = new LinkedHashMap<String, Integer>(source.usage);
|
||||
this.reservations = new LinkedHashMap<String, ResourceReservation>();
|
||||
for (Map.Entry<String, ResourceReservation> entry
|
||||
: source.reservations.entrySet()) {
|
||||
this.reservations.put(entry.getKey(), entry.getValue().copy());
|
||||
}
|
||||
this.nextFencingToken = source.nextFencingToken;
|
||||
}
|
||||
|
||||
public synchronized ReservationDecision reserve(ReservationRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request must not be null");
|
||||
}
|
||||
ResourceReservation existing = reservations.get(request.getReservationId());
|
||||
if (existing != null) {
|
||||
if (!existing.matches(request)) {
|
||||
throw new DomainRuleViolation("reservation id reused with different request");
|
||||
}
|
||||
return existing.getState() == ReservationState.GRANTED
|
||||
|| existing.getState() == ReservationState.OCCUPIED
|
||||
? ReservationDecision.granted(existing)
|
||||
: ReservationDecision.rejected(existing.getReservationId(),
|
||||
existing.getResourceKeys(), existing.getState().name());
|
||||
}
|
||||
|
||||
ResourceReservation reservation = new ResourceReservation(request);
|
||||
List<String> unknown = new ArrayList<String>();
|
||||
List<String> conflicts = new ArrayList<String>();
|
||||
for (String resourceKey : request.getResourceKeys()) {
|
||||
Integer capacity = capacities.get(resourceKey);
|
||||
if (capacity == null) {
|
||||
unknown.add(resourceKey);
|
||||
} else if (usage.get(resourceKey) >= capacity) {
|
||||
conflicts.add(resourceKey);
|
||||
}
|
||||
}
|
||||
if (!unknown.isEmpty() || !conflicts.isEmpty()) {
|
||||
List<String> rejectedResources = unknown.isEmpty() ? conflicts : unknown;
|
||||
String reason = unknown.isEmpty() ? "CAPACITY_CONFLICT" : "UNKNOWN_RESOURCE";
|
||||
reservation.reject(reason);
|
||||
reservations.put(request.getReservationId(), reservation);
|
||||
return ReservationDecision.rejected(request.getReservationId(),
|
||||
rejectedResources, reason);
|
||||
}
|
||||
|
||||
reservation.grant(nextFencingToken++);
|
||||
for (String resourceKey : request.getResourceKeys()) {
|
||||
usage.put(resourceKey, usage.get(resourceKey) + 1);
|
||||
}
|
||||
reservations.put(request.getReservationId(), reservation);
|
||||
return ReservationDecision.granted(reservation);
|
||||
}
|
||||
|
||||
public synchronized void occupy(String reservationId, long fencingToken) {
|
||||
requireReservation(reservationId).occupy(fencingToken);
|
||||
}
|
||||
|
||||
public synchronized void release(String reservationId, String releaseEvidenceId,
|
||||
long fencingToken) {
|
||||
ResourceReservation reservation = requireReservation(reservationId);
|
||||
reservation.release(releaseEvidenceId, fencingToken);
|
||||
decrementUsage(reservation);
|
||||
}
|
||||
|
||||
public synchronized void abortUnoccupied(String reservationId, String reason) {
|
||||
ResourceReservation reservation = requireReservation(reservationId);
|
||||
boolean heldCapacity = reservation.getState() == ReservationState.GRANTED;
|
||||
reservation.abort(reason);
|
||||
if (heldCapacity) {
|
||||
decrementUsage(reservation);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void expireUnoccupied(String reservationId) {
|
||||
ResourceReservation reservation = requireReservation(reservationId);
|
||||
boolean heldCapacity = reservation.getState() == ReservationState.GRANTED;
|
||||
reservation.expire();
|
||||
if (heldCapacity) {
|
||||
decrementUsage(reservation);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void markCommandUnknown(String reservationId) {
|
||||
requireReservation(reservationId).markCommandUnknown();
|
||||
}
|
||||
|
||||
public synchronized void markRecoveryUnresolved(String reservationId) {
|
||||
requireReservation(reservationId).markRecoveryUnresolved();
|
||||
}
|
||||
|
||||
public synchronized void clearProtectionAfterRecovery(
|
||||
String reservationId, String evidenceId) {
|
||||
requireReservation(reservationId).clearProtectionAfterRecovery(evidenceId);
|
||||
}
|
||||
|
||||
public synchronized ResourceReservation reservation(String reservationId) {
|
||||
return requireReservation(reservationId);
|
||||
}
|
||||
|
||||
public synchronized int usage(String resourceKey) {
|
||||
Integer count = usage.get(resourceKey);
|
||||
if (count == null) {
|
||||
throw new DomainRuleViolation("unknown resource: " + resourceKey);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public synchronized int availableCapacity(String resourceKey) {
|
||||
Integer capacity = capacities.get(resourceKey);
|
||||
if (capacity == null) {
|
||||
throw new DomainRuleViolation("unknown resource: " + resourceKey);
|
||||
}
|
||||
return capacity - usage.get(resourceKey);
|
||||
}
|
||||
|
||||
public synchronized Set<String> holders(String resourceKey) {
|
||||
if (!capacities.containsKey(resourceKey)) {
|
||||
throw new DomainRuleViolation("unknown resource: " + resourceKey);
|
||||
}
|
||||
Set<String> holders = new TreeSet<String>();
|
||||
for (ResourceReservation reservation : reservations.values()) {
|
||||
if ((reservation.getState() == ReservationState.GRANTED
|
||||
|| reservation.getState() == ReservationState.OCCUPIED)
|
||||
&& reservation.getResourceKeys().contains(resourceKey)) {
|
||||
holders.add(reservation.getVehicleId());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<String>(holders));
|
||||
}
|
||||
|
||||
public synchronized boolean containsResource(String resourceKey) {
|
||||
return capacities.containsKey(resourceKey);
|
||||
}
|
||||
|
||||
public synchronized ResourceLedger copy() {
|
||||
return new ResourceLedger(this);
|
||||
}
|
||||
|
||||
private void decrementUsage(ResourceReservation reservation) {
|
||||
for (String resourceKey : reservation.getResourceKeys()) {
|
||||
int current = usage.get(resourceKey);
|
||||
if (current <= 0) {
|
||||
throw new IllegalStateException("resource usage underflow: " + resourceKey);
|
||||
}
|
||||
usage.put(resourceKey, current - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceReservation requireReservation(String reservationId) {
|
||||
ResourceReservation reservation = reservations.get(
|
||||
requireText(reservationId, "reservationId"));
|
||||
if (reservation == null) {
|
||||
throw new DomainRuleViolation("reservation not found: " + reservationId);
|
||||
}
|
||||
return reservation;
|
||||
}
|
||||
|
||||
private static Map<String, Integer> capacities(EffectiveTopology topology) {
|
||||
if (topology == null) {
|
||||
throw new IllegalArgumentException("topology must not be null");
|
||||
}
|
||||
Map<String, Integer> result = new LinkedHashMap<String, Integer>();
|
||||
for (ResourceDefinition definition : topology.getResources().values()) {
|
||||
result.put(definition.getResourceKey(), definition.getCapacity());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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,167 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ResourceReservation {
|
||||
private final ReservationRequest request;
|
||||
private ReservationState state;
|
||||
private long fencingToken;
|
||||
private String terminalReason;
|
||||
private String releaseEvidenceId;
|
||||
private boolean commandUnknown;
|
||||
private boolean recoveryUnresolved;
|
||||
|
||||
ResourceReservation(ReservationRequest request) {
|
||||
this.request = request;
|
||||
this.state = ReservationState.REQUESTED;
|
||||
}
|
||||
|
||||
private ResourceReservation(ResourceReservation source) {
|
||||
this.request = source.request;
|
||||
this.state = source.state;
|
||||
this.fencingToken = source.fencingToken;
|
||||
this.terminalReason = source.terminalReason;
|
||||
this.releaseEvidenceId = source.releaseEvidenceId;
|
||||
this.commandUnknown = source.commandUnknown;
|
||||
this.recoveryUnresolved = source.recoveryUnresolved;
|
||||
}
|
||||
|
||||
void grant(long token) {
|
||||
requireState(ReservationState.REQUESTED);
|
||||
if (token <= 0L) {
|
||||
throw new IllegalArgumentException("fencingToken must be positive");
|
||||
}
|
||||
fencingToken = token;
|
||||
state = ReservationState.GRANTED;
|
||||
}
|
||||
|
||||
void reject(String reason) {
|
||||
requireState(ReservationState.REQUESTED);
|
||||
terminalReason = requireText(reason, "reason");
|
||||
state = ReservationState.REJECTED;
|
||||
}
|
||||
|
||||
void occupy(long token) {
|
||||
requireState(ReservationState.GRANTED);
|
||||
requireToken(token);
|
||||
state = ReservationState.OCCUPIED;
|
||||
}
|
||||
|
||||
void release(String evidenceId, long token) {
|
||||
requireState(ReservationState.OCCUPIED);
|
||||
requireToken(token);
|
||||
String evidence = requireText(evidenceId, "releaseEvidenceId");
|
||||
if (commandUnknown || recoveryUnresolved) {
|
||||
throw new DomainRuleViolation(
|
||||
"occupied reservation is protected by unknown command or recovery");
|
||||
}
|
||||
releaseEvidenceId = evidence;
|
||||
state = ReservationState.RELEASED;
|
||||
}
|
||||
|
||||
void abort(String reason) {
|
||||
if (state != ReservationState.REQUESTED && state != ReservationState.GRANTED) {
|
||||
throw new DomainRuleViolation("only unoccupied reservation can be aborted");
|
||||
}
|
||||
terminalReason = requireText(reason, "reason");
|
||||
state = ReservationState.ABORTED;
|
||||
}
|
||||
|
||||
void expire() {
|
||||
if (state != ReservationState.REQUESTED && state != ReservationState.GRANTED) {
|
||||
throw new DomainRuleViolation("only unoccupied reservation can expire");
|
||||
}
|
||||
state = ReservationState.EXPIRED;
|
||||
}
|
||||
|
||||
void markCommandUnknown() {
|
||||
requireState(ReservationState.OCCUPIED);
|
||||
commandUnknown = true;
|
||||
}
|
||||
|
||||
void markRecoveryUnresolved() {
|
||||
requireState(ReservationState.OCCUPIED);
|
||||
recoveryUnresolved = true;
|
||||
}
|
||||
|
||||
void clearProtectionAfterRecovery(String evidenceId) {
|
||||
requireState(ReservationState.OCCUPIED);
|
||||
requireText(evidenceId, "recoveryEvidenceId");
|
||||
commandUnknown = false;
|
||||
recoveryUnresolved = false;
|
||||
}
|
||||
|
||||
ResourceReservation copy() {
|
||||
return new ResourceReservation(this);
|
||||
}
|
||||
|
||||
public String getReservationId() {
|
||||
return request.getReservationId();
|
||||
}
|
||||
|
||||
public String getVehicleId() {
|
||||
return request.getVehicleId();
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return request.getOrderId();
|
||||
}
|
||||
|
||||
public String getStepId() {
|
||||
return request.getStepId();
|
||||
}
|
||||
|
||||
public List<String> getResourceKeys() {
|
||||
return request.getResourceKeys();
|
||||
}
|
||||
|
||||
public ReservationState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public long getFencingToken() {
|
||||
return fencingToken;
|
||||
}
|
||||
|
||||
public String getTerminalReason() {
|
||||
return terminalReason;
|
||||
}
|
||||
|
||||
public String getReleaseEvidenceId() {
|
||||
return releaseEvidenceId;
|
||||
}
|
||||
|
||||
public boolean isCommandUnknown() {
|
||||
return commandUnknown;
|
||||
}
|
||||
|
||||
public boolean isRecoveryUnresolved() {
|
||||
return recoveryUnresolved;
|
||||
}
|
||||
|
||||
boolean matches(ReservationRequest candidate) {
|
||||
return request.sameAs(candidate);
|
||||
}
|
||||
|
||||
private void requireState(ReservationState required) {
|
||||
if (state != required) {
|
||||
throw new DomainRuleViolation("reservation state must be " + required
|
||||
+ " but was " + state);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireToken(long token) {
|
||||
if (token != fencingToken) {
|
||||
throw new DomainRuleViolation("stale reservation fencing token");
|
||||
}
|
||||
}
|
||||
|
||||
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,109 @@
|
||||
package com.ruoyi.wcs.domain.traffic;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ResourceLedgerTest {
|
||||
|
||||
@Test
|
||||
void grantsOnlyOneReservationForCapacityOneResource() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
|
||||
assertTrue(ledger.reserve(request("R1", "V1", "NODE:A")).isGranted());
|
||||
ReservationDecision rejected = ledger.reserve(request("R2", "V2", "NODE:A"));
|
||||
|
||||
assertFalse(rejected.isGranted());
|
||||
assertEquals(Collections.singletonList("NODE:A"),
|
||||
rejected.getConflictingResourceKeys());
|
||||
assertEquals(Collections.singleton("V1"), ledger.holders("NODE:A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWholeMultiResourceRequestWithoutLeakingCapacity() {
|
||||
Map<String, Integer> capacities = new LinkedHashMap<String, Integer>();
|
||||
capacities.put("NODE:A", 1);
|
||||
capacities.put("NODE:B", 1);
|
||||
ResourceLedger ledger = new ResourceLedger(capacities);
|
||||
ledger.reserve(request("R1", "V1", "NODE:B"));
|
||||
|
||||
ReservationDecision rejected = ledger.reserve(new ReservationRequest(
|
||||
"R2", "O2", "S2", "V2", Arrays.asList("NODE:B", "NODE:A")));
|
||||
|
||||
assertFalse(rejected.isGranted());
|
||||
assertEquals(0, ledger.usage("NODE:A"));
|
||||
assertTrue(ledger.reserve(request("R3", "V3", "NODE:A")).isGranted());
|
||||
}
|
||||
|
||||
@Test
|
||||
void occupiedReservationCannotExpireByTime() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
ReservationDecision decision = ledger.reserve(request("R1", "V1", "NODE:A"));
|
||||
ledger.occupy("R1", decision.getFencingToken());
|
||||
|
||||
ResourceReservation reservation = ledger.reservation("R1");
|
||||
|
||||
assertThrows(DomainRuleViolation.class, reservation::expire);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownCommandKeepsOccupiedResourcesProtected() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
ReservationDecision decision = ledger.reserve(request("R1", "V1", "NODE:A"));
|
||||
ledger.occupy("R1", decision.getFencingToken());
|
||||
ledger.markCommandUnknown("R1");
|
||||
|
||||
assertThrows(DomainRuleViolation.class, () -> ledger.release(
|
||||
"R1", "PLC-SNAPSHOT-1", decision.getFencingToken()));
|
||||
assertEquals(1, ledger.usage("NODE:A"));
|
||||
assertEquals(ReservationState.OCCUPIED, ledger.reservation("R1").getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseRequiresMatchingFencingTokenAndPhysicalEvidence() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
ReservationDecision decision = ledger.reserve(request("R1", "V1", "NODE:A"));
|
||||
ledger.occupy("R1", decision.getFencingToken());
|
||||
|
||||
assertThrows(DomainRuleViolation.class, () -> ledger.release(
|
||||
"R1", "PLC-SNAPSHOT-1", decision.getFencingToken() + 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> ledger.release(
|
||||
"R1", "", decision.getFencingToken()));
|
||||
|
||||
ledger.release("R1", "PLC-SNAPSHOT-1", decision.getFencingToken());
|
||||
assertEquals(0, ledger.usage("NODE:A"));
|
||||
assertEquals(ReservationState.RELEASED, ledger.reservation("R1").getState());
|
||||
}
|
||||
|
||||
@Test
|
||||
void abortingAnUnoccupiedGrantReturnsCapacity() {
|
||||
ResourceLedger ledger = ledger("NODE:A", 1);
|
||||
ledger.reserve(request("R1", "V1", "NODE:A"));
|
||||
|
||||
ledger.abortUnoccupied("R1", "DISPATCH_CANCELLED");
|
||||
|
||||
assertEquals(0, ledger.usage("NODE:A"));
|
||||
assertTrue(ledger.reserve(request("R2", "V2", "NODE:A")).isGranted());
|
||||
}
|
||||
|
||||
private static ResourceLedger ledger(String resourceKey, int capacity) {
|
||||
return new ResourceLedger(Collections.singletonMap(resourceKey, capacity));
|
||||
}
|
||||
|
||||
private static ReservationRequest request(String reservationId,
|
||||
String vehicleId,
|
||||
String resourceKey) {
|
||||
return new ReservationRequest(reservationId, "O-" + reservationId,
|
||||
"S-" + reservationId, vehicleId,
|
||||
Collections.singletonList(resourceKey));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user