fix: AbstractInflowControlManager Bandwidth.simple → classic+intervally 버킷 생성 버그 수정 및 테스트 추가
- makeBucketUsingInflowVO: 어댑터/인터페이스 단일 버킷 threshold 리필을 greedy → intervally 변경 - makeGroupBucket: 그룹 threshold 버킷 동일하게 수정 - DualInflowControlManagerMakeBucketTest: 버킷 생성 로직 직접 검증 테스트 추가 - InflowControlManagerTest: @BeforeEach reload + spike/burst 경계 케이스 테스트 추가 - SQL 픽스처: MIN 단위 TEST_ADAPTER/TEST_INTERFACE/test-group-quota 항목 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+92
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user