This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 49720801a9 fix(admin): prevent scaling rules with the same metric from
overwriting each other (#7875)
49720801a9 is described below
commit 49720801a9de605ad97b62189ecd6894be967fc2
Author: Joker <[email protected]>
AuthorDate: Wed Oct 21 10:53:42 2026 +0800
fix(admin): prevent scaling rules with the same metric from overwriting
each other (#7875)
Co-authored-by: Mrmr <[email protected]>
---
.../monitor/subject/cache/ScaleRuleCache.java | 29 ++--
.../admin/service/impl/ScaleRuleServiceImpl.java | 9 +-
.../monitor/subject/cache/ScaleRuleCacheTest.java | 146 +++++++++++++++++++++
.../shenyu/admin/service/ScaleRuleServiceTest.java | 58 ++++++--
4 files changed, 213 insertions(+), 29 deletions(-)
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCache.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCache.java
index d206c4d711..2ec1df4ac2 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCache.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCache.java
@@ -20,9 +20,9 @@ package org.apache.shenyu.admin.scale.monitor.subject.cache;
import org.apache.shenyu.admin.model.entity.ScaleRuleDO;
import org.springframework.stereotype.Component;
-import java.util.HashSet;
+import java.util.Comparator;
import java.util.List;
-import java.util.Set;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Component
@@ -37,7 +37,7 @@ public class ScaleRuleCache {
*/
public void initialize(final List<ScaleRuleDO> rules) {
ruleCache.clear();
- rules.forEach(rule -> ruleCache.put(rule.getMetricName(), rule));
+ rules.forEach(rule -> ruleCache.put(rule.getId(), rule));
}
/**
@@ -46,7 +46,7 @@ public class ScaleRuleCache {
* @param rule rule
*/
public void addOrUpdateRuleToCache(final ScaleRuleDO rule) {
- ruleCache.put(rule.getMetricName(), rule);
+ ruleCache.put(rule.getId(), rule);
}
/**
@@ -55,7 +55,11 @@ public class ScaleRuleCache {
* @param metricNames metricNames
*/
public void removeRulesFromCache(final List<String> metricNames) {
- metricNames.forEach(ruleCache::remove);
+ ruleCache.forEach((id, rule) -> {
+ if (metricNames.contains(rule.getMetricName())) {
+ ruleCache.remove(id, rule);
+ }
+ });
}
/**
@@ -64,12 +68,7 @@ public class ScaleRuleCache {
* @param ids rule primary keys
*/
public void removeRulesByIdsFromCache(final List<String> ids) {
- final Set<String> idSet = new HashSet<>(ids);
- ruleCache.forEach((metricName, rule) -> {
- if (idSet.contains(rule.getId())) {
- ruleCache.remove(metricName, rule);
- }
- });
+ ids.forEach(ruleCache::remove);
}
/**
@@ -82,12 +81,16 @@ public class ScaleRuleCache {
}
/**
- * getRuleByMetricName.
+ * Gets the highest-priority rule for a metric, breaking equal priorities
by rule ID.
*
* @param metricName metricName
* @return ScaleRuleDO
*/
public ScaleRuleDO getRuleByMetricName(final String metricName) {
- return ruleCache.get(metricName);
+ return ruleCache.values().stream()
+ .filter(rule -> Objects.equals(metricName,
rule.getMetricName()))
+ .min(Comparator.comparing(ScaleRuleDO::getSort,
Comparator.nullsLast(Integer::compareTo))
+ .thenComparing(ScaleRuleDO::getId))
+ .orElse(null);
}
}
diff --git
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ScaleRuleServiceImpl.java
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ScaleRuleServiceImpl.java
index faa97abc36..56994834b8 100644
---
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ScaleRuleServiceImpl.java
+++
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/service/impl/ScaleRuleServiceImpl.java
@@ -34,7 +34,6 @@ import
org.springframework.transaction.support.TransactionSynchronization;
import
org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
-import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -125,17 +124,11 @@ public class ScaleRuleServiceImpl implements
ScaleRuleService {
*/
@Override
public int update(final ScaleRuleDTO scaleRuleDTO) {
- final ScaleRuleDO before =
scaleRuleMapper.selectByPrimaryKey(scaleRuleDTO.getId());
final ScaleRuleDO after = ScaleRuleDO.buildScaleRuleDO(scaleRuleDTO);
int rows = scaleRuleMapper.updateByPrimaryKeySelective(after);
if (rows > 0) {
final ScaleRuleDO persisted =
scaleRuleMapper.selectByPrimaryKey(scaleRuleDTO.getId());
- runAfterCommit(() -> {
- if (Objects.nonNull(before) &&
!Objects.equals(before.getMetricName(), after.getMetricName())) {
-
scaleRuleCache.removeRulesFromCache(List.of(before.getMetricName()));
- }
- scaleRuleCache.addOrUpdateRuleToCache(persisted);
- });
+ runAfterCommit(() ->
scaleRuleCache.addOrUpdateRuleToCache(persisted));
}
return rows;
}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCacheTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCacheTest.java
new file mode 100644
index 0000000000..4f6d223f12
--- /dev/null
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/scale/monitor/subject/cache/ScaleRuleCacheTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.scale.monitor.subject.cache;
+
+import org.apache.shenyu.admin.model.entity.ScaleRuleDO;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for the scaling rule cache.
+ */
+public final class ScaleRuleCacheTest {
+
+ @Test
+ public void testInitializeKeepsRulesWithSameMetric() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = ScaleRuleDO.builder()
+ .id("rule-a")
+ .metricName("cpu_usage")
+ .maximum("60")
+ .build();
+ final ScaleRuleDO second = ScaleRuleDO.builder()
+ .id("rule-b")
+ .metricName("cpu_usage")
+ .maximum("80")
+ .build();
+
+ cache.initialize(List.of(first, second));
+
+ assertEquals(2, cache.getAllRules().size());
+ }
+
+ @Test
+ public void testAddingRulesWithSameMetricKeepsBothRules() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = rule("a", "cpu_usage", 1);
+ final ScaleRuleDO second = rule("b", "cpu_usage", 2);
+
+ cache.addOrUpdateRuleToCache(first);
+ cache.addOrUpdateRuleToCache(second);
+
+ assertEquals(2, cache.getAllRules().size());
+ assertTrue(cache.getAllRules().containsAll(List.of(first, second)));
+ }
+
+ @Test
+ public void testUpdatingRuleReplacesOnlyItsOwnEntry() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = rule("a", "cpu_usage", 1);
+ final ScaleRuleDO second = rule("b", "cpu_usage", 2);
+ cache.initialize(List.of(first, second));
+ final ScaleRuleDO updated = rule("a", "cpu_usage", 3);
+
+ cache.addOrUpdateRuleToCache(updated);
+
+ assertEquals(2, cache.getAllRules().size());
+ assertTrue(cache.getAllRules().containsAll(List.of(updated, second)));
+ }
+
+ @Test
+ public void testRenamingRuleKeepsOtherRulesForOriginalMetric() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = rule("a", "cpu_usage", 1);
+ final ScaleRuleDO second = rule("b", "cpu_usage", 2);
+ cache.initialize(List.of(first, second));
+ final ScaleRuleDO renamed = rule("a", "memory_usage", 1);
+
+ cache.addOrUpdateRuleToCache(renamed);
+
+ assertEquals(2, cache.getAllRules().size());
+ assertSame(second, cache.getRuleByMetricName("cpu_usage"));
+ assertSame(renamed, cache.getRuleByMetricName("memory_usage"));
+ }
+
+ @Test
+ public void testDeletingRuleByIdKeepsOtherRulesForSameMetric() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = rule("a", "cpu_usage", 1);
+ final ScaleRuleDO second = rule("b", "cpu_usage", 2);
+ cache.initialize(List.of(first, second));
+
+ cache.removeRulesByIdsFromCache(List.of("b", "missing"));
+
+ assertEquals(List.of(first), cache.getAllRules());
+ }
+
+ @Test
+ public void testDeletingByMetricRemovesAllMatchingRules() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first = rule("a", "cpu_usage", 1);
+ final ScaleRuleDO second = rule("b", "cpu_usage", 2);
+ final ScaleRuleDO other = rule("c", "memory_usage", 1);
+ cache.initialize(List.of(first, second, other));
+
+ cache.removeRulesFromCache(List.of("cpu_usage"));
+
+ assertEquals(List.of(other), cache.getAllRules());
+ }
+
+ @Test
+ public void testInitializeReplacesPreviousRules() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ cache.initialize(List.of(rule("a", "cpu_usage", 1)));
+ final ScaleRuleDO replacement = rule("b", "memory_usage", 1);
+
+ cache.initialize(List.of(replacement));
+
+ assertEquals(List.of(replacement), cache.getAllRules());
+ }
+
+ @Test
+ public void testMetricLookupUsesPriorityAndStableIdOrder() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO expected = rule("a", "cpu_usage", 1);
+ cache.initialize(List.of(rule("c", "cpu_usage", 2), rule("b",
"cpu_usage", 1),
+ rule("d", "cpu_usage", null), expected));
+
+ assertSame(expected, cache.getRuleByMetricName("cpu_usage"));
+ assertNull(cache.getRuleByMetricName("missing"));
+ }
+
+ private ScaleRuleDO rule(final String id, final String metricName, final
Integer sort) {
+ return
ScaleRuleDO.builder().id(id).metricName(metricName).sort(sort).build();
+ }
+}
diff --git
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
index a49815f7db..f96990c1d7 100644
---
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
+++
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/ScaleRuleServiceTest.java
@@ -34,8 +34,10 @@ import
org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.transaction.support.TransactionSynchronizationUtils;
import java.sql.Timestamp;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -103,13 +105,12 @@ public final class ScaleRuleServiceTest {
ScaleRuleDTO dto = new ScaleRuleDTO();
dto.setId("r");
dto.setMetricName("new");
-
when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(ScaleRuleDO.builder().id("r").metricName("old").build(),
- ScaleRuleDO.builder().id("r").metricName("new").build());
+
when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(ScaleRuleDO.builder().id("r").metricName("new").build());
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
TransactionSynchronizationManager.initSynchronization();
scaleRuleService.update(dto);
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
- verify(scaleRuleCache).removeRulesFromCache(any());
+ verify(scaleRuleCache, never()).removeRulesFromCache(any());
verify(scaleRuleCache).addOrUpdateRuleToCache(any(ScaleRuleDO.class));
TransactionSynchronizationManager.clearSynchronization();
when(scaleRuleMapper.delete(any())).thenReturn(1);
@@ -125,7 +126,7 @@ public final class ScaleRuleServiceTest {
ScaleRuleDTO update = new ScaleRuleDTO();
update.setId("r");
ScaleRuleDO persisted =
ScaleRuleDO.builder().id("r").metricName("metric").build();
- when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null,
persisted);
+ when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(persisted);
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
assertEquals(1, scaleRuleService.createOrUpdate(update));
verify(scaleRuleMapper).insertSelective(any(ScaleRuleDO.class));
@@ -150,7 +151,6 @@ public final class ScaleRuleServiceTest {
public void testUpdateAndDeleteSkipCacheWhenMapperReturnsZero() {
ScaleRuleDTO dto = new ScaleRuleDTO();
dto.setId("r");
- when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null);
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(0);
assertEquals(0, scaleRuleService.update(dto));
when(scaleRuleMapper.delete(any())).thenReturn(0);
@@ -172,11 +172,10 @@ public final class ScaleRuleServiceTest {
}
@Test
- public void testUpdateWithoutPreviousRuleAndDeleteAfterCommit() {
+ public void testUpdateAndDeletePublishCacheChangesAfterCommit() {
ScaleRuleDTO dto = new ScaleRuleDTO();
dto.setId("r");
- when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(null,
- ScaleRuleDO.builder().id("r").metricName("metric").build());
+
when(scaleRuleMapper.selectByPrimaryKey("r")).thenReturn(ScaleRuleDO.builder().id("r").metricName("metric").build());
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
TransactionSynchronizationManager.initSynchronization();
scaleRuleService.update(dto);
@@ -191,4 +190,47 @@ public final class ScaleRuleServiceTest {
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
verify(scaleRuleCache).removeRulesByIdsFromCache(any());
}
+
+ @Test
+ public void testRenamePreservesOtherRulesWithSameMetricAfterCommit() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first =
ScaleRuleDO.builder().id("a").metricName("cpu_usage").build();
+ final ScaleRuleDO second =
ScaleRuleDO.builder().id("b").metricName("cpu_usage").build();
+ cache.initialize(List.of(first, second));
+ final ScaleRuleDO renamed =
ScaleRuleDO.builder().id("a").metricName("memory_usage").build();
+
when(scaleRuleMapper.updateByPrimaryKeySelective(any(ScaleRuleDO.class))).thenReturn(1);
+ when(scaleRuleMapper.selectByPrimaryKey("a")).thenReturn(renamed);
+ final ScaleRuleDTO update = new ScaleRuleDTO();
+ update.setId("a");
+ update.setMetricName("memory_usage");
+ final ScaleRuleService service = new
ScaleRuleServiceImpl(scaleRuleMapper, cache);
+ TransactionSynchronizationManager.initSynchronization();
+
+ service.update(update);
+
+ assertEquals(2, cache.getAllRules().size());
+ assertEquals("cpu_usage", first.getMetricName());
+ assertTrue(cache.getAllRules().contains(first));
+
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+ assertEquals(2, cache.getAllRules().size());
+ assertTrue(cache.getAllRules().contains(renamed));
+ assertTrue(cache.getAllRules().contains(second));
+ }
+
+ @Test
+ public void testDeletePreservesOtherRulesWithSameMetricAfterCommit() {
+ final ScaleRuleCache cache = new ScaleRuleCache();
+ final ScaleRuleDO first =
ScaleRuleDO.builder().id("a").metricName("cpu_usage").build();
+ final ScaleRuleDO second =
ScaleRuleDO.builder().id("b").metricName("cpu_usage").build();
+ cache.initialize(List.of(first, second));
+ when(scaleRuleMapper.delete(List.of("a"))).thenReturn(1);
+ final ScaleRuleService service = new
ScaleRuleServiceImpl(scaleRuleMapper, cache);
+ TransactionSynchronizationManager.initSynchronization();
+
+ service.delete(List.of("a"));
+
+ assertEquals(2, cache.getAllRules().size());
+
TransactionSynchronizationUtils.invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations());
+ assertEquals(List.of(second), cache.getAllRules());
+ }
}