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.Bandwidth;
|
||||||
import io.github.bucket4j.Bucket4j;
|
import io.github.bucket4j.Bucket4j;
|
||||||
|
import io.github.bucket4j.Refill;
|
||||||
import io.github.bucket4j.local.LocalBucket;
|
import io.github.bucket4j.local.LocalBucket;
|
||||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||||
|
|
||||||
@@ -345,12 +346,14 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket
|
|||||||
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
||||||
LocalBucketBuilder builder = Bucket4j.builder();
|
LocalBucketBuilder builder = Bucket4j.builder();
|
||||||
if (inflowTarget.getThresholdPerSecond() > 0) {
|
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())) {
|
if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) {
|
||||||
builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(),
|
builder.addLimit(Bandwidth.classic(inflowTarget.getThreshold(),
|
||||||
AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit())));
|
Refill.intervally(inflowTarget.getThreshold(),
|
||||||
|
AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CustomBucket(builder.build(), inflowTarget);
|
return new CustomBucket(builder.build(), inflowTarget);
|
||||||
@@ -369,14 +372,16 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket
|
|||||||
|
|
||||||
if (groupVo.getThresholdPerSecond() > 0) {
|
if (groupVo.getThresholdPerSecond() > 0) {
|
||||||
perSecondBucket = Bucket4j.builder()
|
perSecondBucket = Bucket4j.builder()
|
||||||
.addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1)))
|
.addLimit(Bandwidth.classic(groupVo.getThresholdPerSecond(),
|
||||||
|
Refill.greedy(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) {
|
if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) {
|
||||||
thresholdBucket = Bucket4j.builder()
|
thresholdBucket = Bucket4j.builder()
|
||||||
.addLimit(Bandwidth.simple(groupVo.getThreshold(),
|
.addLimit(Bandwidth.classic(groupVo.getThreshold(),
|
||||||
AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit())))
|
Refill.intervally(groupVo.getThreshold(),
|
||||||
|
AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit()))))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.eactive.eai.common.inflow;
|
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.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -36,9 +38,13 @@ class InflowControlManagerTest {
|
|||||||
InflowControlManager inflowControlManager;
|
InflowControlManager inflowControlManager;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws LifecycleException {
|
void setUp() throws Exception {
|
||||||
if (!inflowControlManager.isStarted()) {
|
if (!inflowControlManager.isStarted()) {
|
||||||
inflowControlManager.start();
|
inflowControlManager.start();
|
||||||
|
} else {
|
||||||
|
inflowControlManager.reloadAdapter();
|
||||||
|
inflowControlManager.reloadInterface();
|
||||||
|
inflowControlManager.reloadGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,4 +63,22 @@ class InflowControlManagerTest {
|
|||||||
assertNull(inflowControlManager.getGroupBucket("non-existent-group"));
|
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
|
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
|
INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES
|
||||||
('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
|
('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
|
||||||
('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
|
('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
|
||||||
('01','_TST_IN_NET_AsS',1,'1',0,NULL),
|
('01','_TST_IN_NET_AsS',1,'1',0,NULL),
|
||||||
('02','FASSFEP00000004S2',10,'1',0,NULL),
|
('02','FASSFEP00000004S2',10,'1',0,NULL),
|
||||||
('02','FDASFEP00000009S2',2,'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