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/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/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/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