feat: coordinate stable shadow snapshots

This commit is contained in:
zgw
2026-09-03 11:27:26 +08:00
parent 03ddf3f386
commit c6a8b4f3b2
4 changed files with 394 additions and 0 deletions

View File

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