From 03ddf3f38645bdc6320a7dd8eb94197a8522fecb Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Thu, 3 Sep 2026 10:52:01 +0800 Subject: [PATCH] feat: validate shadow v3 snapshot settings --- .../shadow/ShadowObservationProperties.java | 18 ++++++++++++++++++ .../algorithm/shadow/ShadowSafetyGuard.java | 12 ++++++++++-- .../shadow/WarehouseIdNormalizer.java | 19 +++++++++++++++++++ .../src/main/resources/application.yml | 2 ++ .../shadow/ShadowConfigurationTest.java | 2 ++ .../shadow/ShadowSafetyGuardTest.java | 17 +++++++++++++++++ .../shadow/WarehouseIdNormalizerTest.java | 15 +++++++++++++++ 7 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java index 7d7588a..52df011 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowObservationProperties.java @@ -9,6 +9,8 @@ public class ShadowObservationProperties { private boolean enabled; private String warehouseId = ""; private int timeoutSeconds = 3; + private int snapshotMaxAgeSeconds = 60; + private int quietIntervalMillis = 1000; public boolean isEnabled() { return enabled; @@ -33,4 +35,20 @@ public class ShadowObservationProperties { public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } + + public int getSnapshotMaxAgeSeconds() { + return snapshotMaxAgeSeconds; + } + + public void setSnapshotMaxAgeSeconds(int snapshotMaxAgeSeconds) { + this.snapshotMaxAgeSeconds = snapshotMaxAgeSeconds; + } + + public int getQuietIntervalMillis() { + return quietIntervalMillis; + } + + public void setQuietIntervalMillis(int quietIntervalMillis) { + this.quietIntervalMillis = quietIntervalMillis; + } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java index 6fbad1b..dd50fc4 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuard.java @@ -33,8 +33,16 @@ public class ShadowSafetyGuard implements InitializingBean { if (!algorithmProperties.isDatabaseWritesEnabled()) { throw new IllegalStateException("Shadow observation requires algorithm database writes for alg.* read models"); } - if (shadowProperties.getTimeoutSeconds() <= 0) { - throw new IllegalStateException("Shadow observation timeout must be positive"); + if (shadowProperties.getTimeoutSeconds() < 1 || shadowProperties.getTimeoutSeconds() > 30) { + throw new IllegalStateException("Shadow observation timeout must be between 1 and 30 seconds"); + } + if (shadowProperties.getSnapshotMaxAgeSeconds() < 5 + || shadowProperties.getSnapshotMaxAgeSeconds() > 300) { + throw new IllegalStateException("Shadow snapshot maximum age must be between 5 and 300 seconds"); + } + if (shadowProperties.getQuietIntervalMillis() < 100 + || shadowProperties.getQuietIntervalMillis() > 10000) { + throw new IllegalStateException("Shadow quiet interval must be between 100 and 10000 milliseconds"); } } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java new file mode 100644 index 0000000..5932423 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizer.java @@ -0,0 +1,19 @@ +package com.ruoyi.algorithm.shadow; + +import java.math.BigInteger; + +final class WarehouseIdNormalizer { + static boolean equivalent(String first, String second) { + if (first == null || second == null) { + return false; + } + String left = first.trim(); + String right = second.trim(); + if (left.matches("[0-9]+") && right.matches("[0-9]+")) { + return new BigInteger(left).equals(new BigInteger(right)); + } + return left.equals(right); + } + + private WarehouseIdNormalizer() { } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 3ac8dc2..f589b94 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -185,6 +185,8 @@ fortucky: enabled: ${FORTUCKY_SHADOW_ENABLED:false} warehouse-id: ${FORTUCKY_SHADOW_WAREHOUSE_ID:} timeout-seconds: ${FORTUCKY_SHADOW_TIMEOUT_SECONDS:3} + snapshot-max-age-seconds: ${FORTUCKY_SHADOW_SNAPSHOT_MAX_AGE_SECONDS:60} + quiet-interval-millis: ${FORTUCKY_SHADOW_QUIET_INTERVAL_MILLIS:1000} algorithm: mode: ${FORTUCKY_ALGORITHM_MODE:SHADOW} warehouse-id: ${FORTUCKY_ALGORITHM_WAREHOUSE_ID:01} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java index a4171be..fcad020 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowConfigurationTest.java @@ -33,6 +33,8 @@ class ShadowConfigurationTest { assertThat(shadow.isEnabled()).isFalse(); assertThat(shadow.getWarehouseId()).isEmpty(); assertThat(shadow.getTimeoutSeconds()).isEqualTo(3); + assertThat(shadow.getSnapshotMaxAgeSeconds()).isEqualTo(60); + assertThat(shadow.getQuietIntervalMillis()).isEqualTo(1000); assertThat(algorithm.getMode()).isEqualTo(SchedulingMode.SHADOW); assertThat(algorithm.isDatabaseWritesEnabled()).isTrue(); assertThat(algorithm.getWarehouseId()).isEqualTo("01"); diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java index e9979d5..86feb04 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowSafetyGuardTest.java @@ -59,6 +59,23 @@ class ShadowSafetyGuardTest { .doesNotThrowAnyException(); } + @Test + void rejectsSnapshotAndQuietIntervalOutsideValidatedRanges() { + ShadowObservationProperties shadow = enabledShadow(); + AlgorithmProperties algorithm = new AlgorithmProperties(); + algorithm.setMode(SchedulingMode.SHADOW); + algorithm.setDatabaseWritesEnabled(true); + + shadow.setSnapshotMaxAgeSeconds(4); + assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate()) + .hasMessageContaining("between 5 and 300"); + + shadow.setSnapshotMaxAgeSeconds(60); + shadow.setQuietIntervalMillis(10001); + assertThatThrownBy(() -> new ShadowSafetyGuard(shadow, algorithm).validate()) + .hasMessageContaining("between 100 and 10000"); + } + private ShadowObservationProperties enabledShadow() { ShadowObservationProperties properties = new ShadowObservationProperties(); properties.setEnabled(true); diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java new file mode 100644 index 0000000..f6b337d --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/WarehouseIdNormalizerTest.java @@ -0,0 +1,15 @@ +package com.ruoyi.algorithm.shadow; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class WarehouseIdNormalizerTest { + @Test + void comparesNumericIdsByValueAndOtherIdsAfterTrimming() { + assertThat(WarehouseIdNormalizer.equivalent("1", "01")).isTrue(); + assertThat(WarehouseIdNormalizer.equivalent(" 01 ", "1")).isTrue(); + assertThat(WarehouseIdNormalizer.equivalent("W02", "01")).isFalse(); + assertThat(WarehouseIdNormalizer.equivalent(" W02 ", "W02")).isTrue(); + } +}