From 496bdd1468dfe3b51b9fd92b3a5677aea939836b Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:15:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20DualInflowControlManager=20DAO=20?= =?UTF-8?q?=EC=9D=B4=EC=A4=91=20=ED=98=B8=EC=B6=9C=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=83=80=EA=B2=9F=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 베이스 맵 메서드 오버라이드(getAdapterAllKeys, getInterfaceAllKeys, getAdapterInflowThreashold, getInterfaceInflowThreashold, removeAdapter, removeInterface)로 DAO 이중 호출 제거 - super.reload*() 호출 제거 - 어댑터/인터페이스/클라이언트별 TargetMetrics 수집 추가 (allowed, rejectedPerSecond, rejectedThreshold) - 메트릭 조회/초기화 API 메서드 추가 - 단위테스트: DualInflowControlManagerBucketMethodsTest(18건), DualInflowControlManagerMetricsTest 확장 Co-Authored-By: Claude Sonnet 4.6 --- .../inflow/dual/DualInflowControlManager.java | 129 +++++-- ...InflowControlManagerBucketMethodsTest.java | 280 ++++++++++++++ .../DualInflowControlManagerMetricsTest.java | 364 +++++++++++++++++- 3 files changed, 744 insertions(+), 29 deletions(-) create mode 100644 src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerBucketMethodsTest.java diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java index 7b91c9b..de1b827 100644 --- a/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java +++ b/src/main/java/com/eactive/eai/common/inflow/dual/DualInflowControlManager.java @@ -1,26 +1,22 @@ package com.eactive.eai.common.inflow.dual; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.Bucket; import com.eactive.eai.common.inflow.CustomGroupBucket; -import com.eactive.eai.common.inflow.InflowControlDAO; -import com.eactive.eai.common.inflow.InflowControlManager; import com.eactive.eai.common.inflow.InflowTargetVO; import com.eactive.eai.common.inflow.InflowType; import com.eactive.eai.common.lifecycle.LifecycleException; -import com.eactive.eai.common.message.EAIMessage; -import com.eactive.eai.common.server.EAIServerManager; import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.Logger; @@ -41,7 +37,7 @@ import io.github.bucket4j.local.LocalBucket; *
inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
*/ @Component -public class DualInflowControlManager extends InflowControlManager implements DualBucket { +public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket { private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); @@ -50,18 +46,14 @@ public class DualInflowControlManager extends InflowControlManager implements Du return ApplicationContextProvider.getContext().getBean(DualInflowControlManager.class); } - public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) { - return InflowControlManager.isTargetOfInflowControl(eaiServerManager, eaiMessage); - } - - @Autowired - private InflowControlDAO dualDao; - private volatile Map adapterBucketMap = new ConcurrentHashMap<>(); private volatile Map interfaceBucketMap = new ConcurrentHashMap<>(); private volatile Map clientBucketMap = new ConcurrentHashMap<>(); - private final ConcurrentHashMap groupMetricsMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap adapterMetricsMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap interfaceMetricsMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap clientMetricsMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap groupMetricsMap = new ConcurrentHashMap<>(); public static class TargetMetrics { public final AtomicLong allowed = new AtomicLong(); @@ -114,7 +106,7 @@ public class DualInflowControlManager extends InflowControlManager implements Du private Map loadBucketMap(InflowType type) throws Exception { Map newMap = new HashMap<>(); - for (InflowTargetVO vo : dualDao.getInflowList(type)) { + for (InflowTargetVO vo : inflowControlDAO.getInflowList(type)) { DualCustomBucket bucket = makeBucket(vo); if (bucket != null) newMap.put(vo.getName(), bucket); } @@ -123,38 +115,44 @@ public class DualInflowControlManager extends InflowControlManager implements Du @Override public synchronized void reloadAdapter() throws Exception { - super.reloadAdapter(); initAdapters(); + adapterMetricsMap.clear(); } @Override public synchronized void reloadAdapter(String adapter) throws Exception { - super.reloadAdapter(adapter); reloadBucketMap(adapterBucketMap, InflowType.ADAPTER, adapter); + TargetMetrics m = adapterMetricsMap.get(adapter); + if (m != null) m.reset(); } @Override public synchronized void reloadInterface() throws Exception { - super.reloadInterface(); initInterfaces(); + interfaceMetricsMap.clear(); } @Override public synchronized void reloadInterface(String inter) throws Exception { - super.reloadInterface(inter); reloadBucketMap(interfaceBucketMap, InflowType.INTERFACE, inter); + TargetMetrics m = interfaceMetricsMap.get(inter); + if (m != null) m.reset(); } public synchronized void reloadClient() throws Exception { initClients(); + clientMetricsMap.clear(); } public synchronized void reloadClient(String clientId) throws Exception { reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId); + TargetMetrics m = clientMetricsMap.get(clientId); + if (m != null) m.reset(); } public synchronized void removeClient(String clientId) { clientBucketMap.remove(clientId); + clientMetricsMap.remove(clientId); } @Override @@ -171,7 +169,7 @@ public class DualInflowControlManager extends InflowControlManager implements Du } private void reloadBucketMap(Map targetMap, InflowType type, String name) throws Exception { - Map updated = dualDao.getInflowMap(type, name); + Map updated = inflowControlDAO.getInflowMap(type, name); if (updated == null) return; for (InflowTargetVO vo : updated.values()) { DualCustomBucket bucket = makeBucket(vo); @@ -180,6 +178,26 @@ public class DualInflowControlManager extends InflowControlManager implements Du } } + // ------------------------------------------------------------------------- + // 어댑터/인터페이스/클라이언트 메트릭 접근자 + // ------------------------------------------------------------------------- + + public TargetMetrics getAdapterMetrics(String adapter) { return adapterMetricsMap.get(adapter); } + public TargetMetrics getInterfaceMetrics(String inter) { return interfaceMetricsMap.get(inter); } + public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); } + + public Map getAllAdapterMetrics() { return Collections.unmodifiableMap(adapterMetricsMap); } + public Map getAllInterfaceMetrics() { return Collections.unmodifiableMap(interfaceMetricsMap); } + public Map getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); } + + public void resetAdapterMetrics(String adapter) { TargetMetrics m = adapterMetricsMap.get(adapter); if (m != null) m.reset(); } + public void resetInterfaceMetrics(String inter) { TargetMetrics m = interfaceMetricsMap.get(inter); if (m != null) m.reset(); } + public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); } + + public void resetAllAdapterMetrics() { adapterMetricsMap.values().forEach(TargetMetrics::reset); } + public void resetAllInterfaceMetrics() { interfaceMetricsMap.values().forEach(TargetMetrics::reset); } + public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); } + // ------------------------------------------------------------------------- // 그룹 메트릭 — isGroupPass() 카운팅 및 접근자 // ------------------------------------------------------------------------- @@ -222,19 +240,35 @@ public class DualInflowControlManager extends InflowControlManager implements Du @Override public String isAdapterPassDetail(String adapter) { DualCustomBucket b = adapterBucketMap.get(adapter); - return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume(); + if (b == null) return DualCustomBucket.RESULT_PASS; + String result = b.tryConsume(); + recordMetrics(adapterMetricsMap, adapter, result); + return result; } @Override public String isInterfacePassDetail(String inter) { DualCustomBucket b = interfaceBucketMap.get(inter); - return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume(); + if (b == null) return DualCustomBucket.RESULT_PASS; + String result = b.tryConsume(); + recordMetrics(interfaceMetricsMap, inter, result); + return result; } @Override public String isClientPassDetail(String clientId) { DualCustomBucket b = clientBucketMap.get(clientId); - return (b == null) ? DualCustomBucket.RESULT_PASS : b.tryConsume(); + if (b == null) return DualCustomBucket.RESULT_PASS; + String result = b.tryConsume(); + recordMetrics(clientMetricsMap, clientId, result); + return result; + } + + private void recordMetrics(ConcurrentHashMap 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")); + } }