feat(wcs): add resource reservation ledger

This commit is contained in:
zgw
2026-08-10 16:11:21 +08:00
parent 42374c24a7
commit 5877d9e60a
6 changed files with 637 additions and 0 deletions

View File

@@ -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));
}
}