feat: validate shadow v3 snapshot settings

This commit is contained in:
zgw
2026-09-03 10:52:01 +08:00
parent 8816ffe7f4
commit 03ddf3f386
7 changed files with 83 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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