From ca8be2658389f0f2c5b3a9ea9493bfeb36021ba6 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 14:59:15 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=9C=A0=EB=9F=89=EC=A0=9C=EC=96=B4=20Com?= =?UTF-8?q?mand=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20removeClient()=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ReloadInflowClientControlCommand: 클라이언트 버킷 전체/개별 리로드 - RemoveInflowClientControlCommand: 클라이언트 버킷 제거 - DualInflowControlManager.removeClient(clientId) 메서드 추가 (기존 removeAdapter/removeInterface와 동일한 패턴) Co-Authored-By: Claude Sonnet 4.6 --- .../ReloadInflowClientControlCommand.java | 59 +++++++++++++++++++ .../RemoveInflowClientControlCommand.java | 49 +++++++++++++++ .../inflow/dual/DualInflowControlManager.java | 4 ++ 3 files changed, 112 insertions(+) create mode 100644 src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java create mode 100644 src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java new file mode 100644 index 0000000..55c00a4 --- /dev/null +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java @@ -0,0 +1,59 @@ +package com.eactive.eai.agent.inflow; + +import com.eactive.eai.agent.command.Command; +import com.eactive.eai.agent.command.CommandException; +import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.dual.DualInflowControlManager; +import com.eactive.eai.common.util.Logger; + +public class ReloadInflowClientControlCommand extends Command { + + private static final long serialVersionUID = 1L; + + public ReloadInflowClientControlCommand() { + super("ReloadInflowClientControlCommand"); + } + + public Object execute() throws CommandException { + Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + if (logger.isInfo()) + logger.info(this.name + " is executed"); + + if (!(args instanceof String)) { + String rspErrorCode = "RECEAIMCM001"; + String msg = makeException(rspErrorCode, null); + if (logger.isError()) + logger.error(msg); + throw new CommandException(msg); + } + + String keyName = (String) args; + try { + InflowControlManager base = InflowControlManager.getInstance(); + if (!(base instanceof DualInflowControlManager)) { + throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + } + DualInflowControlManager manager = (DualInflowControlManager) base; + + if (keyName != null) { + if ("ALL".equals(keyName)) { + manager.reloadClient(); + if (logger.isWarn()) + logger.warn(this.name + "] all rule Reload."); + } else { + manager.reloadClient(keyName); + if (logger.isWarn()) + logger.warn(this.name + "] " + keyName + " Reload."); + } + } + + } catch (Exception e) { + String rspErrorCode = "RECEAIMCM002"; + String msg = makeException(rspErrorCode, e); + if (logger.isError()) + logger.error(msg, e); + throw new CommandException(msg); + } + return "success"; + } +} diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java new file mode 100644 index 0000000..d4fcbad --- /dev/null +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java @@ -0,0 +1,49 @@ +package com.eactive.eai.agent.inflow; + +import com.eactive.eai.agent.command.Command; +import com.eactive.eai.agent.command.CommandException; +import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.dual.DualInflowControlManager; +import com.eactive.eai.common.util.Logger; + +public class RemoveInflowClientControlCommand extends Command { + + private static final long serialVersionUID = 1L; + + public RemoveInflowClientControlCommand() { + super("RemoveInflowClientControlCommand"); + } + + public Object execute() throws CommandException { + Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + if (logger.isInfo()) + logger.info(this.name + " is executed"); + + if (!(args instanceof String)) { + String rspErrorCode = "RECEAIMCM001"; + String msg = makeException(rspErrorCode, null); + if (logger.isError()) + logger.error(msg); + throw new CommandException(msg); + } + + String key = (String) args; + try { + InflowControlManager base = InflowControlManager.getInstance(); + if (!(base instanceof DualInflowControlManager)) { + throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + } + DualInflowControlManager manager = (DualInflowControlManager) base; + manager.removeClient(key); + if (logger.isWarn()) + logger.warn(this.name + "] " + key + " removed."); + } catch (Exception e) { + String rspErrorCode = "RECEAIMCM002"; + String msg = makeException(rspErrorCode, e); + if (logger.isError()) + logger.error(msg, e); + throw new CommandException(msg); + } + return "success"; + } +} 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 eac2c0c..7b91c9b 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 @@ -153,6 +153,10 @@ public class DualInflowControlManager extends InflowControlManager implements Du reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId); } + public synchronized void removeClient(String clientId) { + clientBucketMap.remove(clientId); + } + @Override public synchronized void reloadGroup() throws Exception { super.reloadGroup(); From 9589bc635aac190ddea60f065fe397231fbfcd7b Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 15:15:11 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=9C=A0=EB=9F=89=EC=A0=9C=EC=96=B4=20Com?= =?UTF-8?q?mand=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=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 Co-Authored-By: Claude Sonnet 4.6 --- .../ReloadInflowClientControlCommandTest.java | 104 ++++++++++++++++++ .../RemoveInflowClientControlCommandTest.java | 98 +++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java create mode 100644 src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java diff --git a/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java b/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java new file mode 100644 index 0000000..c666b33 --- /dev/null +++ b/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java @@ -0,0 +1,104 @@ +package com.eactive.eai.agent.inflow; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +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.dual.DualInflowControlManager; +import com.eactive.eai.common.util.ApplicationContextProvider; + +/** + * ReloadInflowClientControlCommand 단위 테스트. + * + *

ApplicationContextProvider의 static context 필드에 mock ApplicationContext를 주입하여 + * InflowControlManager.getInstance() 반환값을 제어한다. + */ +class ReloadInflowClientControlCommandTest { + + private ApplicationContext mockContext; + private DualInflowControlManager mockDualManager; + + @BeforeEach + void setUp() { + mockContext = mock(ApplicationContext.class); + mockDualManager = mock(DualInflowControlManager.class); + ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext); + } + + private ReloadInflowClientControlCommand commandWith(Object args) { + ReloadInflowClientControlCommand cmd = new ReloadInflowClientControlCommand(); + cmd.setArgs(args); + return cmd; + } + + // ========================================================================= + // args 타입 검증 실패 + // ========================================================================= + + @Test + void execute_argsIsInteger_throwsCommandException() { + ReloadInflowClientControlCommand cmd = commandWith(Integer.valueOf(1)); + + assertThrows(CommandException.class, cmd::execute); + verifyNoInteractions(mockContext); + } + + // ========================================================================= + // DualInflowControlManager 비활성 + // ========================================================================= + + @Test + void execute_notDualManager_throwsCommandException() { + InflowControlManager baseMgr = mock(InflowControlManager.class); + when(mockContext.getBean(InflowControlManager.class)).thenReturn(baseMgr); + + ReloadInflowClientControlCommand cmd = commandWith("CLIENT_A"); + + assertThrows(CommandException.class, cmd::execute); + } + + // ========================================================================= + // 전체 재로드 (ALL) + // ========================================================================= + + @Test + void execute_argsIsALL_callsReloadClientNoArgs() throws Exception { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(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 { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(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 { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + + commandWith("CLIENT_B").execute(); + + verify(mockDualManager).reloadClient("CLIENT_B"); + } +} diff --git a/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java b/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java new file mode 100644 index 0000000..c55e07e --- /dev/null +++ b/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java @@ -0,0 +1,98 @@ +package com.eactive.eai.agent.inflow; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +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.dual.DualInflowControlManager; +import com.eactive.eai.common.util.ApplicationContextProvider; + +/** + * RemoveInflowClientControlCommand 단위 테스트. + * + *

ApplicationContextProvider의 static context 필드에 mock ApplicationContext를 주입하여 + * InflowControlManager.getInstance() 반환값을 제어한다. + */ +class RemoveInflowClientControlCommandTest { + + private ApplicationContext mockContext; + private DualInflowControlManager mockDualManager; + + @BeforeEach + void setUp() { + mockContext = mock(ApplicationContext.class); + mockDualManager = mock(DualInflowControlManager.class); + ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext); + } + + private RemoveInflowClientControlCommand commandWith(Object args) { + RemoveInflowClientControlCommand cmd = new RemoveInflowClientControlCommand(); + cmd.setArgs(args); + return cmd; + } + + // ========================================================================= + // args 타입 검증 실패 + // ========================================================================= + + @Test + void execute_argsIsInteger_throwsCommandException() { + RemoveInflowClientControlCommand cmd = commandWith(Integer.valueOf(1)); + + assertThrows(CommandException.class, cmd::execute); + verifyNoInteractions(mockContext); + } + + // ========================================================================= + // DualInflowControlManager 비활성 + // ========================================================================= + + @Test + void execute_notDualManager_throwsCommandException() { + InflowControlManager baseMgr = mock(InflowControlManager.class); + when(mockContext.getBean(InflowControlManager.class)).thenReturn(baseMgr); + + RemoveInflowClientControlCommand cmd = commandWith("CLIENT_A"); + + assertThrows(CommandException.class, cmd::execute); + } + + // ========================================================================= + // 정상 제거 + // ========================================================================= + + @Test + void execute_validClientId_callsRemoveClient() throws CommandException { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + + Object result = commandWith("CLIENT_A").execute(); + + assertEquals("success", result); + verify(mockDualManager).removeClient("CLIENT_A"); + } + + @Test + void execute_differentClientId_callsRemoveClientWithThatId() throws Exception { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + + commandWith("CLIENT_B").execute(); + + verify(mockDualManager).removeClient("CLIENT_B"); + } + + @Test + void execute_validClientId_doesNotCallReload() throws Exception { + when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + + commandWith("CLIENT_A").execute(); + + verify(mockDualManager, never()).reloadClient(); + verify(mockDualManager, never()).reloadClient(anyString()); + } +} From 2bfc3d0bdeae9e4718f0872fdd3ed989bfe65fa3 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:14:31 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20AbstractInflowControlManager=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=20=EA=B5=AC=EC=A1=B0=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=83=80=EC=9E=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InflowControlManager에서 공통 로직을 AbstractInflowControlManager로 분리하고, 커맨드 클래스들이 AbstractInflowControlManager 타입을 사용하도록 변경. InflowControlUtil에 cachedInflowControlManager 정적 캐시 도입. Co-Authored-By: Claude Sonnet 4.6 --- .../ReloadInflowAdapterControlCommand.java | 5 +- .../ReloadInflowClientControlCommand.java | 5 +- .../ReloadInflowGroupControlCommand.java | 5 +- .../ReloadInflowInterfaceControlCommand.java | 5 +- .../RemoveInflowAdapterControlCommand.java | 5 +- .../RemoveInflowClientControlCommand.java | 5 +- .../RemoveInflowGroupControlCommand.java | 5 +- .../RemoveInflowInterfaceControlCommand.java | 5 +- .../inflow/AbstractInflowControlManager.java | 496 +++++++++++++++++ .../common/inflow/InflowControlManager.java | 527 +----------------- .../eai/common/inflow/InflowControlUtil.java | 100 +++- 11 files changed, 592 insertions(+), 571 deletions(-) create mode 100644 src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowAdapterControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowAdapterControlCommand.java index 5506c0d..c9f2241 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowAdapterControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowAdapterControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class ReloadInflowAdapterControlCommand extends Command { @@ -27,7 +28,7 @@ public class ReloadInflowAdapterControlCommand extends Command { String keyName = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); if (keyName != null) { if ("ALL".equals(keyName)) { diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java index 55c00a4..8afe1e1 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.inflow.dual.DualInflowControlManager; import com.eactive.eai.common.util.Logger; @@ -29,7 +30,7 @@ public class ReloadInflowClientControlCommand extends Command { String keyName = (String) args; try { - InflowControlManager base = InflowControlManager.getInstance(); + AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); if (!(base instanceof DualInflowControlManager)) { throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); } diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowGroupControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowGroupControlCommand.java index 0582f01..a156cff 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowGroupControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowGroupControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class ReloadInflowGroupControlCommand extends Command { @@ -28,7 +29,7 @@ public class ReloadInflowGroupControlCommand extends Command { String keyName = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); if (keyName != null) { if ("ALL".equals(keyName)) { diff --git a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowInterfaceControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowInterfaceControlCommand.java index ab9dc47..45e5dc1 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowInterfaceControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowInterfaceControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class ReloadInflowInterfaceControlCommand extends Command { @@ -28,7 +29,7 @@ public class ReloadInflowInterfaceControlCommand extends Command { String keyName = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); if (keyName != null) { if ("ALL".equals(keyName)) { diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowAdapterControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowAdapterControlCommand.java index cce81bb..77bbaf3 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowAdapterControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowAdapterControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class RemoveInflowAdapterControlCommand extends Command { @@ -26,7 +27,7 @@ public class RemoveInflowAdapterControlCommand extends Command { String key = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); manager.removeAdapter(key); } catch (Exception e) { String rspErrorCode = "RECEAIMCM002"; diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java index d4fcbad..fe02028 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.inflow.dual.DualInflowControlManager; import com.eactive.eai.common.util.Logger; @@ -29,7 +30,7 @@ public class RemoveInflowClientControlCommand extends Command { String key = (String) args; try { - InflowControlManager base = InflowControlManager.getInstance(); + AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); if (!(base instanceof DualInflowControlManager)) { throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); } diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowGroupControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowGroupControlCommand.java index a4bdf03..a96fed0 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowGroupControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowGroupControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class RemoveInflowGroupControlCommand extends Command { @@ -28,7 +29,7 @@ public class RemoveInflowGroupControlCommand extends Command { String key = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); manager.removeGroup(key); if (logger.isWarn()) logger.warn(this.name + "] group " + key + " removed."); diff --git a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowInterfaceControlCommand.java b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowInterfaceControlCommand.java index 597e6fa..04e8a17 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowInterfaceControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowInterfaceControlCommand.java @@ -2,7 +2,8 @@ package com.eactive.eai.agent.inflow; import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.util.Logger; public class RemoveInflowInterfaceControlCommand extends Command { @@ -25,7 +26,7 @@ public class RemoveInflowInterfaceControlCommand extends Command { String key = (String) args; try { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); manager.removeInterface(key); } catch (Exception e) { String rspErrorCode = "RECEAIMCM002"; diff --git a/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java new file mode 100644 index 0000000..12c23b1 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java @@ -0,0 +1,496 @@ +package com.eactive.eai.common.inflow; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; + +import com.eactive.eai.common.exception.ExceptionUtil; +import com.eactive.eai.common.lifecycle.Lifecycle; +import com.eactive.eai.common.lifecycle.LifecycleException; +import com.eactive.eai.common.lifecycle.LifecycleListener; +import com.eactive.eai.common.lifecycle.LifecycleSupport; +import com.eactive.eai.common.util.Logger; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.local.LocalBucket; +import io.github.bucket4j.local.LocalBucketBuilder; + +public abstract class AbstractInflowControlManager implements Lifecycle, Bucket { + static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + public static final String QUOTA_TIMEUNIT_SECOND = "SEC"; + public static final String QUOTA_TIMEUNIT_MINUTE = "MIN"; + public static final String QUOTA_TIMEUNIT_HOUR = "HOU"; + public static final String QUOTA_TIMEUNIT_DAY = "DAY"; + public static final String QUOTA_TIMEUNIT_MONTH = "MON"; + + private volatile Map adapterBucketList = new ConcurrentHashMap<>(); + private volatile Map interfaceBucketList = new ConcurrentHashMap<>(); + private volatile Map groupBucketList = new ConcurrentHashMap<>(); + private volatile Map interfaceToGroupMap = new ConcurrentHashMap<>(); + private volatile Map groupVoMap = new ConcurrentHashMap<>(); + private boolean started; + private LifecycleSupport lifecycle = new LifecycleSupport(this); + + @Autowired + protected InflowControlDAO inflowControlDAO; + + public void start() throws LifecycleException { + if (started) + throw new LifecycleException("RECEAICMM201"); + + lifecycle.fireLifecycleEvent(STARTING_EVENT, this); + try { + init(); + } catch (Exception e) { + throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAICMM202")); + } + started = true; + lifecycle.fireLifecycleEvent(STARTED_EVENT, this); + } + + private void init() throws Exception { + initAdapter(); + initInterface(); + initGroup(); + } + + private void initAdapter() throws Exception { + Map newMap = new HashMap<>(); + List inflowAdapterVoList = inflowControlDAO.getInflowAdapterList(); + + for (InflowTargetVO inflowVo : inflowAdapterVoList) { + if (AbstractInflowControlManager.isInflowTarget(inflowVo)) { + CustomBucket bucket = makeBucketUsingInflowVO(inflowVo); + if (bucket != null) { + newMap.put(inflowVo.getName(), bucket); + } + } + } + this.adapterBucketList = newMap; + } + + private void initInterface() throws Exception { + Map newMap = new HashMap<>(); + List inflowInterfaceVoList = inflowControlDAO.getInflowInterfaceList(); + + for (InflowTargetVO inflowVo : inflowInterfaceVoList) { + if (AbstractInflowControlManager.isInflowTarget(inflowVo)) { + CustomBucket bucket = makeBucketUsingInflowVO(inflowVo); + if (bucket != null) { + newMap.put(inflowVo.getName(), bucket); + } + } + } + this.interfaceBucketList = newMap; + } + + private void initGroup() throws Exception { + Map newGroupBucketList = new HashMap<>(); + Map newInterfaceToGroupMap = new HashMap<>(); + Map newGroupVoMap = new HashMap<>(); + + List inflowGroupVoList = inflowControlDAO.getInflowGroupList(); + + for (InflowGroupVO groupVo : inflowGroupVoList) { + if (isInflowGroupTarget(groupVo)) { + CustomGroupBucket bucket = makeGroupBucket(groupVo); + if (bucket != null) { + newGroupBucketList.put(groupVo.getGroupId(), bucket); + newGroupVoMap.put(groupVo.getGroupId(), groupVo); + for (String interfaceId : groupVo.getInterfaceList()) { + newInterfaceToGroupMap.put(interfaceId, groupVo.getGroupId()); + } + } + } + } + + if (logger.isInfo()) { + logger.info("InflowControlManager] initGroup completed. groups=" + newGroupBucketList.size() + + ", mappings=" + newInterfaceToGroupMap.size()); + } + + this.groupBucketList = newGroupBucketList; + this.interfaceToGroupMap = newInterfaceToGroupMap; + this.groupVoMap = newGroupVoMap; + } + + public synchronized void reloadAdapter() throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload all Started ..."); + } + initAdapter(); + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload all finished ..."); + } + } + + public synchronized void reloadInterface() throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload all Started ..."); + } + initInterface(); + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload all finished ..."); + } + } + + public synchronized void reloadAdapter(String adapter) throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload Started ..."); + } + Map map = inflowControlDAO.getInflowTargetByAdater(adapter); + if (map != null) { + Iterator it = map.keySet().iterator(); + String key = ""; + while (it.hasNext()) { + key = it.next(); + + InflowTargetVO inflowTarget = map.get(key); + if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) { + CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget); + if (bucket != null) { + adapterBucketList.put(inflowTarget.getName(), bucket); + } else { + adapterBucketList.remove(inflowTarget.getName()); + } + } else { + adapterBucketList.remove(inflowTarget.getName()); + } + } + } + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload finished ..."); + } + } + + public synchronized void reloadInterface(String inter) throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload Started ..."); + } + Map map = inflowControlDAO.getInflowTargetByInterface(inter); + if (map != null) { + Iterator it = map.keySet().iterator(); + String key = ""; + while (it.hasNext()) { + key = it.next(); + + InflowTargetVO inflowTarget = map.get(key); + if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) { + CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget); + if (bucket != null) { + interfaceBucketList.put(inflowTarget.getName(), bucket); + } else { + interfaceBucketList.remove(inflowTarget.getName()); + } + } else { + interfaceBucketList.remove(inflowTarget.getName()); + } + } + } + + if (logger.isWarn()) { + logger.warn("InflowControlManager] reload finished ..."); + } + } + + public synchronized void reloadGroup() throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reloadGroup all Started ..."); + } + initGroup(); + if (logger.isWarn()) { + logger.warn("InflowControlManager] reloadGroup all finished ..."); + } + } + + public synchronized void reloadGroup(String groupId) throws Exception { + if (logger.isWarn()) { + logger.warn("InflowControlManager] reloadGroup Started ... groupId=" + groupId); + } + Map map = inflowControlDAO.getInflowTargetByGroup(groupId); + if (map != null) { + Iterator> mappingIt = interfaceToGroupMap.entrySet().iterator(); + while (mappingIt.hasNext()) { + Map.Entry entry = mappingIt.next(); + if (groupId.equals(entry.getValue())) { + mappingIt.remove(); + } + } + + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + InflowGroupVO groupVo = map.get(key); + + if (isInflowGroupTarget(groupVo)) { + CustomGroupBucket bucket = makeGroupBucket(groupVo); + if (bucket != null) { + groupBucketList.put(groupVo.getGroupId(), bucket); + groupVoMap.put(groupVo.getGroupId(), groupVo); + for (String interfaceId : groupVo.getInterfaceList()) { + interfaceToGroupMap.put(interfaceId, groupVo.getGroupId()); + } + } else { + groupBucketList.remove(groupVo.getGroupId()); + groupVoMap.remove(groupVo.getGroupId()); + } + } else { + groupBucketList.remove(groupVo.getGroupId()); + groupVoMap.remove(groupVo.getGroupId()); + } + } + } + + if (logger.isWarn()) { + logger.warn("InflowControlManager] reloadGroup finished ..."); + } + } + + public void stop() throws LifecycleException { + if (!started) + throw new LifecycleException("RECEAICMM203"); + + lifecycle.fireLifecycleEvent(STOPING_EVENT, this); + + adapterBucketList = new ConcurrentHashMap<>(); + interfaceBucketList = new ConcurrentHashMap<>(); + groupBucketList = new ConcurrentHashMap<>(); + interfaceToGroupMap = new ConcurrentHashMap<>(); + groupVoMap = new ConcurrentHashMap<>(); + + started = false; + lifecycle.fireLifecycleEvent(STOPPED_EVENT, this); + } + + public void addLifecycleListener(LifecycleListener listener) { + lifecycle.addLifecycleListener(listener); + } + + public LifecycleListener[] findLifecycleListeners() { + return lifecycle.findLifecycleListeners(); + } + + public void removeLifecycleListener(LifecycleListener listener) { + lifecycle.removeLifecycleListener(listener); + } + + public boolean isStarted() { + return this.started; + } + + public String[] getAdapterAllKeys() { + Iterator it = this.adapterBucketList.keySet().iterator(); + String[] svcCd = new String[this.adapterBucketList.size()]; + for (int i = 0; it.hasNext(); i++) { + svcCd[i] = it.next(); + } + Arrays.sort(svcCd); + return svcCd; + } + + public String[] getInterfaceAllKeys() { + Iterator it = this.interfaceBucketList.keySet().iterator(); + String[] svcCd = new String[this.interfaceBucketList.size()]; + for (int i = 0; it.hasNext(); i++) { + svcCd[i] = it.next(); + } + Arrays.sort(svcCd); + return svcCd; + } + + public synchronized void removeAdapter(String adapter) { + adapterBucketList.remove(adapter); + } + + public synchronized void removeInterface(String inter) { + interfaceBucketList.remove(inter); + } + + public synchronized void removeGroup(String groupId) { + groupBucketList.remove(groupId); + groupVoMap.remove(groupId); + Iterator> it = interfaceToGroupMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (groupId.equals(entry.getValue())) { + it.remove(); + } + } + } + + public String[] getGroupAllKeys() { + Iterator it = this.groupBucketList.keySet().iterator(); + String[] groupIds = new String[this.groupBucketList.size()]; + for (int i = 0; it.hasNext(); i++) { + groupIds[i] = it.next(); + } + Arrays.sort(groupIds); + return groupIds; + } + + public CustomGroupBucket getGroupBucket(String groupId) { + return groupBucketList.get(groupId); + } + + private CustomBucket makeBucketUsingInflowVO(InflowTargetVO inflowTarget) { + if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) { + LocalBucketBuilder builder = Bucket4j.builder(); + if (inflowTarget.getThresholdPerSecond() > 0) { + builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1))); + } + + if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) { + builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(), + AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))); + } + + return new CustomBucket(builder.build(), inflowTarget); + } + + return null; + } + + private CustomGroupBucket makeGroupBucket(InflowGroupVO groupVo) { + if (!isInflowGroupTarget(groupVo)) { + return null; + } + + LocalBucket perSecondBucket = null; + LocalBucket thresholdBucket = null; + + if (groupVo.getThresholdPerSecond() > 0) { + perSecondBucket = Bucket4j.builder() + .addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))) + .build(); + } + + if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) { + thresholdBucket = Bucket4j.builder() + .addLimit(Bandwidth.simple(groupVo.getThreshold(), + AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit()))) + .build(); + } + + return new CustomGroupBucket(perSecondBucket, thresholdBucket, groupVo); + } + + @Override + public boolean isAdapterPass(String adapter) { + CustomBucket b = adapterBucketList.get(adapter); + + if (b == null) + return true; + + return b.getLocalBucket().tryConsume(1); + } + + @Override + public boolean isInterfacePass(String inter) { + CustomBucket b = interfaceBucketList.get(inter); + + if (b == null) + return true; + + return b.getLocalBucket().tryConsume(1); + } + + @Override + public InflowTargetVO getAdapterInflowThreashold(String adapter) { + CustomBucket b = adapterBucketList.get(adapter); + if (b == null) + return null; + return b.getInflowTargetVo(); + } + + @Override + public InflowTargetVO getInterfaceInflowThreashold(String inter) { + CustomBucket b = interfaceBucketList.get(inter); + if (b == null) + return null; + return b.getInflowTargetVo(); + } + + @Override + public InflowTargetVO getInflowThreashold(String inter) { + return null; + } + + public InflowTargetVO getAdapterInflow(String adapter) { + return getAdapterInflowThreashold(adapter); + } + + public InflowTargetVO getInterfaceInflow(String inter) { + return getInterfaceInflowThreashold(inter); + } + + @Override + public String isGroupPass(String groupId) { + CustomGroupBucket b = groupBucketList.get(groupId); + + if (b == null) + return null; + + return b.tryConsume(); + } + + @Override + public InflowGroupVO getGroupInflowThreshold(String groupId) { + return groupVoMap.get(groupId); + } + + @Override + public String getGroupIdByInterface(String interfaceId) { + return interfaceToGroupMap.get(interfaceId); + } + + public InflowGroupVO getGroupInflow(String groupId) { + return getGroupInflowThreshold(groupId); + } + + public static boolean isInflowTarget(InflowTargetVO inflowTarget) { + if (inflowTarget == null || !inflowTarget.isActivate()) { + return false; + } + + return inflowTarget.getThresholdPerSecond() > 0 || (inflowTarget.getThreshold() > 0 && StringUtils + .equalsAnyIgnoreCase(inflowTarget.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND, + QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH)); + + } + + public static boolean isInflowGroupTarget(InflowGroupVO groupVo) { + if (groupVo == null || !groupVo.isActivate()) { + return false; + } + + return groupVo.getThresholdPerSecond() > 0 || (groupVo.getThreshold() > 0 && StringUtils + .equalsAnyIgnoreCase(groupVo.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND, + QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH)); + } + + public static Duration getPeriod(String timeUnit) { + if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_SECOND)) { + return Duration.ofSeconds(1); + } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MINUTE)) { + return Duration.ofMinutes(1); + } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_HOUR)) { + return Duration.ofHours(1); + } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_DAY)) { + return Duration.ofDays(1); + } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MONTH)) { + Calendar calendar = Calendar.getInstance(); + return Duration.ofDays(calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); + } else { + return null; + } + } +} diff --git a/src/main/java/com/eactive/eai/common/inflow/InflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/InflowControlManager.java index 1992993..7418cc0 100644 --- a/src/main/java/com/eactive/eai/common/inflow/InflowControlManager.java +++ b/src/main/java/com/eactive/eai/common/inflow/InflowControlManager.java @@ -1,55 +1,11 @@ package com.eactive.eai.common.inflow; -import java.time.Duration; -import java.util.Arrays; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import com.eactive.eai.common.exception.ExceptionUtil; -import com.eactive.eai.common.lifecycle.Lifecycle; -import com.eactive.eai.common.lifecycle.LifecycleException; -import com.eactive.eai.common.lifecycle.LifecycleListener; -import com.eactive.eai.common.lifecycle.LifecycleSupport; -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; -import com.eactive.eai.common.util.MessageUtil; -import com.eactive.eai.message.service.InterfaceMapper; -import com.ext.eai.common.stdmessage.STDMessageKeys; - -import io.github.bucket4j.Bandwidth; -import io.github.bucket4j.Bucket4j; -import io.github.bucket4j.local.LocalBucket; -import io.github.bucket4j.local.LocalBucketBuilder; @Component -public class InflowControlManager implements Lifecycle, Bucket { - static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); - public static final String QUOTA_TIMEUNIT_SECOND = "SEC"; - public static final String QUOTA_TIMEUNIT_MINUTE = "MIN"; - public static final String QUOTA_TIMEUNIT_HOUR = "HOU"; - public static final String QUOTA_TIMEUNIT_DAY = "DAY"; - public static final String QUOTA_TIMEUNIT_MONTH = "MON"; - - private volatile Map adapterBucketList = new ConcurrentHashMap<>(); - private volatile Map interfaceBucketList = new ConcurrentHashMap<>(); - private volatile Map groupBucketList = new ConcurrentHashMap<>(); - private volatile Map interfaceToGroupMap = new ConcurrentHashMap<>(); - private volatile Map groupVoMap = new ConcurrentHashMap<>(); - private boolean started; - private LifecycleSupport lifecycle = new LifecycleSupport(this); - - @Autowired - private InflowControlDAO inflowControlDAO; +public class InflowControlManager extends AbstractInflowControlManager { public static synchronized InflowControlManager getInstance() { return ApplicationContextProvider.getContext().getBean(InflowControlManager.class); @@ -58,485 +14,4 @@ public class InflowControlManager implements Lifecycle, Bucket { public static synchronized Bucket getBucket() { return ApplicationContextProvider.getContext().getBean(InflowControlManager.class); } - - /** - * 유량제어 대상인 거래인지 체크한다.(사이트에 맞게 구성) - * - * @param eaiServerManager - * @param eaiMessage - * @return - */ - public static Boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) { - InterfaceMapper mapper = eaiMessage.getMapper(); - String returnType = mapper.getSendRecvDivision(eaiMessage.getStandardMessage()); // S|R - - if (STDMessageKeys.SEND_RECV_CD_SEND.equals(returnType)) { - return Boolean.valueOf(true); - } - - return Boolean.valueOf(false); - } - - public void start() throws LifecycleException { - if (started) - throw new LifecycleException("RECEAICMM201"); - - lifecycle.fireLifecycleEvent(STARTING_EVENT, this); - try { - init(); - } catch (Exception e) { - throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAICMM202")); - } - started = true; - lifecycle.fireLifecycleEvent(STARTED_EVENT, this); - } - - private void init() throws Exception { - initAdapter(); - initInterface(); - initGroup(); - } - - private void initAdapter() throws Exception { - Map newMap = new HashMap<>(); - List inflowAdapterVoList = inflowControlDAO.getInflowAdapterList(); - - for (InflowTargetVO inflowVo : inflowAdapterVoList) { - if (InflowControlManager.isInflowTarget(inflowVo)) { - CustomBucket bucket = makeBucketUsingInflowVO(inflowVo); - if (bucket != null) { - newMap.put(inflowVo.getName(), bucket); - } - } - } - this.adapterBucketList = newMap; - } - - private void initInterface() throws Exception { - Map newMap = new HashMap<>(); - List inflowInterfaceVoList = inflowControlDAO.getInflowInterfaceList(); - - for (InflowTargetVO inflowVo : inflowInterfaceVoList) { - if (InflowControlManager.isInflowTarget(inflowVo)) { - CustomBucket bucket = makeBucketUsingInflowVO(inflowVo); - if (bucket != null) { - newMap.put(inflowVo.getName(), bucket); - } - } - } - this.interfaceBucketList = newMap; - } - - private void initGroup() throws Exception { - Map newGroupBucketList = new HashMap<>(); - Map newInterfaceToGroupMap = new HashMap<>(); - Map newGroupVoMap = new HashMap<>(); - - List inflowGroupVoList = inflowControlDAO.getInflowGroupList(); - - for (InflowGroupVO groupVo : inflowGroupVoList) { - if (isInflowGroupTarget(groupVo)) { - CustomGroupBucket bucket = makeGroupBucket(groupVo); - if (bucket != null) { - newGroupBucketList.put(groupVo.getGroupId(), bucket); - newGroupVoMap.put(groupVo.getGroupId(), groupVo); - // 인터페이스 → 그룹 매핑 등록 - for (String interfaceId : groupVo.getInterfaceList()) { - newInterfaceToGroupMap.put(interfaceId, groupVo.getGroupId()); - } - } - } - } - - if (logger.isInfo()) { - logger.info("InflowControlManager] initGroup completed. groups=" + newGroupBucketList.size() - + ", mappings=" + newInterfaceToGroupMap.size()); - } - - this.groupBucketList = newGroupBucketList; - this.interfaceToGroupMap = newInterfaceToGroupMap; - this.groupVoMap = newGroupVoMap; - } - - public synchronized void reloadAdapter() throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload all Started ..."); - } - initAdapter(); - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload all finished ..."); - } - } - - public synchronized void reloadInterface() throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload all Started ..."); - } - initInterface(); - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload all finished ..."); - } - } - - public synchronized void reloadAdapter(String adapter) throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload Started ..."); - } - Map map = inflowControlDAO.getInflowTargetByAdater(adapter); - if (map != null) { - Iterator it = map.keySet().iterator(); - String key = ""; - while (it.hasNext()) { - key = it.next(); - - InflowTargetVO inflowTarget = map.get(key); - if (InflowControlManager.isInflowTarget(inflowTarget)) { - CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget); - if (bucket != null) { - adapterBucketList.put(inflowTarget.getName(), bucket); - } else { - adapterBucketList.remove(inflowTarget.getName()); - } - } else { - adapterBucketList.remove(inflowTarget.getName()); - } - } - } - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload finished ..."); - } - } - - public synchronized void reloadInterface(String inter) throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload Started ..."); - } - Map map = inflowControlDAO.getInflowTargetByInterface(inter); - if (map != null) { - Iterator it = map.keySet().iterator(); - String key = ""; - while (it.hasNext()) { - key = it.next(); - - InflowTargetVO inflowTarget = map.get(key); - if (InflowControlManager.isInflowTarget(inflowTarget)) { - CustomBucket bucket = makeBucketUsingInflowVO(inflowTarget); - if (bucket != null) { - interfaceBucketList.put(inflowTarget.getName(), bucket); - } else { - interfaceBucketList.remove(inflowTarget.getName()); - } - } else { - interfaceBucketList.remove(inflowTarget.getName()); - } - } - } - - if (logger.isWarn()) { - logger.warn("InflowControlManager] reload finished ..."); - } - } - - public synchronized void reloadGroup() throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reloadGroup all Started ..."); - } - initGroup(); - if (logger.isWarn()) { - logger.warn("InflowControlManager] reloadGroup all finished ..."); - } - } - - public synchronized void reloadGroup(String groupId) throws Exception { - if (logger.isWarn()) { - logger.warn("InflowControlManager] reloadGroup Started ... groupId=" + groupId); - } - Map map = inflowControlDAO.getInflowTargetByGroup(groupId); - if (map != null) { - // 기존 그룹에 속한 인터페이스 매핑 제거 - Iterator> mappingIt = interfaceToGroupMap.entrySet().iterator(); - while (mappingIt.hasNext()) { - Map.Entry entry = mappingIt.next(); - if (groupId.equals(entry.getValue())) { - mappingIt.remove(); - } - } - - Iterator it = map.keySet().iterator(); - while (it.hasNext()) { - String key = it.next(); - InflowGroupVO groupVo = map.get(key); - - if (isInflowGroupTarget(groupVo)) { - CustomGroupBucket bucket = makeGroupBucket(groupVo); - if (bucket != null) { - groupBucketList.put(groupVo.getGroupId(), bucket); - groupVoMap.put(groupVo.getGroupId(), groupVo); - // 인터페이스 → 그룹 매핑 재등록 - for (String interfaceId : groupVo.getInterfaceList()) { - interfaceToGroupMap.put(interfaceId, groupVo.getGroupId()); - } - } else { - groupBucketList.remove(groupVo.getGroupId()); - groupVoMap.remove(groupVo.getGroupId()); - } - } else { - groupBucketList.remove(groupVo.getGroupId()); - groupVoMap.remove(groupVo.getGroupId()); - } - } - } - - if (logger.isWarn()) { - logger.warn("InflowControlManager] reloadGroup finished ..."); - } - } - - public void stop() throws LifecycleException { - // Validate and update our current component state - if (!started) - throw new LifecycleException("RECEAICMM203"); - - // Notify our interested LifecycleListeners - lifecycle.fireLifecycleEvent(STOPING_EVENT, this); - - adapterBucketList = new ConcurrentHashMap<>(); - interfaceBucketList = new ConcurrentHashMap<>(); - groupBucketList = new ConcurrentHashMap<>(); - interfaceToGroupMap = new ConcurrentHashMap<>(); - groupVoMap = new ConcurrentHashMap<>(); - - started = false; - // Notify our interested LifecycleListeners - lifecycle.fireLifecycleEvent(STOPPED_EVENT, this); - } - - public void addLifecycleListener(LifecycleListener listener) { - lifecycle.addLifecycleListener(listener); - } - - public LifecycleListener[] findLifecycleListeners() { - return lifecycle.findLifecycleListeners(); - } - - public void removeLifecycleListener(LifecycleListener listener) { - lifecycle.removeLifecycleListener(listener); - } - - public boolean isStarted() { - return this.started; - } - - public String[] getAdapterAllKeys() { - Iterator it = this.adapterBucketList.keySet().iterator(); - String[] svcCd = new String[this.adapterBucketList.size()]; - for (int i = 0; it.hasNext(); i++) { - svcCd[i] = it.next(); - } - Arrays.sort(svcCd); - return svcCd; - } - - public String[] getInterfaceAllKeys() { - Iterator it = this.interfaceBucketList.keySet().iterator(); - String[] svcCd = new String[this.interfaceBucketList.size()]; - for (int i = 0; it.hasNext(); i++) { - svcCd[i] = it.next(); - } - Arrays.sort(svcCd); - return svcCd; - } - - public synchronized void removeAdapter(String adapter) { - adapterBucketList.remove(adapter); - } - - public synchronized void removeInterface(String inter) { - interfaceBucketList.remove(inter); - } - - public synchronized void removeGroup(String groupId) { - groupBucketList.remove(groupId); - groupVoMap.remove(groupId); - // 해당 그룹에 속한 인터페이스 매핑도 제거 - Iterator> it = interfaceToGroupMap.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (groupId.equals(entry.getValue())) { - it.remove(); - } - } - } - - public String[] getGroupAllKeys() { - Iterator it = this.groupBucketList.keySet().iterator(); - String[] groupIds = new String[this.groupBucketList.size()]; - for (int i = 0; it.hasNext(); i++) { - groupIds[i] = it.next(); - } - Arrays.sort(groupIds); - return groupIds; - } - - public CustomGroupBucket getGroupBucket(String groupId) { - return groupBucketList.get(groupId); - } - - private CustomBucket makeBucketUsingInflowVO(InflowTargetVO inflowTarget) { - if (InflowControlManager.isInflowTarget(inflowTarget)) { - LocalBucketBuilder builder = Bucket4j.builder(); - if (inflowTarget.getThresholdPerSecond() > 0) { - builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1))); - } - - if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) { - builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(), - InflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))); - } - - return new CustomBucket(builder.build(), inflowTarget); - } - - return null; - } - - /** - * 그룹용 버킷 생성 - 초당/추가 임계치를 별도 버킷으로 분리 - */ - private CustomGroupBucket makeGroupBucket(InflowGroupVO groupVo) { - if (!isInflowGroupTarget(groupVo)) { - return null; - } - - LocalBucket perSecondBucket = null; - LocalBucket thresholdBucket = null; - - // 초당 임계치 버킷 (별도) - if (groupVo.getThresholdPerSecond() > 0) { - perSecondBucket = Bucket4j.builder() - .addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))) - .build(); - } - - // 추가 임계치 버킷 (별도) - if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) { - thresholdBucket = Bucket4j.builder() - .addLimit(Bandwidth.simple(groupVo.getThreshold(), - InflowControlManager.getPeriod(groupVo.getThresholdTimeUnit()))) - .build(); - } - - return new CustomGroupBucket(perSecondBucket, thresholdBucket, groupVo); - } - - @Override - public boolean isAdapterPass(String adapter) { - CustomBucket b = adapterBucketList.get(adapter); - - if (b == null) - return true; - - return b.getLocalBucket().tryConsume(1); - } - - @Override - public boolean isInterfacePass(String inter) { - CustomBucket b = interfaceBucketList.get(inter); - - if (b == null) - return true; - - return b.getLocalBucket().tryConsume(1); - } - - @Override - public InflowTargetVO getAdapterInflowThreashold(String adapter) { - CustomBucket b = adapterBucketList.get(adapter); - if (b == null) - return null; - return b.getInflowTargetVo(); - } - - @Override - public InflowTargetVO getInterfaceInflowThreashold(String inter) { - CustomBucket b = interfaceBucketList.get(inter); - if (b == null) - return null; - return b.getInflowTargetVo(); - } - - @Override - public InflowTargetVO getInflowThreashold(String inter) { - return null; - } - - public InflowTargetVO getAdapterInflow(String adapter) { - return getAdapterInflowThreashold(adapter); - } - - public InflowTargetVO getInterfaceInflow(String inter) { - return getInterfaceInflowThreashold(inter); - } - - @Override - public String isGroupPass(String groupId) { - CustomGroupBucket b = groupBucketList.get(groupId); - - if (b == null) - return null; - - return b.tryConsume(); - } - - @Override - public InflowGroupVO getGroupInflowThreshold(String groupId) { - return groupVoMap.get(groupId); - } - - @Override - public String getGroupIdByInterface(String interfaceId) { - return interfaceToGroupMap.get(interfaceId); - } - - public InflowGroupVO getGroupInflow(String groupId) { - return getGroupInflowThreshold(groupId); - } - - public static boolean isInflowTarget(InflowTargetVO inflowTarget) { - if (inflowTarget == null || !inflowTarget.isActivate()) { - return false; - } - - return inflowTarget.getThresholdPerSecond() > 0 || (inflowTarget.getThreshold() > 0 && StringUtils - .equalsAnyIgnoreCase(inflowTarget.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND, - QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH)); - - } - - public static boolean isInflowGroupTarget(InflowGroupVO groupVo) { - if (groupVo == null || !groupVo.isActivate()) { - return false; - } - - return groupVo.getThresholdPerSecond() > 0 || (groupVo.getThreshold() > 0 && StringUtils - .equalsAnyIgnoreCase(groupVo.getThresholdTimeUnit(), QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_SECOND, - QUOTA_TIMEUNIT_MINUTE, QUOTA_TIMEUNIT_HOUR, QUOTA_TIMEUNIT_DAY, QUOTA_TIMEUNIT_MONTH)); - } - - public static Duration getPeriod(String timeUnit) { - if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_SECOND)) { - return Duration.ofSeconds(1); - } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MINUTE)) { - return Duration.ofMinutes(1); - } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_HOUR)) { - return Duration.ofHours(1); - } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_DAY)) { - return Duration.ofDays(1); - } else if (StringUtils.equalsIgnoreCase(timeUnit, QUOTA_TIMEUNIT_MONTH)) { - Calendar calendar = Calendar.getInstance(); - return Duration.ofDays(calendar.getActualMaximum(Calendar.DAY_OF_MONTH)); - } else { - return null; - } - } } diff --git a/src/main/java/com/eactive/eai/common/inflow/InflowControlUtil.java b/src/main/java/com/eactive/eai/common/inflow/InflowControlUtil.java index 3fa037f..d4b7b48 100644 --- a/src/main/java/com/eactive/eai/common/inflow/InflowControlUtil.java +++ b/src/main/java/com/eactive/eai/common/inflow/InflowControlUtil.java @@ -8,7 +8,10 @@ import org.apache.commons.lang3.StringUtils; import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.server.EAIServerManager; +import com.eactive.eai.common.util.ApplicationContextProvider; import com.eactive.eai.common.util.Logger; +import com.eactive.eai.message.service.InterfaceMapper; +import com.ext.eai.common.stdmessage.STDMessageKeys; public class InflowControlUtil { private InflowControlUtil() { @@ -16,28 +19,70 @@ public class InflowControlUtil { static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + private static volatile AbstractInflowControlManager cachedInflowControlManager; + private static volatile Bucket cachedBucket; + + /** + * INFLOW.properties의 inflow.control.bucket.className 설정에 따라 + * 활성화된 InflowControlManager 구현체를 반환한다. + * + * getBean(InflowControlManager.class) 대신 설정된 구체 타입으로 조회하므로 + * InflowControlManager와 DualInflowControlManager가 모두 Bean으로 등록된 + * 환경에서도 NoUniqueBeanDefinitionException 없이 올바른 인스턴스를 반환한다. + * + * 최초 1회만 조회하고 이후에는 캐시된 인스턴스를 반환한다. + */ @SuppressWarnings({ "rawtypes", "unchecked" }) - public static Bucket getBucket() { - Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW"); - String className = inFlowProp.getProperty("inflow.control.bucket.className"); - if (StringUtils.isBlank(className) - || StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) { - return InflowControlManager.getBucket(); - } else { - try { - Class clazz = Class.forName(className); - Method method = clazz.getMethod("getBucket", (Class[]) null); - return (Bucket) method.invoke(null, (Object[]) null); - } catch (Exception e) { - logger.error("Method call error class= {}, method= getBucket", className); - logger.error(e.getMessage()); + public static AbstractInflowControlManager getInflowControlManager() { + if (cachedInflowControlManager == null) { + synchronized (InflowControlUtil.class) { + if (cachedInflowControlManager == null) { + Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW"); + String className = inFlowProp.getProperty("inflow.control.bucket.className"); + if (StringUtils.isBlank(className) + || StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) { + cachedInflowControlManager = InflowControlManager.getInstance(); + } else { + try { + Class clazz = Class.forName(className); + cachedInflowControlManager = (AbstractInflowControlManager) ApplicationContextProvider.getContext().getBean(clazz); + } catch (Exception e) { + logger.error("getInflowControlManager error class= {}", className); + logger.error(e.getMessage()); + } + } + } } } - - return null; + return cachedInflowControlManager; } @SuppressWarnings({ "rawtypes", "unchecked" }) + public static Bucket getBucket() { + if (cachedBucket == null) { + synchronized (InflowControlUtil.class) { + if (cachedBucket == null) { + Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW"); + String className = inFlowProp.getProperty("inflow.control.bucket.className"); + if (StringUtils.isBlank(className) + || StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) { + cachedBucket = InflowControlManager.getBucket(); + } else { + try { + Class clazz = Class.forName(className); + Method method = clazz.getMethod("getBucket", (Class[]) null); + cachedBucket = (Bucket) method.invoke(null, (Object[]) null); + } catch (Exception e) { + logger.error("Method call error class= {}, method= getBucket", className); + logger.error(e.getMessage()); + } + } + } + } + } + return cachedBucket; + } + public static boolean isTargetOfInflowControl(EAIServerManager eaiServerManager, EAIMessage eaiMessage) { Properties inFlowProp = PropManager.getInstance().getProperties("INFLOW"); String controlInflow = inFlowProp.getProperty("inflow.control.yn", "N"); @@ -45,21 +90,18 @@ public class InflowControlUtil { if (!"Y".equalsIgnoreCase(controlInflow)) { return false; } + + try { + InterfaceMapper mapper = eaiMessage.getMapper(); + String returnType = mapper.getSendRecvDivision(eaiMessage.getStandardMessage()); // S|R - String className = inFlowProp.getProperty("inflow.control.bucket.className"); - if (StringUtils.isBlank(className) - || StringUtils.equals(className, "com.eactive.eai.common.inflow.InflowControlManager")) { - return InflowControlManager.isTargetOfInflowControl(eaiServerManager, eaiMessage).booleanValue(); - } else { - try { - Class clazz = Class.forName(className); - Method method = clazz.getMethod("isTargetOfInflowControl", EAIServerManager.class, EAIMessage.class); - Boolean returnBoolean = (Boolean) method.invoke(null, eaiServerManager, eaiMessage); - return returnBoolean.booleanValue(); - } catch (Exception e) { - logger.error("Method call error class= {}, method= isTargetOfInflowControl", className); - logger.error(e.getMessage()); + if (STDMessageKeys.SEND_RECV_CD_SEND.equals(returnType)) { + return Boolean.valueOf(true); } + + return Boolean.valueOf(false); + } catch (Exception e) { + logger.error("isTargetOfInflowControl call error", e.getMessage()); } return false; From 496bdd1468dfe3b51b9fd92b3a5677aea939836b Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:15:04 +0900 Subject: [PATCH 4/5] =?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")); + } } From 437961e7d518df958b2b3109bf19d0c8beec4b3f Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:15:27 +0900 Subject: [PATCH 5/5] =?UTF-8?q?test:=20ReloadInflowClientControlCommandTes?= =?UTF-8?q?t=20/=20RemoveInflowClientControlCommandTest=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PropManager NPE 및 정적 캐시 미초기화로 인한 테스트 실패 수정. InflowControlUtil.cachedInflowControlManager를 ReflectionTestUtils로 직접 주입/초기화하는 방식으로 변경. Co-Authored-By: Claude Sonnet 4.6 --- .../ReloadInflowClientControlCommandTest.java | 35 +++++++++---------- .../RemoveInflowClientControlCommandTest.java | 34 +++++++++--------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java b/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java index c666b33..d0d4985 100644 --- a/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java +++ b/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java @@ -3,32 +3,35 @@ 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.context.ApplicationContext; 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; -import com.eactive.eai.common.util.ApplicationContextProvider; /** * ReloadInflowClientControlCommand 단위 테스트. * - *

ApplicationContextProvider의 static context 필드에 mock ApplicationContext를 주입하여 - * InflowControlManager.getInstance() 반환값을 제어한다. + *

InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여 + * PropManager/ApplicationContext 없이 동작을 검증한다. */ class ReloadInflowClientControlCommandTest { - private ApplicationContext mockContext; private DualInflowControlManager mockDualManager; @BeforeEach void setUp() { - mockContext = mock(ApplicationContext.class); mockDualManager = mock(DualInflowControlManager.class); - ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); + } + + @AfterEach + void tearDown() { + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); } private ReloadInflowClientControlCommand commandWith(Object args) { @@ -43,10 +46,8 @@ class ReloadInflowClientControlCommandTest { @Test void execute_argsIsInteger_throwsCommandException() { - ReloadInflowClientControlCommand cmd = commandWith(Integer.valueOf(1)); - - assertThrows(CommandException.class, cmd::execute); - verifyNoInteractions(mockContext); + // args 검증이 getInflowControlManager() 호출 이전에 발생하므로 캐시 설정 불필요 + assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute); } // ========================================================================= @@ -56,11 +57,9 @@ class ReloadInflowClientControlCommandTest { @Test void execute_notDualManager_throwsCommandException() { InflowControlManager baseMgr = mock(InflowControlManager.class); - when(mockContext.getBean(InflowControlManager.class)).thenReturn(baseMgr); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr); - ReloadInflowClientControlCommand cmd = commandWith("CLIENT_A"); - - assertThrows(CommandException.class, cmd::execute); + assertThrows(CommandException.class, commandWith("CLIENT_A")::execute); } // ========================================================================= @@ -69,7 +68,7 @@ class ReloadInflowClientControlCommandTest { @Test void execute_argsIsALL_callsReloadClientNoArgs() throws Exception { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); Object result = commandWith("ALL").execute(); @@ -84,7 +83,7 @@ class ReloadInflowClientControlCommandTest { @Test void execute_argsIsClientId_callsReloadClientWithId() throws Exception { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); Object result = commandWith("CLIENT_A").execute(); @@ -95,7 +94,7 @@ class ReloadInflowClientControlCommandTest { @Test void execute_differentClientId_callsReloadClientWithThatId() throws Exception { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); commandWith("CLIENT_B").execute(); diff --git a/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java b/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java index c55e07e..10cff5b 100644 --- a/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java +++ b/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java @@ -3,32 +3,35 @@ 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.context.ApplicationContext; 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; -import com.eactive.eai.common.util.ApplicationContextProvider; /** * RemoveInflowClientControlCommand 단위 테스트. * - *

ApplicationContextProvider의 static context 필드에 mock ApplicationContext를 주입하여 - * InflowControlManager.getInstance() 반환값을 제어한다. + *

InflowControlUtil의 cachedInflowControlManager 필드에 mock을 직접 주입하여 + * PropManager/ApplicationContext 없이 동작을 검증한다. */ class RemoveInflowClientControlCommandTest { - private ApplicationContext mockContext; private DualInflowControlManager mockDualManager; @BeforeEach void setUp() { - mockContext = mock(ApplicationContext.class); mockDualManager = mock(DualInflowControlManager.class); - ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); + } + + @AfterEach + void tearDown() { + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); } private RemoveInflowClientControlCommand commandWith(Object args) { @@ -43,10 +46,7 @@ class RemoveInflowClientControlCommandTest { @Test void execute_argsIsInteger_throwsCommandException() { - RemoveInflowClientControlCommand cmd = commandWith(Integer.valueOf(1)); - - assertThrows(CommandException.class, cmd::execute); - verifyNoInteractions(mockContext); + assertThrows(CommandException.class, commandWith(Integer.valueOf(1))::execute); } // ========================================================================= @@ -56,11 +56,9 @@ class RemoveInflowClientControlCommandTest { @Test void execute_notDualManager_throwsCommandException() { InflowControlManager baseMgr = mock(InflowControlManager.class); - when(mockContext.getBean(InflowControlManager.class)).thenReturn(baseMgr); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr); - RemoveInflowClientControlCommand cmd = commandWith("CLIENT_A"); - - assertThrows(CommandException.class, cmd::execute); + assertThrows(CommandException.class, commandWith("CLIENT_A")::execute); } // ========================================================================= @@ -69,7 +67,7 @@ class RemoveInflowClientControlCommandTest { @Test void execute_validClientId_callsRemoveClient() throws CommandException { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); Object result = commandWith("CLIENT_A").execute(); @@ -79,7 +77,7 @@ class RemoveInflowClientControlCommandTest { @Test void execute_differentClientId_callsRemoveClientWithThatId() throws Exception { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); commandWith("CLIENT_B").execute(); @@ -88,7 +86,7 @@ class RemoveInflowClientControlCommandTest { @Test void execute_validClientId_doesNotCallReload() throws Exception { - when(mockContext.getBean(InflowControlManager.class)).thenReturn(mockDualManager); + ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", mockDualManager); commandWith("CLIENT_A").execute();