فهرست منبع

fix(cockpit): revalidate cached snapshot before return

Kazerin 3 هفته پیش
والد
کامیت
6bbb0ae38d

+ 22 - 0
pipe-network-service/zksy-admin/src/test/java/com/zksy/web/controller/cockpit/BusinessCockpitMapSnapshotAdapterTest.java

@@ -341,6 +341,26 @@ class BusinessCockpitMapSnapshotAdapterTest {
         verify(values, never()).setIfAbsent(any(), any(), anyLong(), any());
     }
 
+    @Test
+    void cachedSnapshotIsRejectedWhenClockExpiresBeforeReturn() {
+        Clock advancingClock = mock(Clock.class);
+        when(advancingClock.getZone()).thenReturn(ZoneOffset.UTC);
+        when(advancingClock.instant()).thenReturn(
+                Instant.parse("2026-09-02T02:00:00Z"),
+                Instant.parse("2026-09-02T02:00:00Z"),
+                Instant.parse("2026-09-02T02:09:59.999Z"),
+                Instant.parse("2026-09-02T02:10:00.001Z"));
+        when(monitorPointService.list()).thenReturn(Collections.emptyList());
+        when(equipmentBaseService.list()).thenReturn(Collections.emptyList());
+        when(equipmentTypeService.list()).thenReturn(Collections.emptyList());
+        BusinessCockpitMapSnapshotAdapter advancingAdapter = new BusinessCockpitMapSnapshotAdapter(
+                monitorPointService, equipmentBaseService, equipmentTypeService, deviceFactResolver, advancingClock);
+        String snapshotAt = "2026-09-02T10:00:00+08:00";
+        advancingAdapter.load(snapshotAt);
+
+        assertThrows(CockpitSnapshotExpiredException.class, () -> advancingAdapter.load(snapshotAt));
+    }
+
     @Test
     void expiredSnapshotAtIsRejectedInsteadOfLoadingCurrentRelations() {
         when(monitorPointService.list()).thenReturn(Collections.emptyList());
@@ -349,6 +369,8 @@ class BusinessCockpitMapSnapshotAdapterTest {
                 () -> adapter.load("2026-09-02T09:50:00+08:00"));
         assertThrows(CockpitSnapshotExpiredException.class,
                 () -> adapter.load("2026-09-02T09:49:59+08:00"));
+        assertThrows(CockpitSnapshotExpiredException.class,
+                () -> adapter.load("2026-09-02T10:10:00+08:00"));
     }
 
     @Test

+ 13 - 8
pipe-network-service/zksy-system/src/main/java/com/zksy/cockpit/map/BusinessCockpitMapSnapshotAdapter.java

@@ -101,15 +101,13 @@ public class BusinessCockpitMapSnapshotAdapter implements CockpitMapSnapshotAdap
         String resolvedSnapshotAt = snapshotAt == null ? businessTime() : snapshotAt;
         OffsetDateTime requestedAt = OffsetDateTime.parse(resolvedSnapshotAt);
         OffsetDateTime now = OffsetDateTime.now(clock);
-        if (!requestedAt.isAfter(now.minus(SNAPSHOT_RETENTION))
-                || !requestedAt.isBefore(now.plus(SNAPSHOT_RETENTION))) {
-            throw new CockpitSnapshotExpiredException("snapshot timestamp outside retention window");
-        }
+        validateSnapshotWindow(requestedAt, now, "snapshot timestamp outside retention window");
 
         snapshots.keySet().removeIf(cachedAt ->
                 !OffsetDateTime.parse(cachedAt).isAfter(now.minus(SNAPSHOT_RETENTION)));
         CockpitMapSnapshot cached = snapshots.get(resolvedSnapshotAt);
         if (cached != null) {
+            validateSnapshotWindow(requestedAt, OffsetDateTime.now(clock), "snapshot timestamp expired during load");
             return cached;
         }
 
@@ -120,6 +118,8 @@ public class BusinessCockpitMapSnapshotAdapter implements CockpitMapSnapshotAdap
                 if (snapshots.size() < MAX_CACHED_SNAPSHOTS) {
                     snapshots.putIfAbsent(resolvedSnapshotAt, shared);
                 }
+                validateSnapshotWindow(requestedAt, OffsetDateTime.now(clock),
+                        "snapshot timestamp expired during load");
                 return shared;
             }
         }
@@ -129,10 +129,7 @@ public class BusinessCockpitMapSnapshotAdapter implements CockpitMapSnapshotAdap
 
         CockpitMapSnapshot built = buildSnapshot(resolvedSnapshotAt);
         OffsetDateTime writeTime = OffsetDateTime.now(clock);
-        if (!requestedAt.isAfter(writeTime.minus(SNAPSHOT_RETENTION))
-                || !requestedAt.isBefore(writeTime.plus(SNAPSHOT_RETENTION))) {
-            throw new CockpitSnapshotExpiredException("snapshot timestamp expired during load");
-        }
+        validateSnapshotWindow(requestedAt, writeTime, "snapshot timestamp expired during load");
         if (redisTemplate != null) {
             Duration remaining = Duration.between(writeTime, requestedAt.plus(SNAPSHOT_RETENTION));
             long ttlSeconds = Math.max(1,
@@ -153,6 +150,14 @@ public class BusinessCockpitMapSnapshotAdapter implements CockpitMapSnapshotAdap
         return snapshots.getOrDefault(resolvedSnapshotAt, built);
     }
 
+    private static void validateSnapshotWindow(
+            OffsetDateTime requestedAt, OffsetDateTime now, String message) {
+        if (!requestedAt.isAfter(now.minus(SNAPSHOT_RETENTION))
+                || !requestedAt.isBefore(now.plus(SNAPSHOT_RETENTION))) {
+            throw new CockpitSnapshotExpiredException(message);
+        }
+    }
+
     private CockpitMapSnapshot buildSnapshot(String resolvedSnapshotAt) {
 
         Map<Integer, EquipmentType> types = equipmentTypes();