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 8afe1e1..becbfeb 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommand.java @@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; 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.inflow.dual.ClientDualInflowControlManager; import com.eactive.eai.common.util.Logger; public class ReloadInflowClientControlCommand extends Command { @@ -31,10 +31,10 @@ public class ReloadInflowClientControlCommand extends Command { String keyName = (String) args; try { AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); - if (!(base instanceof DualInflowControlManager)) { - throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + if (!(base instanceof ClientDualInflowControlManager)) { + throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다."); } - DualInflowControlManager manager = (DualInflowControlManager) base; + ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base; if (keyName != null) { if ("ALL".equals(keyName)) { 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 fe02028..97fe72c 100644 --- a/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java +++ b/src/main/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommand.java @@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command; import com.eactive.eai.agent.command.CommandException; 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.inflow.dual.ClientDualInflowControlManager; import com.eactive.eai.common.util.Logger; public class RemoveInflowClientControlCommand extends Command { @@ -31,10 +31,10 @@ public class RemoveInflowClientControlCommand extends Command { String key = (String) args; try { AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); - if (!(base instanceof DualInflowControlManager)) { - throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다."); + if (!(base instanceof ClientDualInflowControlManager)) { + throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다."); } - DualInflowControlManager manager = (DualInflowControlManager) base; + ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base; manager.removeClient(key); if (logger.isWarn()) logger.warn(this.name + "] " + key + " removed."); diff --git a/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java index 12c23b1..8898d1f 100644 --- a/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java +++ b/src/main/java/com/eactive/eai/common/inflow/AbstractInflowControlManager.java @@ -21,6 +21,7 @@ import com.eactive.eai.common.util.Logger; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; import io.github.bucket4j.local.LocalBucket; import io.github.bucket4j.local.LocalBucketBuilder; @@ -345,12 +346,14 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) { LocalBucketBuilder builder = Bucket4j.builder(); if (inflowTarget.getThresholdPerSecond() > 0) { - builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1))); + builder.addLimit(Bandwidth.classic(inflowTarget.getThresholdPerSecond(), + Refill.greedy(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1)))); } if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) { - builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(), - AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))); + builder.addLimit(Bandwidth.classic(inflowTarget.getThreshold(), + Refill.intervally(inflowTarget.getThreshold(), + AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit())))); } return new CustomBucket(builder.build(), inflowTarget); @@ -369,14 +372,16 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket if (groupVo.getThresholdPerSecond() > 0) { perSecondBucket = Bucket4j.builder() - .addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))) + .addLimit(Bandwidth.classic(groupVo.getThresholdPerSecond(), + Refill.greedy(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()))) + .addLimit(Bandwidth.classic(groupVo.getThreshold(), + Refill.intervally(groupVo.getThreshold(), + AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit())))) .build(); } diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java new file mode 100644 index 0000000..da8d686 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualBucket.java @@ -0,0 +1,22 @@ +package com.eactive.eai.common.inflow.dual; + +import com.eactive.eai.common.inflow.InflowTargetVO; + +/** + * DualBucket에 클라이언트 유량제어 메서드를 추가한 인터페이스. + * {@link ClientDualInflowControlManager}가 구현한다. + */ +public interface ClientDualBucket extends DualBucket { + + /** + * 클라이언트 유량제어 체크 — 차단 원인 포함. + * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 + */ + String isClientPassDetail(String clientId); + + /** + * 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용. + * @return 등록된 설정이 없으면 null + */ + InflowTargetVO getClientInflowThreshold(String clientId); +} diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java new file mode 100644 index 0000000..606d1e7 --- /dev/null +++ b/src/main/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManager.java @@ -0,0 +1,112 @@ +package com.eactive.eai.common.inflow.dual; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +import com.eactive.eai.common.inflow.Bucket; +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.util.ApplicationContextProvider; +import com.eactive.eai.common.util.Logger; + +/** + * 어댑터/인터페이스 이중 버킷(DualInflowControlManager)에 클라이언트 유량제어를 추가한 관리자. + * + *

적용 방법: INFLOW.properties에 아래 한 줄 추가. + *

inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager
+ */ +@Component +public class ClientDualInflowControlManager extends DualInflowControlManager implements ClientDualBucket { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); + + public static synchronized Bucket getBucket() { + return ApplicationContextProvider.getContext().getBean(ClientDualInflowControlManager.class); + } + + private volatile Map clientBucketMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap clientMetricsMap = new ConcurrentHashMap<>(); + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + @Override + public void start() throws LifecycleException { + super.start(); + try { + initClients(); + } catch (Exception e) { + throw new LifecycleException("ClientDualInflowControlManager init failed: " + e.getMessage()); + } + } + + // ------------------------------------------------------------------------- + // 초기화 / 리로드 + // ------------------------------------------------------------------------- + + private void initClients() throws Exception { + this.clientBucketMap = loadBucketMap(InflowType.CLIENT); + if (logger.isInfo()) logger.info("ClientDualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets"); + } + + 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); + } + + // ------------------------------------------------------------------------- + // 클라이언트 메트릭 접근자 + // ------------------------------------------------------------------------- + + public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); } + public Map getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); } + public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); } + public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); } + + // ------------------------------------------------------------------------- + // ClientDualBucket — 클라이언트 유량제어 + // ------------------------------------------------------------------------- + + @Override + public String isClientPassDetail(String clientId) { + DualCustomBucket b = clientBucketMap.get(clientId); + if (b == null) return DualCustomBucket.RESULT_PASS; + String result = b.tryConsume(); + recordMetrics(clientMetricsMap, clientId, result); + return result; + } + + @Override + public InflowTargetVO getClientInflowThreshold(String clientId) { + DualCustomBucket b = clientBucketMap.get(clientId); + return (b == null) ? null : b.getInflowTargetVo(); + } + + // ------------------------------------------------------------------------- + // 모니터링용 접근자 + // ------------------------------------------------------------------------- + + public DualCustomBucket getClientBucket(String clientId) { + return clientBucketMap.get(clientId); + } + + public Map getClientBucketMap() { + return clientBucketMap; + } +} diff --git a/src/main/java/com/eactive/eai/common/inflow/dual/DualBucket.java b/src/main/java/com/eactive/eai/common/inflow/dual/DualBucket.java index 7f7a47c..fa4d18e 100644 --- a/src/main/java/com/eactive/eai/common/inflow/dual/DualBucket.java +++ b/src/main/java/com/eactive/eai/common/inflow/dual/DualBucket.java @@ -1,13 +1,13 @@ package com.eactive.eai.common.inflow.dual; import com.eactive.eai.common.inflow.Bucket; -import com.eactive.eai.common.inflow.InflowTargetVO; /** * Bucket 인터페이스 확장. * 기존 isAdapterPass/isInterfacePass(boolean)에 더해 * 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지 반환하는 메서드 추가. - * 클라이언트별 유량제어 메서드도 포함. + * + *

클라이언트 유량제어는 {@link ClientDualBucket}으로 분리됨. */ public interface DualBucket extends Bucket { @@ -22,16 +22,4 @@ public interface DualBucket extends Bucket { * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 */ String isInterfacePassDetail(String inter); - - /** - * 클라이언트 유량제어 체크 — 차단 원인 포함. - * @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단 - */ - String isClientPassDetail(String clientId); - - /** - * 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용. - * @return 등록된 설정이 없으면 null - */ - InflowTargetVO getClientInflowThreshold(String clientId); } 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 de1b827..d6c415a 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 @@ -9,7 +9,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.Bucket; @@ -22,21 +21,17 @@ import com.eactive.eai.common.util.Logger; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; import io.github.bucket4j.local.LocalBucket; /** - * 어댑터/인터페이스/클라이언트 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자. + * 어댑터/인터페이스 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자. * - *

기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여 - * 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다. - * 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고 - * {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로 - * 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다. + *

클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨. * *

적용 방법: INFLOW.properties에 아래 한 줄 추가. - *

inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
+ *
inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager
*/ -@Component public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket { private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); @@ -48,11 +43,9 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple private volatile Map adapterBucketMap = new ConcurrentHashMap<>(); private volatile Map interfaceBucketMap = new ConcurrentHashMap<>(); - private volatile Map clientBucketMap = 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 { @@ -79,7 +72,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple try { initAdapters(); initInterfaces(); - initClients(); } catch (Exception e) { throw new LifecycleException("DualInflowControlManager init failed: " + e.getMessage()); } @@ -99,12 +91,7 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple if (logger.isInfo()) logger.info("DualInflowControlManager] initInterfaces: " + interfaceBucketMap.size() + " buckets"); } - private void initClients() throws Exception { - this.clientBucketMap = loadBucketMap(InflowType.CLIENT); - if (logger.isInfo()) logger.info("DualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets"); - } - - private Map loadBucketMap(InflowType type) throws Exception { + protected Map loadBucketMap(InflowType type) throws Exception { Map newMap = new HashMap<>(); for (InflowTargetVO vo : inflowControlDAO.getInflowList(type)) { DualCustomBucket bucket = makeBucket(vo); @@ -139,22 +126,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple 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 public synchronized void reloadGroup() throws Exception { super.reloadGroup(); @@ -168,7 +139,7 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple if (m != null) m.reset(); } - private void reloadBucketMap(Map targetMap, InflowType type, String name) throws Exception { + protected void reloadBucketMap(Map targetMap, InflowType type, String name) throws Exception { Map updated = inflowControlDAO.getInflowMap(type, name); if (updated == null) return; for (InflowTargetVO vo : updated.values()) { @@ -179,24 +150,20 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple } // ------------------------------------------------------------------------- - // 어댑터/인터페이스/클라이언트 메트릭 접근자 + // 어댑터/인터페이스 메트릭 접근자 // ------------------------------------------------------------------------- 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() 카운팅 및 접근자 @@ -255,32 +222,15 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple return result; } - @Override - public String isClientPassDetail(String clientId) { - DualCustomBucket b = clientBucketMap.get(clientId); - 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) { + protected 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 - public InflowTargetVO getClientInflowThreshold(String clientId) { - DualCustomBucket b = clientBucketMap.get(clientId); - return (b == null) ? null : b.getInflowTargetVo(); - } - // ------------------------------------------------------------------------- // Bucket 인터페이스 오버라이드 — 이중 버킷으로 교체 (boolean 반환 유지) - // isAdapterPassDetail/isInterfacePassDetail이 토큰을 소비하므로 - // 두 메서드를 동일 요청에서 중복 호출하면 토큰이 이중 소비됨에 주의. // ------------------------------------------------------------------------- @Override @@ -295,7 +245,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple // ------------------------------------------------------------------------- // 어댑터/인터페이스 키·VO·삭제 — Dual 맵 기준으로 오버라이드 - // (base의 adapterBucketList / interfaceBucketList 는 읽지 않아 DAO 이중 호출 제거) // ------------------------------------------------------------------------- @Override @@ -356,19 +305,16 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple return interfaceBucketMap; } - public DualCustomBucket getClientBucket(String clientId) { - return clientBucketMap.get(clientId); - } - - public Map getClientBucketMap() { - return clientBucketMap; - } - // ------------------------------------------------------------------------- // 버킷 팩토리 // ------------------------------------------------------------------------- - private DualCustomBucket makeBucket(InflowTargetVO vo) { + /** + * perSecond 버킷: greedy refill — 초당 N개 요청을 균등하게 허용. + * threshold 버킷: intervally refill — 기간(시/일/월 등) 단위 쿼터를 기간 시작 시 일괄 충전. + * Bandwidth.simple은 두 버킷 모두 greedy로 생성되어 시간 단위 구분이 없어지므로 classic으로 교체. + */ + protected DualCustomBucket makeBucket(InflowTargetVO vo) { if (!AbstractInflowControlManager.isInflowTarget(vo)) { return null; } @@ -378,14 +324,16 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple if (vo.getThresholdPerSecond() > 0) { perSecondBucket = Bucket4j.builder() - .addLimit(Bandwidth.simple(vo.getThresholdPerSecond(), Duration.ofSeconds(1))) + .addLimit(Bandwidth.classic(vo.getThresholdPerSecond(), + Refill.greedy(vo.getThresholdPerSecond(), Duration.ofSeconds(1)))) .build(); } if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) { + Duration period = AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit()); thresholdBucket = Bucket4j.builder() - .addLimit(Bandwidth.simple(vo.getThreshold(), - AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit()))) + .addLimit(Bandwidth.classic(vo.getThreshold(), + Refill.intervally(vo.getThreshold(), period))) .build(); } diff --git a/src/main/java/com/eactive/eai/inbound/processor/DualRequestProcessor.java b/src/main/java/com/eactive/eai/inbound/processor/DualRequestProcessor.java deleted file mode 100644 index 318a326..0000000 --- a/src/main/java/com/eactive/eai/inbound/processor/DualRequestProcessor.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.eactive.eai.inbound.processor; - -import org.apache.commons.lang3.StringUtils; - -import com.eactive.eai.common.inflow.Bucket; -import com.eactive.eai.common.inflow.InflowTargetVO; -import com.eactive.eai.common.inflow.dual.DualBucket; -import com.eactive.eai.common.inflow.dual.DualCustomBucket; - -/** - * 어댑터/인터페이스 유량제어 차단 시 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지 - * 에러 메시지에 포함하는 RequestProcessor 확장. - * - *

DualInflowControlManager와 함께 사용: - *

- * inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
- * 
- * - *

Spring Bean 등록 후 기존 RequestProcessor 대신 이 클래스를 어댑터에 설정한다. - */ -public class DualRequestProcessor extends RequestProcessor { - - @Override - protected String checkClientInflow(Bucket bucket, String clientId) { - if (StringUtils.isBlank(clientId)) return null; - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isClientPassDetail(clientId); - } - return null; - } - - @Override - protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).getClientInflowThreshold(clientId); - } - return null; - } - - @Override - protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isAdapterPassDetail(adapterGroupName); - } - return super.checkAdapterInflow(bucket, adapterGroupName); - } - - @Override - protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) { - if (bucket instanceof DualBucket) { - return ((DualBucket) bucket).isInterfacePassDetail(eaiSvcCd); - } - return super.checkInterfaceInflow(bucket, eaiSvcCd); - } - - /** - * 차단 원인에 따라 에러 메시지에 한도 정보를 포함. - *

    - *
  • PER_SECOND: "API 호출 한도 초과 [name: Nreq/sec]"
  • - *
  • THRESHOLD: "API 호출 한도 초과 [name: Nreq/UNIT]"
  • - *
  • 그 외: "API 호출 한도 초과 [name]" (기존 동작)
  • - *
- */ - @Override - protected String buildInflowTargetErrorMsg(InflowTargetVO inflowTargetVO, String blockedType) { - if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(blockedType)) { - return String.format("API 호출 한도 초과 [%s: %dreq/sec]", - inflowTargetVO.getName(), - inflowTargetVO.getThresholdPerSecond()); - } - if (DualCustomBucket.RESULT_BLOCKED_THRESHOLD.equals(blockedType)) { - return String.format("API 호출 한도 초과 [%s: %dreq/%s]", - inflowTargetVO.getName(), - inflowTargetVO.getThreshold(), - inflowTargetVO.getThresholdTimeUnit()); - } - return super.buildInflowTargetErrorMsg(inflowTargetVO, blockedType); - } -} 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 d0d4985..31ece45 100644 --- a/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java +++ b/src/test/java/com/eactive/eai/agent/inflow/ReloadInflowClientControlCommandTest.java @@ -11,7 +11,7 @@ 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.inflow.dual.ClientDualInflowControlManager; /** * ReloadInflowClientControlCommand 단위 테스트. @@ -21,11 +21,11 @@ import com.eactive.eai.common.inflow.dual.DualInflowControlManager; */ class ReloadInflowClientControlCommandTest { - private DualInflowControlManager mockDualManager; + private ClientDualInflowControlManager mockDualManager; @BeforeEach void setUp() { - mockDualManager = mock(DualInflowControlManager.class); + mockDualManager = mock(ClientDualInflowControlManager.class); ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); } @@ -51,11 +51,11 @@ class ReloadInflowClientControlCommandTest { } // ========================================================================= - // DualInflowControlManager 비활성 + // ClientDualInflowControlManager 비활성 // ========================================================================= @Test - void execute_notDualManager_throwsCommandException() { + void execute_notClientDualManager_throwsCommandException() { InflowControlManager baseMgr = mock(InflowControlManager.class); ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr); 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 10cff5b..5dfb8d4 100644 --- a/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java +++ b/src/test/java/com/eactive/eai/agent/inflow/RemoveInflowClientControlCommandTest.java @@ -11,7 +11,7 @@ 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.inflow.dual.ClientDualInflowControlManager; /** * RemoveInflowClientControlCommand 단위 테스트. @@ -21,11 +21,11 @@ import com.eactive.eai.common.inflow.dual.DualInflowControlManager; */ class RemoveInflowClientControlCommandTest { - private DualInflowControlManager mockDualManager; + private ClientDualInflowControlManager mockDualManager; @BeforeEach void setUp() { - mockDualManager = mock(DualInflowControlManager.class); + mockDualManager = mock(ClientDualInflowControlManager.class); ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null); } @@ -50,11 +50,11 @@ class RemoveInflowClientControlCommandTest { } // ========================================================================= - // DualInflowControlManager 비활성 + // ClientDualInflowControlManager 비활성 // ========================================================================= @Test - void execute_notDualManager_throwsCommandException() { + void execute_notClientDualManager_throwsCommandException() { InflowControlManager baseMgr = mock(InflowControlManager.class); ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr); diff --git a/src/test/java/com/eactive/eai/common/inflow/InflowControlManagerTest.java b/src/test/java/com/eactive/eai/common/inflow/InflowControlManagerTest.java index 01e1a79..8b59d40 100644 --- a/src/test/java/com/eactive/eai/common/inflow/InflowControlManagerTest.java +++ b/src/test/java/com/eactive/eai/common/inflow/InflowControlManagerTest.java @@ -1,5 +1,7 @@ package com.eactive.eai.common.inflow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -36,9 +38,13 @@ class InflowControlManagerTest { InflowControlManager inflowControlManager; @BeforeEach - void setUp() throws LifecycleException { + void setUp() throws Exception { if (!inflowControlManager.isStarted()) { inflowControlManager.start(); + } else { + inflowControlManager.reloadAdapter(); + inflowControlManager.reloadInterface(); + inflowControlManager.reloadGroup(); } } @@ -57,4 +63,22 @@ class InflowControlManagerTest { assertNull(inflowControlManager.getGroupBucket("non-existent-group")); } + @Test + void adapter_spikePasses_thenBurstBlocked() { + for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isAdapterPass("TEST_ADAPTER")); + assertFalse(inflowControlManager.isAdapterPass("TEST_ADAPTER")); + } + + @Test + void interface_spikePasses_thenBurstBlocked() { + for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isInterfacePass("TEST_INTERFACE")); + assertFalse(inflowControlManager.isInterfacePass("TEST_INTERFACE")); + } + + @Test + void group_spikePasses_thenBurstBlocked() { + for (int i = 0; i < 5; i++) assertNull(inflowControlManager.isGroupPass("test-group-quota")); + assertEquals(CustomGroupBucket.RESULT_BLOCKED_THRESHOLD, inflowControlManager.isGroupPass("test-group-quota")); + } + } diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManagerMetricsTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManagerMetricsTest.java new file mode 100644 index 0000000..cb991d2 --- /dev/null +++ b/src/test/java/com/eactive/eai/common/inflow/dual/ClientDualInflowControlManagerMetricsTest.java @@ -0,0 +1,190 @@ +package com.eactive.eai.common.inflow.dual; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.util.Map; +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; + +/** + * ClientDualInflowControlManager 클라이언트 메트릭 단위 테스트. + */ +class ClientDualInflowControlManagerMetricsTest { + + private ClientDualInflowControlManager manager; + + @BeforeEach + void setUp() { + manager = new ClientDualInflowControlManager(); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + 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 LocalBucket stableBucket(long capacity) { + return Bucket4j.builder() + .addLimit(Bandwidth.simple(capacity, Duration.ofHours(1))) + .build(); + } + + private LocalBucket exhaustedBucket(long capacity) { + LocalBucket b = stableBucket(capacity); + b.tryConsume(capacity); + return b; + } + + 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 injectClientMap(Map map) { + ReflectionTestUtils.setField(manager, "clientBucketMap", map); + } + + // ========================================================================= + // 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()); + } + + @Test + void isClientPassDetail_blockedPerSecond_incrementsRejectedPerSecond() { + Map map = new ConcurrentHashMap<>(); + map.put("C1", blockedPerSecondDualBucket("C1")); + injectClientMap(map); + + manager.isClientPassDetail("C1"); + + assertEquals(1, manager.getClientMetrics("C1").rejectedPerSecond.get()); + } + + // ========================================================================= + // getAllClientMetrics — 조회 및 불변성 + // ========================================================================= + + @Test + void getAllClientMetrics_empty_returnsEmptyMap() { + assertTrue(manager.getAllClientMetrics().isEmpty()); + } + + @Test + void getAllClientMetrics_afterCalls_containsEntries() { + Map map = new ConcurrentHashMap<>(); + map.put("C1", passDualBucket("C1")); + map.put("C2", passDualBucket("C2")); + injectClientMap(map); + + manager.isClientPassDetail("C1"); + manager.isClientPassDetail("C2"); + + Map all = manager.getAllClientMetrics(); + assertEquals(2, all.size()); + assertTrue(all.containsKey("C1")); + assertTrue(all.containsKey("C2")); + } + + // ========================================================================= + // resetClientMetrics / resetAllClientMetrics + // ========================================================================= + + @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()); + } + + // ========================================================================= + // removeClient — 메트릭 자동 삭제 + // ========================================================================= + + @Test + void removeClient_cleansUpMetrics() { + Map map = new ConcurrentHashMap<>(); + map.put("C1", passDualBucket("C1")); + injectClientMap(map); + + manager.isClientPassDetail("C1"); + assertNotNull(manager.getClientMetrics("C1")); + + manager.removeClient("C1"); + + assertNull(manager.getClientMetrics("C1")); + } +} diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java new file mode 100644 index 0000000..c1a77c3 --- /dev/null +++ b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java @@ -0,0 +1,92 @@ +package com.eactive.eai.common.inflow.dual; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.eactive.eai.common.inflow.InflowTargetVO; + +/** + * DualInflowControlManager.makeBucket() 버킷 생성 동작 검증. + * + *

검증 포인트: + * - threshold/perSecond 버킷이 실제로 생성되는지 + * - 할당량 이내 spike는 통과하고 초과 burst는 차단되는지 + * - perSecond/threshold 중 어느 쪽이 binding 제약인지 구분되는지 + */ +class DualInflowControlManagerMakeBucketTest { + + private final DualInflowControlManager manager = new DualInflowControlManager(); + + private InflowTargetVO vo(long perSecond, long threshold, String timeUnit) { + InflowTargetVO vo = new InflowTargetVO(); + vo.setName("TEST"); + vo.setThresholdPerSecond(perSecond); + vo.setThreshold(threshold); + vo.setThresholdTimeUnit(timeUnit); + vo.setActivate(true); + return vo; + } + + // ========================================================================= + // threshold 단독 (5건/분) + // ========================================================================= + + @Test + void thresholdOnly_spikePasses_thenBurstBlocked() { + DualCustomBucket bucket = manager.makeBucket(vo(0, 5, "MIN")); + + for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume()); + assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume()); + } + + // ========================================================================= + // perSecond 단독 + // ========================================================================= + + @Test + void perSecondOnly_spikePasses_thenBurstBlocked() { + DualCustomBucket bucket = manager.makeBucket(vo(5, 0, null)); + + for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume()); + assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume()); + } + + // ========================================================================= + // perSecond + threshold 동시 설정 — binding 제약 구분 + // ========================================================================= + + @Test + void both_perSecondIsBinding_blockedByPerSecond() { + // perSecond=3 이 binding: threshold=100/MIN은 여유 있음 + DualCustomBucket bucket = manager.makeBucket(vo(3, 100, "MIN")); + + for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume()); + assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume()); + } + + @Test + void both_thresholdIsBinding_blockedByThreshold() { + // threshold=3/MIN 이 binding: perSecond=100은 여유 있음 + DualCustomBucket bucket = manager.makeBucket(vo(100, 3, "MIN")); + + for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume()); + assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume()); + } + + // ========================================================================= + // 비활성 / 조건 미충족 → null 반환 + // ========================================================================= + + @Test + void inactiveVO_returnsNull() { + InflowTargetVO vo = vo(5, 0, null); + vo.setActivate(false); + assertNull(manager.makeBucket(vo)); + } + + @Test + void noLimitsSet_returnsNull() { + assertNull(manager.makeBucket(vo(0, 0, null))); + } +} 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 c52f309..be4280c 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 @@ -21,9 +21,8 @@ import io.github.bucket4j.local.LocalBucket; /** * DualInflowControlManager 메트릭 단위 테스트. * - *

start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여 - * isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의 - * 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다. + *

어댑터/인터페이스/그룹 메트릭을 검증한다. + * 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다. */ class DualInflowControlManagerMetricsTest { @@ -100,10 +99,6 @@ class DualInflowControlManagerMetricsTest { 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); } @@ -358,7 +353,6 @@ class DualInflowControlManagerMetricsTest { @Test void isAdapterPassDetail_bucketNotRegistered_noMetricsCreated() { - // 버킷 없는 어댑터는 메트릭 맵에 항목을 생성하지 않음 manager.isAdapterPassDetail("UNKNOWN"); assertNull(manager.getAdapterMetrics("UNKNOWN")); @@ -473,39 +467,6 @@ class DualInflowControlManagerMetricsTest { 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 — 조회 및 불변성 // ========================================================================= @@ -537,11 +498,6 @@ class DualInflowControlManagerMetricsTest { assertTrue(manager.getAllInterfaceMetrics().isEmpty()); } - @Test - void getAllClientMetrics_empty_returnsEmptyMap() { - assertTrue(manager.getAllClientMetrics().isEmpty()); - } - // ========================================================================= // resetXxxMetrics / resetAllXxxMetrics // ========================================================================= @@ -560,7 +516,7 @@ class DualInflowControlManagerMetricsTest { manager.resetAdapterMetrics("A1"); assertEquals(0, manager.getAdapterMetrics("A1").allowed.get()); - assertEquals(1, manager.getAdapterMetrics("A2").allowed.get()); // 영향 없음 + assertEquals(1, manager.getAdapterMetrics("A2").allowed.get()); } @Test @@ -601,33 +557,6 @@ class DualInflowControlManagerMetricsTest { 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 시 메트릭 자동 삭제 // ========================================================================= @@ -659,18 +588,4 @@ class DualInflowControlManagerMetricsTest { 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")); - } } diff --git a/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java b/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java deleted file mode 100644 index ac6853d..0000000 --- a/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.eactive.eai.inbound.processor; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.test.util.ReflectionTestUtils; - -import com.eactive.eai.common.inflow.Bucket; -import com.eactive.eai.common.inflow.InflowTargetVO; -import com.eactive.eai.common.inflow.dual.DualBucket; -import com.eactive.eai.common.inflow.dual.DualCustomBucket; -import com.eactive.eai.common.server.EAIServerManager; -import com.eactive.eai.common.util.ApplicationContextProvider; - -/** - * DualRequestProcessor 단위 테스트. - * - *

RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다. - * @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DualRequestProcessor를 최초 로딩시킨다. - * - *

주의: DualRequestProcessor를 필드 타입으로 선언하면 테스트 클래스 로딩 시 함께 로딩되어 - * @BeforeAll 실행 전에 NPE가 발생한다. 반드시 메서드 본문 내에서만 참조해야 한다. - */ -class DualRequestProcessorTest { - - @BeforeAll - static void setupMockApplicationContext() { - ApplicationContext mockContext = mock(ApplicationContext.class); - EAIServerManager mockServerManager = mock(EAIServerManager.class); - when(mockContext.getBean(EAIServerManager.class)).thenReturn(mockServerManager); - when(mockServerManager.getLocalServerName()).thenReturn("TEST-SERVER"); - when(mockServerManager.getGroupInstId()).thenReturn("TEST-INST"); - - ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext); - } - - private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit) { - InflowTargetVO vo = new InflowTargetVO(); - vo.setName(name); - vo.setThresholdPerSecond(perSec); - vo.setThreshold(threshold); - vo.setThresholdTimeUnit(unit); - vo.setActivate(true); - return vo; - } - - // ------------------------------------------------------------------------- - // checkAdapterInflow - // ------------------------------------------------------------------------- - - @Test - void checkAdapterInflow_dualBucket_pass_returnsNull() { - DualRequestProcessor processor = new DualRequestProcessor(); - DualBucket mockBucket = mock(DualBucket.class); - when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(null); - - assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A")); - verify(mockBucket).isAdapterPassDetail("ADAPTER_A"); - } - - @Test - void checkAdapterInflow_dualBucket_blockedPerSecond() { - DualRequestProcessor processor = new DualRequestProcessor(); - DualBucket mockBucket = mock(DualBucket.class); - when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND); - - assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, - processor.checkAdapterInflow(mockBucket, "ADAPTER_A")); - } - - @Test - void checkAdapterInflow_dualBucket_blockedThreshold() { - DualRequestProcessor processor = new DualRequestProcessor(); - DualBucket mockBucket = mock(DualBucket.class); - when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD); - - assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, - processor.checkAdapterInflow(mockBucket, "ADAPTER_A")); - } - - @Test - void checkAdapterInflow_nonDualBucket_pass_returnsNull() { - DualRequestProcessor processor = new DualRequestProcessor(); - Bucket mockBucket = mock(Bucket.class); - when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(true); - - assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A")); - } - - @Test - void checkAdapterInflow_nonDualBucket_blocked_returnsBlocked() { - DualRequestProcessor processor = new DualRequestProcessor(); - Bucket mockBucket = mock(Bucket.class); - when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(false); - - assertEquals("BLOCKED", processor.checkAdapterInflow(mockBucket, "ADAPTER_A")); - } - - // ------------------------------------------------------------------------- - // checkInterfaceInflow - // ------------------------------------------------------------------------- - - @Test - void checkInterfaceInflow_dualBucket_pass_returnsNull() { - DualRequestProcessor processor = new DualRequestProcessor(); - DualBucket mockBucket = mock(DualBucket.class); - when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(null); - - assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001")); - verify(mockBucket).isInterfacePassDetail("IF_001"); - } - - @Test - void checkInterfaceInflow_dualBucket_blockedPerSecond() { - DualRequestProcessor processor = new DualRequestProcessor(); - DualBucket mockBucket = mock(DualBucket.class); - when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND); - - assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, - processor.checkInterfaceInflow(mockBucket, "IF_001")); - } - - @Test - void checkInterfaceInflow_nonDualBucket_pass_returnsNull() { - DualRequestProcessor processor = new DualRequestProcessor(); - Bucket mockBucket = mock(Bucket.class); - when(mockBucket.isInterfacePass("IF_001")).thenReturn(true); - - assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001")); - } - - @Test - void checkInterfaceInflow_nonDualBucket_blocked_returnsBlocked() { - DualRequestProcessor processor = new DualRequestProcessor(); - Bucket mockBucket = mock(Bucket.class); - when(mockBucket.isInterfacePass("IF_001")).thenReturn(false); - - assertEquals("BLOCKED", processor.checkInterfaceInflow(mockBucket, "IF_001")); - } - - // ------------------------------------------------------------------------- - // buildInflowTargetErrorMsg - // ------------------------------------------------------------------------- - - @Test - void buildInflowTargetErrorMsg_perSecond_includesReqPerSec() { - DualRequestProcessor processor = new DualRequestProcessor(); - InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN"); - - String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_PER_SECOND); - - assertEquals("API 호출 한도 초과 [결제API: 30req/sec]", msg); - } - - @Test - void buildInflowTargetErrorMsg_threshold_includesReqPerUnit() { - DualRequestProcessor processor = new DualRequestProcessor(); - InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN"); - - String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD); - - assertEquals("API 호출 한도 초과 [결제API: 1000req/MIN]", msg); - } - - @Test - void buildInflowTargetErrorMsg_blockedFallback_simpleFormat() { - DualRequestProcessor processor = new DualRequestProcessor(); - InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN"); - - String msg = processor.buildInflowTargetErrorMsg(vo, "BLOCKED"); - - assertEquals("API 호출 한도 초과 [결제API]", msg); - } - - @Test - void buildInflowTargetErrorMsg_nullBlockedType_simpleFormat() { - DualRequestProcessor processor = new DualRequestProcessor(); - InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN"); - - String msg = processor.buildInflowTargetErrorMsg(vo, null); - - assertEquals("API 호출 한도 초과 [결제API]", msg); - } - - @Test - void buildInflowTargetErrorMsg_hourUnit() { - DualRequestProcessor processor = new DualRequestProcessor(); - InflowTargetVO vo = makeVO("조회API", 10, 500, "HOUR"); - - String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD); - - assertEquals("API 호출 한도 초과 [조회API: 500req/HOUR]", msg); - } -} diff --git a/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql b/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql index d55aeb7..3b97c89 100644 --- a/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql +++ b/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql @@ -1,2 +1,3 @@ INSERT INTO inflow_control_group (group_id, group_name, threshold, threshold_per_second, threshold_time_unit, use_yn) VALUES - ('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1'); + ('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1'), + ('test-group-quota', '쿼터전용 그룹', 5, 0, 'MIN', '1'); diff --git a/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql b/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql index ac25a7f..ec220ab 100644 --- a/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql +++ b/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql @@ -1,6 +1,8 @@ -INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES - ('01','_AA0_IN_NET_AsS',10,'1',0,NULL), - ('01','_AB0_IN_NET_AsS',3,'1',0,NULL), - ('01','_TST_IN_NET_AsS',1,'1',0,NULL), - ('02','FASSFEP00000004S2',10,'1',0,NULL), - ('02','FDASFEP00000009S2',2,'1',0,NULL); \ No newline at end of file +INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES + ('01','_AA0_IN_NET_AsS',10,'1',0,NULL), + ('01','_AB0_IN_NET_AsS',3,'1',0,NULL), + ('01','_TST_IN_NET_AsS',1,'1',0,NULL), + ('02','FASSFEP00000004S2',10,'1',0,NULL), + ('02','FDASFEP00000009S2',2,'1',0,NULL), + ('01','TEST_ADAPTER',5,'1',0,'MIN'), + ('02','TEST_INTERFACE',5,'1',0,'MIN'); \ No newline at end of file