Merge branch 'feature/inflow-control-improvement'

- AbstractInflowControlManager 상속 구조 리팩토링
- DualInflowControlManager DAO 이중 호출 제거 및 타겟 메트릭 추가
- ReloadInflowClientControlCommandTest / RemoveInflowClientControlCommandTest 수정
This commit is contained in:
curry772
2026-04-30 10:16:52 +09:00
16 changed files with 1643 additions and 596 deletions
@@ -0,0 +1,103 @@
package com.eactive.eai.agent.inflow;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.InflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
/**
* ReloadInflowClientControlCommand 단위 테스트.
*
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
* PropManager/ApplicationContext 없이 동작을 검증한다.
*/
class ReloadInflowClientControlCommandTest {
private DualInflowControlManager mockDualManager;
@BeforeEach
void setUp() {
mockDualManager = mock(DualInflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
@AfterEach
void tearDown() {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
private ReloadInflowClientControlCommand commandWith(Object args) {
ReloadInflowClientControlCommand cmd = new ReloadInflowClientControlCommand();
cmd.setArgs(args);
return cmd;
}
// =========================================================================
// args 타입 검증 실패
// =========================================================================
@Test
void execute_argsIsInteger_throwsCommandException() {
// args 검증이 getInflowControlManager() 호출 이전에 발생하므로 캐시 설정 불필요
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
}
// =========================================================================
// DualInflowControlManager 비활성
// =========================================================================
@Test
void execute_notDualManager_throwsCommandException() {
InflowControlManager baseMgr = mock(InflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
}
// =========================================================================
// 전체 재로드 (ALL)
// =========================================================================
@Test
void execute_argsIsALL_callsReloadClientNoArgs() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("ALL").execute();
assertEquals("success", result);
verify(mockDualManager).reloadClient();
verify(mockDualManager, never()).reloadClient(anyString());
}
// =========================================================================
// 개별 재로드 (ClientId)
// =========================================================================
@Test
void execute_argsIsClientId_callsReloadClientWithId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("CLIENT_A").execute();
assertEquals("success", result);
verify(mockDualManager).reloadClient("CLIENT_A");
verify(mockDualManager, never()).reloadClient();
}
@Test
void execute_differentClientId_callsReloadClientWithThatId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_B").execute();
verify(mockDualManager).reloadClient("CLIENT_B");
}
}
@@ -0,0 +1,96 @@
package com.eactive.eai.agent.inflow;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import com.eactive.eai.agent.command.CommandException;
import com.eactive.eai.common.inflow.InflowControlManager;
import com.eactive.eai.common.inflow.InflowControlUtil;
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
/**
* RemoveInflowClientControlCommand 단위 테스트.
*
* <p>InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여
* PropManager/ApplicationContext 없이 동작을 검증한다.
*/
class RemoveInflowClientControlCommandTest {
private DualInflowControlManager mockDualManager;
@BeforeEach
void setUp() {
mockDualManager = mock(DualInflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
@AfterEach
void tearDown() {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
}
private RemoveInflowClientControlCommand commandWith(Object args) {
RemoveInflowClientControlCommand cmd = new RemoveInflowClientControlCommand();
cmd.setArgs(args);
return cmd;
}
// =========================================================================
// args 타입 검증 실패
// =========================================================================
@Test
void execute_argsIsInteger_throwsCommandException() {
assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute);
}
// =========================================================================
// DualInflowControlManager 비활성
// =========================================================================
@Test
void execute_notDualManager_throwsCommandException() {
InflowControlManager baseMgr = mock(InflowControlManager.class);
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
assertThrows(CommandException.class, commandWith("CLIENT_A")::execute);
}
// =========================================================================
// 정상 제거
// =========================================================================
@Test
void execute_validClientId_callsRemoveClient() throws CommandException {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
Object result = commandWith("CLIENT_A").execute();
assertEquals("success", result);
verify(mockDualManager).removeClient("CLIENT_A");
}
@Test
void execute_differentClientId_callsRemoveClientWithThatId() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_B").execute();
verify(mockDualManager).removeClient("CLIENT_B");
}
@Test
void execute_validClientId_doesNotCallReload() throws Exception {
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager);
commandWith("CLIENT_A").execute();
verify(mockDualManager, never()).reloadClient();
verify(mockDualManager, never()).reloadClient(anyString());
}
}
@@ -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 맵 기반 오버라이드 메서드 단위 테스트.
*
* <p>검증 포인트:
* - 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<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
}
private void setInterfaceMap(ConcurrentHashMap<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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"));
}
}
@@ -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 메트릭 단위 테스트.
*
* <p>start() 없이 groupBucketList를 ReflectionTestUtils로 직접 주입하여
* isGroupPass() 카운팅과 getGroupMetrics / reset 메서드를 검증한다.
* <p>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<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "adapterBucketMap", map);
}
private void injectInterfaceMap(Map<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
}
private void injectClientMap(Map<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
}
private void injectGroupBucketList(Map<String, CustomGroupBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("A1", passDualBucket("A1"));
map.put("A2", passDualBucket("A2"));
injectAdapterMap(map);
manager.isAdapterPassDetail("A1");
manager.isAdapterPassDetail("A2");
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllAdapterMetrics();
assertEquals(2, all.size());
assertTrue(all.containsKey("A1"));
assertTrue(all.containsKey("A2"));
}
@Test
void getAllAdapterMetrics_returnsUnmodifiableView() {
Map<String, DualInflowControlManager.TargetMetrics> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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<String, DualCustomBucket> 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"));
}
}