feat: coordinate stable shadow snapshots
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowSnapshotCoordinatorTest {
|
||||
|
||||
private static final Instant NOW = Instant.parse("2026-09-03T00:00:00Z");
|
||||
|
||||
@Test
|
||||
void startsNotReadyAndAllowsOnlyOneObservationLease() {
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("01");
|
||||
|
||||
ShadowSnapshotCoordinator.Admission beforePublish = coordinator.enter(NOW);
|
||||
assertThat(beforePublish.accepted()).isFalse();
|
||||
assertThat(beforePublish.skipReason()).isEqualTo(ShadowSkipReason.NOT_READY);
|
||||
coordinator.exit(beforePublish);
|
||||
|
||||
publish(coordinator, snapshot(1, "01", NOW));
|
||||
ShadowSnapshotCoordinator.Admission first = coordinator.enter(NOW);
|
||||
ShadowSnapshotCoordinator.Admission second = coordinator.enter(NOW);
|
||||
|
||||
assertThat(first.accepted()).isTrue();
|
||||
assertThat(first.snapshot()).isEqualTo(snapshot(1, "01", NOW));
|
||||
assertThat(second.accepted()).isFalse();
|
||||
assertThat(second.skipReason()).isEqualTo(ShadowSkipReason.BUSY);
|
||||
|
||||
coordinator.finishObservation(first);
|
||||
coordinator.finishObservation(first);
|
||||
assertThat(coordinator.activeLegacyCount()).isEqualTo(2);
|
||||
|
||||
coordinator.exit(second);
|
||||
coordinator.exit(second);
|
||||
coordinator.exit(first);
|
||||
coordinator.exit(first);
|
||||
assertThat(coordinator.activeLegacyCount()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentLegacyEntryInvalidatesRefreshPublicationAndComparison() {
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("01");
|
||||
publish(coordinator, snapshot(1, "01", NOW));
|
||||
|
||||
ShadowSnapshotCoordinator.Admission first = coordinator.enter(NOW);
|
||||
ShadowSnapshotCoordinator.Admission later = coordinator.enter(NOW);
|
||||
assertThat(first.accepted()).isTrue();
|
||||
assertThat(coordinator.isComparable(first)).isFalse();
|
||||
coordinator.exit(later);
|
||||
coordinator.finishObservation(first);
|
||||
coordinator.exit(first);
|
||||
|
||||
ShadowSnapshotCoordinator.RefreshTicket refresh = coordinator.tryBeginRefresh(NOW).orElseThrow();
|
||||
ShadowSnapshotCoordinator.Admission concurrent = coordinator.enter(NOW);
|
||||
|
||||
assertThat(coordinator.publish(refresh, snapshot(2, "01", NOW), NOW)).isFalse();
|
||||
assertThat(coordinator.phase()).isNotEqualTo(ShadowSnapshotCoordinator.Phase.READY);
|
||||
coordinator.exit(concurrent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipsStaleSnapshotsAndMismatchedWarehouses() {
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("01");
|
||||
|
||||
publish(coordinator, snapshot(3, "01", NOW.minusSeconds(61)));
|
||||
ShadowSnapshotCoordinator.Admission stale = coordinator.enter(NOW);
|
||||
assertThat(stale.accepted()).isFalse();
|
||||
assertThat(stale.skipReason()).isEqualTo(ShadowSkipReason.STALE);
|
||||
coordinator.exit(stale);
|
||||
|
||||
publish(coordinator, snapshot(4, "02", NOW));
|
||||
ShadowSnapshotCoordinator.Admission wrongWarehouse = coordinator.enter(NOW);
|
||||
assertThat(wrongWarehouse.accepted()).isFalse();
|
||||
assertThat(wrongWarehouse.skipReason()).isEqualTo(ShadowSkipReason.WAREHOUSE);
|
||||
coordinator.exit(wrongWarehouse);
|
||||
}
|
||||
|
||||
@Test
|
||||
void treatsNumericWarehouseIdsAsEquivalent() {
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("01");
|
||||
publish(coordinator, snapshot(5, "1", NOW));
|
||||
|
||||
ShadowSnapshotCoordinator.Admission admission = coordinator.enter(NOW);
|
||||
|
||||
assertThat(admission.accepted()).isTrue();
|
||||
coordinator.exit(admission);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedRefreshBecomesUnavailableUntilAValidPublication() {
|
||||
ShadowSnapshotCoordinator coordinator = coordinatorAt("01");
|
||||
ShadowSnapshotCoordinator.RefreshTicket refresh = coordinator.tryBeginRefresh(NOW).orElseThrow();
|
||||
|
||||
coordinator.failRefresh(refresh, "signature changed");
|
||||
|
||||
assertThat(coordinator.phase()).isEqualTo(ShadowSnapshotCoordinator.Phase.UNAVAILABLE);
|
||||
assertThat(coordinator.lastFailure()).isEqualTo("signature changed");
|
||||
ShadowSnapshotCoordinator.Admission unavailable = coordinator.enter(NOW);
|
||||
assertThat(unavailable.skipReason()).isEqualTo(ShadowSkipReason.NOT_READY);
|
||||
coordinator.exit(unavailable);
|
||||
|
||||
publish(coordinator, snapshot(6, "01", NOW));
|
||||
assertThat(coordinator.phase()).isEqualTo(ShadowSnapshotCoordinator.Phase.READY);
|
||||
}
|
||||
|
||||
private ShadowSnapshotCoordinator coordinatorAt(String warehouseId) {
|
||||
return new ShadowSnapshotCoordinator(warehouseId, Duration.ofSeconds(60));
|
||||
}
|
||||
|
||||
private static PublishedShadowSnapshot snapshot(long generation, String warehouseId, Instant synchronizedAt) {
|
||||
return new PublishedShadowSnapshot(generation, warehouseId, synchronizedAt, "signature-" + generation);
|
||||
}
|
||||
|
||||
private static void publish(ShadowSnapshotCoordinator coordinator, PublishedShadowSnapshot snapshot) {
|
||||
ShadowSnapshotCoordinator.RefreshTicket refresh = coordinator.tryBeginRefresh(NOW).orElseThrow();
|
||||
assertThat(coordinator.publish(refresh, snapshot, NOW)).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user