Merge branch 'feature/inflow-control-improvement' into master

This commit is contained in:
curry772
2026-05-08 10:51:21 +09:00
17 changed files with 504 additions and 481 deletions
@@ -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)) {
@@ -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.");
@@ -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();
}
@@ -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);
}
@@ -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)에 클라이언트 유량제어를 추가한 관리자.
*
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
*/
@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<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> 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<String, TargetMetrics> 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<String, DualCustomBucket> getClientBucketMap() {
return clientBucketMap;
}
}
@@ -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)에서 차단됐는지 반환하는 메서드 추가.
* 클라이언트별 유량제어 메서드도 포함.
*
* <p>클라이언트 유량제어는 {@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);
}
@@ -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)로 관리하는 유량제어 관리자.
*
* <p>기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여
* 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다.
* 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고
* {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로
* 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다.
* <p>클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨.
*
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager</pre>
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
*/
@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<String, DualCustomBucket> adapterBucketMap = new ConcurrentHashMap<>();
private volatile Map<String, DualCustomBucket> interfaceBucketMap = new ConcurrentHashMap<>();
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> adapterMetricsMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> interfaceMetricsMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> clientMetricsMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TargetMetrics> 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<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
protected Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
Map<String, DualCustomBucket> 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<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
protected void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
Map<String, InflowTargetVO> 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<String, TargetMetrics> getAllAdapterMetrics() { return Collections.unmodifiableMap(adapterMetricsMap); }
public Map<String, TargetMetrics> getAllInterfaceMetrics() { return Collections.unmodifiableMap(interfaceMetricsMap); }
public Map<String, TargetMetrics> 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<String, TargetMetrics> metricsMap, String key, String result) {
protected void recordMetrics(ConcurrentHashMap<String, TargetMetrics> 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<String, DualCustomBucket> 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();
}
@@ -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 확장.
*
* <p>DualInflowControlManager와 함께 사용:
* <pre>
* inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
* </pre>
*
* <p>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);
}
/**
* 차단 원인에 따라 에러 메시지에 한도 정보를 포함.
* <ul>
* <li>PER_SECOND: "API 호출 한도 초과 [name: Nreq/sec]"</li>
* <li>THRESHOLD: "API 호출 한도 초과 [name: Nreq/UNIT]"</li>
* <li>그 외: "API 호출 한도 초과 [name]" (기존 동작)</li>
* </ul>
*/
@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);
}
}
@@ -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);
@@ -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);
@@ -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"));
}
}
@@ -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<String, DualCustomBucket> 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<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
}
@Test
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", blockedThresholdDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
}
@Test
void isClientPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
Map<String, DualCustomBucket> 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<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
map.put("C2", passDualBucket("C2"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.isClientPassDetail("C2");
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllClientMetrics();
assertEquals(2, all.size());
assertTrue(all.containsKey("C1"));
assertTrue(all.containsKey("C2"));
}
// =========================================================================
// resetClientMetrics / resetAllClientMetrics
// =========================================================================
@Test
void resetClientMetrics_resetsTargetClient() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.resetClientMetrics("C1");
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
}
@Test
void resetAllClientMetrics_resetsAll() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
map.put("C2", passDualBucket("C2"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.isClientPassDetail("C2");
manager.resetAllClientMetrics();
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
}
// =========================================================================
// removeClient — 메트릭 자동 삭제
// =========================================================================
@Test
void removeClient_cleansUpMetrics() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertNotNull(manager.getClientMetrics("C1"));
manager.removeClient("C1");
assertNull(manager.getClientMetrics("C1"));
}
}
@@ -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() 버킷 생성 동작 검증.
*
* <p>검증 포인트:
* - 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)));
}
}
@@ -21,9 +21,8 @@ import io.github.bucket4j.local.LocalBucket;
/**
* DualInflowControlManager 메트릭 단위 테스트.
*
* <p>start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여
* isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의
* 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다.
* <p>어댑터/인터페이스/그룹 메트릭을 검증한다.
* 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
*/
class DualInflowControlManagerMetricsTest {
@@ -100,10 +99,6 @@ class DualInflowControlManagerMetricsTest {
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
}
private void injectClientMap(Map<String, DualCustomBucket> map) {
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
}
private void injectGroupBucketList(Map<String, CustomGroupBucket> map) {
ReflectionTestUtils.setField(manager, "groupBucketList", map);
}
@@ -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<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
}
@Test
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", blockedThresholdDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
}
// =========================================================================
// getAllXxxMetrics — 조회 및 불변성
// =========================================================================
@@ -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<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.resetClientMetrics("C1");
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
}
@Test
void resetAllClientMetrics_resetsAll() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
map.put("C2", passDualBucket("C2"));
injectClientMap(map);
manager.isClientPassDetail("C1");
manager.isClientPassDetail("C2");
manager.resetAllClientMetrics();
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
}
// =========================================================================
// remove 시 메트릭 자동 삭제
// =========================================================================
@@ -659,18 +588,4 @@ class DualInflowControlManagerMetricsTest {
assertNull(manager.getInterfaceMetrics("IF1"));
}
@Test
void removeClient_cleansUpMetrics() {
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
map.put("C1", passDualBucket("C1"));
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
manager.isClientPassDetail("C1");
assertNotNull(manager.getClientMetrics("C1"));
manager.removeClient("C1");
assertNull(manager.getClientMetrics("C1"));
}
}
@@ -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 단위 테스트.
*
* <p>RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance() 호출한다.
* @BeforeAll에서 mock ApplicationContext를 주입한 DualRequestProcessor를 최초 로딩시킨다.
*
* <p>주의: 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);
}
}
@@ -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');
@@ -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);
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');