metricsMap, String key, String result) {
+ TargetMetrics m = metricsMap.computeIfAbsent(key, k -> new TargetMetrics());
+ if (result == null) m.allowed.incrementAndGet();
+ else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) m.rejectedPerSecond.incrementAndGet();
+ else m.rejectedThreshold.incrementAndGet();
}
@Override
@@ -259,6 +293,49 @@ public class DualInflowControlManager extends InflowControlManager implements Du
return isInterfacePassDetail(inter) == DualCustomBucket.RESULT_PASS;
}
+ // -------------------------------------------------------------------------
+ // 어댑터/인터페이스 키·VO·삭제 — Dual 맵 기준으로 오버라이드
+ // (base의 adapterBucketList / interfaceBucketList 는 읽지 않아 DAO 이중 호출 제거)
+ // -------------------------------------------------------------------------
+
+ @Override
+ public String[] getAdapterAllKeys() {
+ String[] keys = adapterBucketMap.keySet().toArray(new String[0]);
+ Arrays.sort(keys);
+ return keys;
+ }
+
+ @Override
+ public String[] getInterfaceAllKeys() {
+ String[] keys = interfaceBucketMap.keySet().toArray(new String[0]);
+ Arrays.sort(keys);
+ return keys;
+ }
+
+ @Override
+ public InflowTargetVO getAdapterInflowThreashold(String adapter) {
+ DualCustomBucket b = adapterBucketMap.get(adapter);
+ return (b == null) ? null : b.getInflowTargetVo();
+ }
+
+ @Override
+ public InflowTargetVO getInterfaceInflowThreashold(String inter) {
+ DualCustomBucket b = interfaceBucketMap.get(inter);
+ return (b == null) ? null : b.getInflowTargetVo();
+ }
+
+ @Override
+ public synchronized void removeAdapter(String adapter) {
+ adapterBucketMap.remove(adapter);
+ adapterMetricsMap.remove(adapter);
+ }
+
+ @Override
+ public synchronized void removeInterface(String inter) {
+ interfaceBucketMap.remove(inter);
+ interfaceMetricsMap.remove(inter);
+ }
+
// -------------------------------------------------------------------------
// 모니터링용 접근자
// -------------------------------------------------------------------------
@@ -292,7 +369,7 @@ public class DualInflowControlManager extends InflowControlManager implements Du
// -------------------------------------------------------------------------
private DualCustomBucket makeBucket(InflowTargetVO vo) {
- if (!InflowControlManager.isInflowTarget(vo)) {
+ if (!AbstractInflowControlManager.isInflowTarget(vo)) {
return null;
}
@@ -308,7 +385,7 @@ public class DualInflowControlManager extends InflowControlManager implements Du
if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) {
thresholdBucket = Bucket4j.builder()
.addLimit(Bandwidth.simple(vo.getThreshold(),
- InflowControlManager.getPeriod(vo.getThresholdTimeUnit())))
+ AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit())))
.build();
}
diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerBucketMethodsTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerBucketMethodsTest.java
new file mode 100644
index 0000000..6b5166c
--- /dev/null
+++ b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerBucketMethodsTest.java
@@ -0,0 +1,280 @@
+package com.eactive.eai.common.inflow.dual;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Duration;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.eactive.eai.common.inflow.InflowTargetVO;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket4j;
+import io.github.bucket4j.local.LocalBucket;
+
+/**
+ * DualInflowControlManager의 Dual 맵 기반 오버라이드 메서드 단위 테스트.
+ *
+ * 검증 포인트:
+ * - getAdapterAllKeys / getInterfaceAllKeys 가 Dual 맵(adapterBucketMap/interfaceBucketMap) 을 사용
+ * - getAdapterInflowThreashold / getInterfaceInflowThreashold 가 Dual 맵의 VO 를 반환
+ * - removeAdapter / removeInterface 가 Dual 맵에서 항목을 제거
+ * - base 의 adapterBucketList / interfaceBucketList 상태와 무관하게 동작 (DAO 이중 호출 제거 효과)
+ */
+class DualInflowControlManagerBucketMethodsTest {
+
+ private DualInflowControlManager manager;
+
+ @BeforeEach
+ void setUp() {
+ manager = new DualInflowControlManager();
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private InflowTargetVO makeVO(String name) {
+ InflowTargetVO vo = new InflowTargetVO();
+ vo.setName(name);
+ vo.setThresholdPerSecond(100);
+ vo.setThreshold(10000);
+ vo.setThresholdTimeUnit("DAY");
+ vo.setActivate(true);
+ return vo;
+ }
+
+ private LocalBucket bucket(long capacity) {
+ return Bucket4j.builder()
+ .addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(1)))
+ .build();
+ }
+
+ private DualCustomBucket dualBucket(String name) {
+ return new DualCustomBucket(bucket(100), bucket(1000), makeVO(name));
+ }
+
+ private void setAdapterMap(ConcurrentHashMap map) {
+ ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
+ }
+
+ private void setInterfaceMap(ConcurrentHashMap map) {
+ ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
+ }
+
+ // =========================================================================
+ // getAdapterAllKeys
+ // =========================================================================
+
+ @Test
+ void getAdapterAllKeys_emptyMap_returnsEmptyArray() {
+ setAdapterMap(new ConcurrentHashMap<>());
+
+ assertEquals(0, manager.getAdapterAllKeys().length);
+ }
+
+ @Test
+ void getAdapterAllKeys_twoEntries_returnsSortedKeys() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
+ map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
+ setAdapterMap(map);
+
+ String[] keys = manager.getAdapterAllKeys();
+
+ assertArrayEquals(new String[]{"ADAPTER_A", "ADAPTER_B"}, keys);
+ }
+
+ @Test
+ void getAdapterAllKeys_usesDualMap_notBaseList() {
+ // Dual 맵에만 항목 존재 — base 의 adapterBucketList 는 비어 있음
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("DUAL_ONLY", dualBucket("DUAL_ONLY"));
+ setAdapterMap(map);
+
+ String[] keys = manager.getAdapterAllKeys();
+
+ assertEquals(1, keys.length);
+ assertEquals("DUAL_ONLY", keys[0]);
+ }
+
+ // =========================================================================
+ // getInterfaceAllKeys
+ // =========================================================================
+
+ @Test
+ void getInterfaceAllKeys_emptyMap_returnsEmptyArray() {
+ setInterfaceMap(new ConcurrentHashMap<>());
+
+ assertEquals(0, manager.getInterfaceAllKeys().length);
+ }
+
+ @Test
+ void getInterfaceAllKeys_threeEntries_returnsSortedKeys() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("IF_C", dualBucket("IF_C"));
+ map.put("IF_A", dualBucket("IF_A"));
+ map.put("IF_B", dualBucket("IF_B"));
+ setInterfaceMap(map);
+
+ String[] keys = manager.getInterfaceAllKeys();
+
+ assertArrayEquals(new String[]{"IF_A", "IF_B", "IF_C"}, keys);
+ }
+
+ @Test
+ void getInterfaceAllKeys_usesDualMap_notBaseList() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("DUAL_IF", dualBucket("DUAL_IF"));
+ setInterfaceMap(map);
+
+ String[] keys = manager.getInterfaceAllKeys();
+
+ assertEquals(1, keys.length);
+ assertEquals("DUAL_IF", keys[0]);
+ }
+
+ // =========================================================================
+ // getAdapterInflowThreashold
+ // =========================================================================
+
+ @Test
+ void getAdapterInflowThreashold_found_returnsVO() {
+ InflowTargetVO vo = makeVO("ADAPTER_A");
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("ADAPTER_A", new DualCustomBucket(bucket(10), bucket(100), vo));
+ setAdapterMap(map);
+
+ InflowTargetVO result = manager.getAdapterInflowThreashold("ADAPTER_A");
+
+ assertSame(vo, result);
+ }
+
+ @Test
+ void getAdapterInflowThreashold_notFound_returnsNull() {
+ setAdapterMap(new ConcurrentHashMap<>());
+
+ assertNull(manager.getAdapterInflowThreashold("MISSING"));
+ }
+
+ @Test
+ void getAdapterInflowThreashold_usesDualMap_notBaseList() {
+ // base adapterBucketList 에는 항목 없고 Dual 맵에만 존재
+ InflowTargetVO vo = makeVO("ADAPTER_A");
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("ADAPTER_A", new DualCustomBucket(null, null, vo));
+ setAdapterMap(map);
+
+ assertSame(vo, manager.getAdapterInflowThreashold("ADAPTER_A"));
+ }
+
+ // =========================================================================
+ // getInterfaceInflowThreashold
+ // =========================================================================
+
+ @Test
+ void getInterfaceInflowThreashold_found_returnsVO() {
+ InflowTargetVO vo = makeVO("IF_001");
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("IF_001", new DualCustomBucket(bucket(10), bucket(100), vo));
+ setInterfaceMap(map);
+
+ assertSame(vo, manager.getInterfaceInflowThreashold("IF_001"));
+ }
+
+ @Test
+ void getInterfaceInflowThreashold_notFound_returnsNull() {
+ setInterfaceMap(new ConcurrentHashMap<>());
+
+ assertNull(manager.getInterfaceInflowThreashold("MISSING"));
+ }
+
+ @Test
+ void getInterfaceInflowThreashold_usesDualMap_notBaseList() {
+ InflowTargetVO vo = makeVO("IF_001");
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("IF_001", new DualCustomBucket(null, null, vo));
+ setInterfaceMap(map);
+
+ assertSame(vo, manager.getInterfaceInflowThreashold("IF_001"));
+ }
+
+ // =========================================================================
+ // removeAdapter
+ // =========================================================================
+
+ @Test
+ void removeAdapter_existingKey_removedFromDualMap() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
+ map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
+ setAdapterMap(map);
+
+ manager.removeAdapter("ADAPTER_A");
+
+ assertNull(manager.getAdapterBucket("ADAPTER_A"));
+ assertNotNull(manager.getAdapterBucket("ADAPTER_B"));
+ }
+
+ @Test
+ void removeAdapter_keysReflectRemoval() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("ADAPTER_A", dualBucket("ADAPTER_A"));
+ map.put("ADAPTER_B", dualBucket("ADAPTER_B"));
+ setAdapterMap(map);
+
+ manager.removeAdapter("ADAPTER_A");
+
+ String[] keys = manager.getAdapterAllKeys();
+ assertEquals(1, keys.length);
+ assertEquals("ADAPTER_B", keys[0]);
+ }
+
+ @Test
+ void removeAdapter_missingKey_noException() {
+ setAdapterMap(new ConcurrentHashMap<>());
+
+ assertDoesNotThrow(() -> manager.removeAdapter("NOT_EXISTS"));
+ }
+
+ // =========================================================================
+ // removeInterface
+ // =========================================================================
+
+ @Test
+ void removeInterface_existingKey_removedFromDualMap() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("IF_001", dualBucket("IF_001"));
+ map.put("IF_002", dualBucket("IF_002"));
+ setInterfaceMap(map);
+
+ manager.removeInterface("IF_001");
+
+ assertNull(manager.getInterfaceBucket("IF_001"));
+ assertNotNull(manager.getInterfaceBucket("IF_002"));
+ }
+
+ @Test
+ void removeInterface_keysReflectRemoval() {
+ ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put("IF_001", dualBucket("IF_001"));
+ map.put("IF_002", dualBucket("IF_002"));
+ setInterfaceMap(map);
+
+ manager.removeInterface("IF_001");
+
+ String[] keys = manager.getInterfaceAllKeys();
+ assertEquals(1, keys.length);
+ assertEquals("IF_002", keys[0]);
+ }
+
+ @Test
+ void removeInterface_missingKey_noException() {
+ setInterfaceMap(new ConcurrentHashMap<>());
+
+ assertDoesNotThrow(() -> manager.removeInterface("NOT_EXISTS"));
+ }
+}
diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
index 6e59247..c52f309 100644
--- a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
+++ b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
@@ -12,16 +12,18 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.common.inflow.CustomGroupBucket;
import com.eactive.eai.common.inflow.InflowGroupVO;
+import com.eactive.eai.common.inflow.InflowTargetVO;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.local.LocalBucket;
/**
- * DualInflowControlManager 그룹 메트릭 단위 테스트.
+ * DualInflowControlManager 메트릭 단위 테스트.
*
- * start() 없이 groupBucketList를 ReflectionTestUtils로 직접 주입하여
- * isGroupPass() 카운팅과 getGroupMetrics / reset 메서드를 검증한다.
+ *
start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여
+ * isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의
+ * 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다.
*/
class DualInflowControlManagerMetricsTest {
@@ -68,6 +70,40 @@ class DualInflowControlManagerMetricsTest {
return new CustomGroupBucket(stableBucket(100), exhaustedBucket(1), makeGroupVO(groupId, groupId + "-name"));
}
+ private InflowTargetVO makeTargetVO(String name) {
+ InflowTargetVO vo = new InflowTargetVO();
+ vo.setName(name);
+ vo.setThresholdPerSecond(100);
+ vo.setThreshold(10000);
+ vo.setThresholdTimeUnit("DAY");
+ vo.setActivate(true);
+ return vo;
+ }
+
+ private DualCustomBucket passDualBucket(String name) {
+ return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
+ }
+
+ private DualCustomBucket blockedPerSecondDualBucket(String name) {
+ return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
+ }
+
+ private DualCustomBucket blockedThresholdDualBucket(String name) {
+ return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
+ }
+
+ private void injectAdapterMap(Map map) {
+ ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
+ }
+
+ private void injectInterfaceMap(Map map) {
+ ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
+ }
+
+ private void injectClientMap(Map map) {
+ ReflectionTestUtils.setField(manager, "clientBucketMap", map);
+ }
+
private void injectGroupBucketList(Map map) {
ReflectionTestUtils.setField(manager, "groupBucketList", map);
}
@@ -315,4 +351,326 @@ class DualInflowControlManagerMetricsTest {
assertNotNull(manager.getGroupMetrics("G1"));
assertNotNull(manager.getGroupMetrics("G2"));
}
+
+ // =========================================================================
+ // isAdapterPassDetail() — 카운터 증가
+ // =========================================================================
+
+ @Test
+ void isAdapterPassDetail_bucketNotRegistered_noMetricsCreated() {
+ // 버킷 없는 어댑터는 메트릭 맵에 항목을 생성하지 않음
+ manager.isAdapterPassDetail("UNKNOWN");
+
+ assertNull(manager.getAdapterMetrics("UNKNOWN"));
+ }
+
+ @Test
+ void isAdapterPassDetail_pass_incrementsAllowed() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", passDualBucket("A1"));
+ injectAdapterMap(map);
+
+ String result = manager.isAdapterPassDetail("A1");
+
+ assertNull(result);
+ DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
+ assertNotNull(m);
+ assertEquals(1, m.allowed.get());
+ assertEquals(0, m.rejectedPerSecond.get());
+ assertEquals(0, m.rejectedThreshold.get());
+ }
+
+ @Test
+ void isAdapterPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", blockedPerSecondDualBucket("A1"));
+ injectAdapterMap(map);
+
+ String result = manager.isAdapterPassDetail("A1");
+
+ assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, result);
+ DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
+ assertEquals(0, m.allowed.get());
+ assertEquals(1, m.rejectedPerSecond.get());
+ assertEquals(0, m.rejectedThreshold.get());
+ }
+
+ @Test
+ void isAdapterPassDetail_blockedThreshold_incrementsRejectedThreshold() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", blockedThresholdDualBucket("A1"));
+ injectAdapterMap(map);
+
+ String result = manager.isAdapterPassDetail("A1");
+
+ assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, result);
+ DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
+ assertEquals(0, m.allowed.get());
+ assertEquals(0, m.rejectedPerSecond.get());
+ assertEquals(1, m.rejectedThreshold.get());
+ }
+
+ @Test
+ void isAdapterPassDetail_multipleCalls_countersAccumulate() {
+ Map map = new ConcurrentHashMap<>();
+ LocalBucket b = stableBucket(3);
+ map.put("A1", new DualCustomBucket(b, null, makeTargetVO("A1")));
+ injectAdapterMap(map);
+
+ for (int i = 0; i < 5; i++) manager.isAdapterPassDetail("A1");
+
+ DualInflowControlManager.TargetMetrics m = manager.getAdapterMetrics("A1");
+ assertEquals(3, m.allowed.get());
+ assertEquals(2, m.rejectedPerSecond.get());
+ assertEquals(5, m.allowed.get() + m.rejectedPerSecond.get() + m.rejectedThreshold.get());
+ }
+
+ // =========================================================================
+ // isInterfacePassDetail() — 카운터 증가
+ // =========================================================================
+
+ @Test
+ void isInterfacePassDetail_bucketNotRegistered_noMetricsCreated() {
+ manager.isInterfacePassDetail("UNKNOWN");
+
+ assertNull(manager.getInterfaceMetrics("UNKNOWN"));
+ }
+
+ @Test
+ void isInterfacePassDetail_pass_incrementsAllowed() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF1", passDualBucket("IF1"));
+ injectInterfaceMap(map);
+
+ manager.isInterfacePassDetail("IF1");
+
+ assertEquals(1, manager.getInterfaceMetrics("IF1").allowed.get());
+ }
+
+ @Test
+ void isInterfacePassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF1", blockedPerSecondDualBucket("IF1"));
+ injectInterfaceMap(map);
+
+ manager.isInterfacePassDetail("IF1");
+
+ assertEquals(1, manager.getInterfaceMetrics("IF1").rejectedPerSecond.get());
+ }
+
+ @Test
+ void isInterfacePassDetail_differentInterfaces_independentMetrics() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF1", passDualBucket("IF1"));
+ map.put("IF2", blockedPerSecondDualBucket("IF2"));
+ injectInterfaceMap(map);
+
+ manager.isInterfacePassDetail("IF1");
+ manager.isInterfacePassDetail("IF1");
+ manager.isInterfacePassDetail("IF2");
+
+ assertEquals(2, manager.getInterfaceMetrics("IF1").allowed.get());
+ assertEquals(1, manager.getInterfaceMetrics("IF2").rejectedPerSecond.get());
+ }
+
+ // =========================================================================
+ // isClientPassDetail() — 카운터 증가
+ // =========================================================================
+
+ @Test
+ void isClientPassDetail_bucketNotRegistered_noMetricsCreated() {
+ manager.isClientPassDetail("UNKNOWN");
+
+ assertNull(manager.getClientMetrics("UNKNOWN"));
+ }
+
+ @Test
+ void isClientPassDetail_pass_incrementsAllowed() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("C1", passDualBucket("C1"));
+ injectClientMap(map);
+
+ manager.isClientPassDetail("C1");
+
+ assertEquals(1, manager.getClientMetrics("C1").allowed.get());
+ }
+
+ @Test
+ void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("C1", blockedThresholdDualBucket("C1"));
+ injectClientMap(map);
+
+ manager.isClientPassDetail("C1");
+
+ assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
+ }
+
+ // =========================================================================
+ // getAllXxxMetrics — 조회 및 불변성
+ // =========================================================================
+
+ @Test
+ void getAllAdapterMetrics_afterCalls_containsEntries() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", passDualBucket("A1"));
+ map.put("A2", passDualBucket("A2"));
+ injectAdapterMap(map);
+
+ manager.isAdapterPassDetail("A1");
+ manager.isAdapterPassDetail("A2");
+
+ Map all = manager.getAllAdapterMetrics();
+ assertEquals(2, all.size());
+ assertTrue(all.containsKey("A1"));
+ assertTrue(all.containsKey("A2"));
+ }
+
+ @Test
+ void getAllAdapterMetrics_returnsUnmodifiableView() {
+ Map all = manager.getAllAdapterMetrics();
+ assertThrows(UnsupportedOperationException.class, () -> all.put("NEW", null));
+ }
+
+ @Test
+ void getAllInterfaceMetrics_empty_returnsEmptyMap() {
+ assertTrue(manager.getAllInterfaceMetrics().isEmpty());
+ }
+
+ @Test
+ void getAllClientMetrics_empty_returnsEmptyMap() {
+ assertTrue(manager.getAllClientMetrics().isEmpty());
+ }
+
+ // =========================================================================
+ // resetXxxMetrics / resetAllXxxMetrics
+ // =========================================================================
+
+ @Test
+ void resetAdapterMetrics_resetsTargetAdapter() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", passDualBucket("A1"));
+ map.put("A2", passDualBucket("A2"));
+ injectAdapterMap(map);
+
+ manager.isAdapterPassDetail("A1");
+ manager.isAdapterPassDetail("A1");
+ manager.isAdapterPassDetail("A2");
+
+ manager.resetAdapterMetrics("A1");
+
+ assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
+ assertEquals(1, manager.getAdapterMetrics("A2").allowed.get()); // 영향 없음
+ }
+
+ @Test
+ void resetAdapterMetrics_nonExistent_noException() {
+ assertDoesNotThrow(() -> manager.resetAdapterMetrics("NONE"));
+ }
+
+ @Test
+ void resetAllAdapterMetrics_resetsAll() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", passDualBucket("A1"));
+ map.put("A2", passDualBucket("A2"));
+ injectAdapterMap(map);
+
+ manager.isAdapterPassDetail("A1");
+ manager.isAdapterPassDetail("A2");
+ manager.resetAllAdapterMetrics();
+
+ assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
+ assertEquals(0, manager.getAdapterMetrics("A2").allowed.get());
+ }
+
+ @Test
+ void resetInterfaceMetrics_resetsTargetInterface() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF1", passDualBucket("IF1"));
+ injectInterfaceMap(map);
+
+ manager.isInterfacePassDetail("IF1");
+ manager.isInterfacePassDetail("IF1");
+ manager.resetInterfaceMetrics("IF1");
+
+ assertEquals(0, manager.getInterfaceMetrics("IF1").allowed.get());
+ }
+
+ @Test
+ void resetAllInterfaceMetrics_emptyMap_noException() {
+ assertDoesNotThrow(() -> manager.resetAllInterfaceMetrics());
+ }
+
+ @Test
+ void resetClientMetrics_resetsTargetClient() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("C1", passDualBucket("C1"));
+ injectClientMap(map);
+
+ manager.isClientPassDetail("C1");
+ manager.resetClientMetrics("C1");
+
+ assertEquals(0, manager.getClientMetrics("C1").allowed.get());
+ }
+
+ @Test
+ void resetAllClientMetrics_resetsAll() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("C1", passDualBucket("C1"));
+ map.put("C2", passDualBucket("C2"));
+ injectClientMap(map);
+
+ manager.isClientPassDetail("C1");
+ manager.isClientPassDetail("C2");
+ manager.resetAllClientMetrics();
+
+ assertEquals(0, manager.getClientMetrics("C1").allowed.get());
+ assertEquals(0, manager.getClientMetrics("C2").allowed.get());
+ }
+
+ // =========================================================================
+ // remove 시 메트릭 자동 삭제
+ // =========================================================================
+
+ @Test
+ void removeAdapter_cleansUpMetrics() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("A1", passDualBucket("A1"));
+ ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
+
+ manager.isAdapterPassDetail("A1");
+ assertNotNull(manager.getAdapterMetrics("A1"));
+
+ manager.removeAdapter("A1");
+
+ assertNull(manager.getAdapterMetrics("A1"));
+ }
+
+ @Test
+ void removeInterface_cleansUpMetrics() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF1", passDualBucket("IF1"));
+ ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
+
+ manager.isInterfacePassDetail("IF1");
+ assertNotNull(manager.getInterfaceMetrics("IF1"));
+
+ manager.removeInterface("IF1");
+
+ assertNull(manager.getInterfaceMetrics("IF1"));
+ }
+
+ @Test
+ void removeClient_cleansUpMetrics() {
+ Map map = new ConcurrentHashMap<>();
+ map.put("C1", passDualBucket("C1"));
+ ReflectionTestUtils.setField(manager, "clientBucketMap", map);
+
+ manager.isClientPassDetail("C1");
+ assertNotNull(manager.getClientMetrics("C1"));
+
+ manager.removeClient("C1");
+
+ assertNull(manager.getClientMetrics("C1"));
+ }
}