mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-14 08:18:23 +00:00
fix: Fix test stability issues
This commit is contained in:
1 parent
13cde46f2e
commit
177dc52704
24 files changed
+253
-111
No files matched your search
@@ -43,6 +43,11 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
|||||||
public class App {
|
public class App {
|
||||||
/** Program entry point. */
|
/** Program entry point. */
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(App.class, args);
|
var context = SpringApplication.run(App.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,6 @@ class AppTest {
|
|||||||
/** Entry point */
|
/** Entry point */
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteApplicationWithoutException() {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
assertDoesNotThrow(() -> App.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -73,6 +73,11 @@ public class Main {
|
|||||||
* @param args command line args
|
* @param args command line args
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Main.class, args);
|
var context = SpringApplication.run(Main.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -32,6 +32,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
class MainTest {
|
class MainTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteApplicationWithoutException() {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
assertDoesNotThrow(() -> Main.main(new String[] {}));
|
assertDoesNotThrow(() -> Main.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -72,6 +72,11 @@ public class Main {
|
|||||||
* @param args command line args
|
* @param args command line args
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Main.class, args);
|
var context = SpringApplication.run(Main.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -32,6 +32,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
class MainTest {
|
class MainTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteApplicationWithoutException() {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
assertDoesNotThrow(() -> Main.main(new String[] {}));
|
assertDoesNotThrow(() -> Main.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -72,6 +72,11 @@ public class Main {
|
|||||||
* @param args command line args
|
* @param args command line args
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Main.class, args);
|
var context = SpringApplication.run(Main.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -33,6 +33,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
class MainTest {
|
class MainTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteApplicationWithoutException() {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
assertDoesNotThrow(() -> Main.main(new String[] {}));
|
assertDoesNotThrow(() -> Main.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-1
@@ -45,7 +45,12 @@ import org.springframework.context.annotation.Bean;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class App {
|
public class App {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(App.class, args);
|
var context = SpringApplication.run(App.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ class AppTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMain() {
|
void testMain() {
|
||||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
assertDoesNotThrow(() -> App.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ public class App {
|
|||||||
* @param args command line args
|
* @param args command line args
|
||||||
*/
|
*/
|
||||||
public static void main(final String[] args) {
|
public static void main(final String[] args) {
|
||||||
SpringApplication.run(App.class, args);
|
var context = SpringApplication.run(App.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,6 @@ import org.junit.jupiter.api.Test;
|
|||||||
public class AppTest {
|
public class AppTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteApplicationWithoutException() {
|
void shouldExecuteApplicationWithoutException() {
|
||||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
assertDoesNotThrow(() -> App.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,11 @@ public class App {
|
|||||||
* @param args command line args.
|
* @param args command line args.
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(App.class, args);
|
var context = SpringApplication.run(App.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-1
@@ -44,9 +44,22 @@ public class DataSourceService {
|
|||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
/** Constructor & Scheduler to push random data. */
|
/** Constructor & Scheduler to push random data. */
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired
|
||||||
public DataSourceService(DataRepository repository) {
|
public DataSourceService(DataRepository repository) {
|
||||||
|
this(repository, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with option to start the scheduler.
|
||||||
|
*
|
||||||
|
* @param repository the data repository
|
||||||
|
* @param startScheduler true to start the data generation scheduler
|
||||||
|
*/
|
||||||
|
public DataSourceService(DataRepository repository, boolean startScheduler) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
scheduleDataGeneration();
|
if (startScheduler) {
|
||||||
|
scheduleDataGeneration();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleDataGeneration() {
|
private void scheduleDataGeneration() {
|
||||||
@@ -78,4 +91,9 @@ public class DataSourceService {
|
|||||||
public Map<Integer, String> getAllData() {
|
public Map<Integer, String> getAllData() {
|
||||||
return repository.findAll();
|
return repository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@javax.annotation.PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -28,12 +28,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@EmbeddedKafka(
|
||||||
|
partitions = 1,
|
||||||
|
topics = {"updates", "API"})
|
||||||
public class AppTest {
|
public class AppTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void polling() {
|
void polling() {
|
||||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
assertDoesNotThrow(() -> App.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -38,7 +38,7 @@ public class DataSourceServiceTest {
|
|||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
repository = new DataRepository();
|
repository = new DataRepository();
|
||||||
service = new DataSourceService(repository);
|
service = new DataSourceService(repository, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -77,8 +77,8 @@ public class DataSourceServiceTest {
|
|||||||
|
|
||||||
Map<Integer, String> result1 = service.getAllData();
|
Map<Integer, String> result1 = service.getAllData();
|
||||||
|
|
||||||
assertEquals(size + 2, result.size(), "Should return all stored data.");
|
assertEquals(size + 2, result1.size(), "Should return all stored data.");
|
||||||
assertEquals("First", result.get(1), "Value for key 1 should be 'First'.");
|
assertEquals("First", result1.get(1), "Value for key 1 should be 'First'.");
|
||||||
assertEquals("Second", result.get(2), "Value for key 2 should be 'Second'.");
|
assertEquals("Second", result1.get(2), "Value for key 2 should be 'Second'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class App {
|
public class App {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(App.class, args);
|
var context = SpringApplication.run(App.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-1
@@ -29,12 +29,16 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.kafka.test.context.EmbeddedKafka;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@EmbeddedKafka(
|
||||||
|
partitions = 1,
|
||||||
|
topics = {"updates", "API"})
|
||||||
public class AppTest {
|
public class AppTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void subscriber() {
|
void subscriber() {
|
||||||
assertDoesNotThrow(() -> App.main(new String[] {}));
|
assertDoesNotThrow(() -> App.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+33
-4
@@ -10,13 +10,28 @@ public class AdaptiveRateLimiter implements RateLimiter {
|
|||||||
private final AtomicInteger currentLimit;
|
private final AtomicInteger currentLimit;
|
||||||
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
|
||||||
private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
|
private final ScheduledExecutorService healthChecker = Executors.newScheduledThreadPool(1);
|
||||||
|
private final long checkInterval;
|
||||||
|
private final TimeUnit timeUnit;
|
||||||
|
|
||||||
public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
|
public AdaptiveRateLimiter(int initialLimit, int maxLimit) {
|
||||||
|
this(initialLimit, maxLimit, 10, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with option to set custom interval.
|
||||||
|
*
|
||||||
|
* @param initialLimit initial rate limit
|
||||||
|
* @param maxLimit maximum rate limit
|
||||||
|
* @param interval check interval
|
||||||
|
* @param timeUnit check interval time unit
|
||||||
|
*/
|
||||||
|
public AdaptiveRateLimiter(int initialLimit, int maxLimit, long interval, TimeUnit timeUnit) {
|
||||||
this.initialLimit = initialLimit;
|
this.initialLimit = initialLimit;
|
||||||
this.maxLimit = maxLimit;
|
this.maxLimit = maxLimit;
|
||||||
this.currentLimit = new AtomicInteger(initialLimit);
|
this.currentLimit = new AtomicInteger(initialLimit);
|
||||||
// Periodically increase limit to recover if system appears healthy
|
this.checkInterval = interval;
|
||||||
healthChecker.scheduleAtFixedRate(this::adjustLimits, 10, 10, TimeUnit.SECONDS);
|
this.timeUnit = timeUnit;
|
||||||
|
healthChecker.scheduleAtFixedRate(this::adjustLimits, interval, interval, timeUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -24,9 +39,10 @@ public class AdaptiveRateLimiter implements RateLimiter {
|
|||||||
String key = serviceName + ":" + operationName;
|
String key = serviceName + ":" + operationName;
|
||||||
int current = currentLimit.get();
|
int current = currentLimit.get();
|
||||||
|
|
||||||
// Reuse or create TokenBucket for this key using currentLimit
|
// Reuse or create TokenBucket for this key using currentLimit and checkInterval
|
||||||
RateLimiter limiter =
|
RateLimiter limiter =
|
||||||
limiters.computeIfAbsent(key, k -> new TokenBucketRateLimiter(current, current));
|
limiters.computeIfAbsent(
|
||||||
|
key, k -> new TokenBucketRateLimiter(current, current, checkInterval, timeUnit));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
limiter.check(serviceName, operationName);
|
limiter.check(serviceName, operationName);
|
||||||
@@ -47,4 +63,17 @@ public class AdaptiveRateLimiter implements RateLimiter {
|
|||||||
int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
|
int updated = currentLimit.updateAndGet(curr -> Math.min(maxLimit, curr + (initialLimit / 2)));
|
||||||
System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated);
|
System.out.printf("[Adaptive] Health check passed - Increasing limit to %d%n", updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gracefully shut down the health checker executor and child limiters. */
|
||||||
|
public void shutdown() {
|
||||||
|
healthChecker.shutdown();
|
||||||
|
limiters
|
||||||
|
.values()
|
||||||
|
.forEach(
|
||||||
|
limiter -> {
|
||||||
|
if (limiter instanceof TokenBucketRateLimiter) {
|
||||||
|
((TokenBucketRateLimiter) limiter).shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+30
-7
@@ -4,8 +4,10 @@ import java.util.concurrent.*;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token Bucket rate limiter implementation. Allows requests to proceed as long as there are tokens
|
* Token Bucket rate limiter implementation. Allows requests to proceed as long
|
||||||
* available in the bucket. Tokens are added at a fixed interval up to a defined capacity.
|
* as there are tokens
|
||||||
|
* available in the bucket. Tokens are added at a fixed interval up to a defined
|
||||||
|
* capacity.
|
||||||
*/
|
*/
|
||||||
public class TokenBucketRateLimiter implements RateLimiter {
|
public class TokenBucketRateLimiter implements RateLimiter {
|
||||||
private final int capacity;
|
private final int capacity;
|
||||||
@@ -14,10 +16,22 @@ public class TokenBucketRateLimiter implements RateLimiter {
|
|||||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
|
||||||
public TokenBucketRateLimiter(int capacity, int refillRate) {
|
public TokenBucketRateLimiter(int capacity, int refillRate) {
|
||||||
|
this(capacity, refillRate, 1, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with custom refill interval.
|
||||||
|
*
|
||||||
|
* @param capacity token bucket capacity
|
||||||
|
* @param refillRate token refill rate
|
||||||
|
* @param refillInterval refill interval value
|
||||||
|
* @param timeUnit refill interval time unit
|
||||||
|
*/
|
||||||
|
public TokenBucketRateLimiter(
|
||||||
|
int capacity, int refillRate, long refillInterval, TimeUnit timeUnit) {
|
||||||
this.capacity = capacity;
|
this.capacity = capacity;
|
||||||
this.refillRate = refillRate;
|
this.refillRate = refillRate;
|
||||||
// Refill tokens in all buckets every second
|
scheduler.scheduleAtFixedRate(this::refillBuckets, refillInterval, refillInterval, timeUnit);
|
||||||
scheduler.scheduleAtFixedRate(this::refillBuckets, 1, 1, TimeUnit.SECONDS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -39,7 +53,10 @@ public class TokenBucketRateLimiter implements RateLimiter {
|
|||||||
buckets.forEach((k, b) -> b.refill(refillRate));
|
buckets.forEach((k, b) -> b.refill(refillRate));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inner class that represents the bucket holding tokens for each service-operation. */
|
/**
|
||||||
|
* Inner class that represents the bucket holding tokens for each
|
||||||
|
* service-operation.
|
||||||
|
*/
|
||||||
private static class TokenBucket {
|
private static class TokenBucket {
|
||||||
private final int capacity;
|
private final int capacity;
|
||||||
private final AtomicInteger tokens;
|
private final AtomicInteger tokens;
|
||||||
@@ -52,8 +69,10 @@ public class TokenBucketRateLimiter implements RateLimiter {
|
|||||||
boolean tryConsume() {
|
boolean tryConsume() {
|
||||||
while (true) {
|
while (true) {
|
||||||
int current = tokens.get();
|
int current = tokens.get();
|
||||||
if (current <= 0) return false;
|
if (current <= 0)
|
||||||
if (tokens.compareAndSet(current, current - 1)) return true;
|
return false;
|
||||||
|
if (tokens.compareAndSet(current, current - 1))
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +80,8 @@ public class TokenBucketRateLimiter implements RateLimiter {
|
|||||||
tokens.getAndUpdate(current -> Math.min(current + amount, capacity));
|
tokens.getAndUpdate(current -> Math.min(current + amount, capacity));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+46
-35
@@ -8,49 +8,60 @@ class AdaptiveRateLimiterTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldDecreaseLimitWhenThrottled() throws Exception {
|
void shouldDecreaseLimitWhenThrottled() throws Exception {
|
||||||
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
|
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
|
||||||
|
try {
|
||||||
// Exceed initial limit
|
// Exceed initial limit
|
||||||
for (int i = 0; i < 11; i++) {
|
for (int i = 0; i < 11; i++) {
|
||||||
try {
|
try {
|
||||||
limiter.check("test", "op");
|
limiter.check("test", "op");
|
||||||
} catch (RateLimitException e) {
|
} catch (RateLimitException e) {
|
||||||
// Expected after 10 requests
|
// Expected after 10 requests
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Verify limit was reduced
|
// Verify limit was reduced
|
||||||
assertThrows(
|
assertThrows(
|
||||||
RateLimitException.class,
|
RateLimitException.class,
|
||||||
() -> {
|
() -> {
|
||||||
for (int i = 0; i < 6; i++) { // New limit should be 5 (10/2)
|
for (int i = 0; i < 6; i++) { // New limit should be 5 (10/2)
|
||||||
limiter.check("test", "op");
|
limiter.check("test", "op");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
limiter.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGraduallyIncreaseLimitWhenHealthy() throws Exception {
|
void shouldGraduallyIncreaseLimitWhenHealthy() throws Exception {
|
||||||
AdaptiveRateLimiter limiter =
|
AdaptiveRateLimiter limiter =
|
||||||
new AdaptiveRateLimiter(4, 10); // Start from 4 → expect 2 → expect increase to 4
|
new AdaptiveRateLimiter(
|
||||||
|
4,
|
||||||
// Force throttling to reduce limit
|
10,
|
||||||
for (int i = 0; i < 5; i++) {
|
100,
|
||||||
try {
|
java.util.concurrent.TimeUnit
|
||||||
limiter.check("test", "op");
|
.MILLISECONDS); // Start from 4 → expect 2 → expect increase to 4
|
||||||
} catch (RateLimitException e) {
|
try {
|
||||||
// Expected to throttle and reduce limit
|
// Force throttling to reduce limit
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
try {
|
||||||
|
limiter.check("test", "op");
|
||||||
|
} catch (RateLimitException e) {
|
||||||
|
// Expected to throttle and reduce limit
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for health check to increase limit
|
||||||
|
Thread.sleep(150); // Wait slightly more than 100 milliseconds
|
||||||
|
|
||||||
|
// Allow up to 4 requests again (limit should've increased to 4)
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
limiter.check("test", "op");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5th should throw exception again
|
||||||
|
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
||||||
|
} finally {
|
||||||
|
limiter.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for health check to increase limit
|
|
||||||
Thread.sleep(11000); // Wait slightly more than 10 seconds
|
|
||||||
|
|
||||||
// Allow up to 4 requests again (limit should've increased to 4)
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
limiter.check("test", "op");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5th should throw exception again
|
|
||||||
assertThrows(RateLimitException.class, () -> limiter.check("test", "op"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+50
-42
@@ -13,57 +13,65 @@ class ConcurrencyTests {
|
|||||||
int requestLimit = 5;
|
int requestLimit = 5;
|
||||||
RateLimiter limiter = new TokenBucketRateLimiter(requestLimit, requestLimit);
|
RateLimiter limiter = new TokenBucketRateLimiter(requestLimit, requestLimit);
|
||||||
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||||
CountDownLatch latch = new CountDownLatch(threadCount);
|
try {
|
||||||
|
CountDownLatch latch = new CountDownLatch(threadCount);
|
||||||
|
|
||||||
AtomicInteger successCount = new AtomicInteger();
|
AtomicInteger successCount = new AtomicInteger();
|
||||||
AtomicInteger failureCount = new AtomicInteger();
|
AtomicInteger failureCount = new AtomicInteger();
|
||||||
|
|
||||||
for (int i = 0; i < threadCount; i++) {
|
for (int i = 0; i < threadCount; i++) {
|
||||||
executor.submit(
|
executor.submit(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
limiter.check("test", "op");
|
limiter.check("test", "op");
|
||||||
successCount.incrementAndGet();
|
successCount.incrementAndGet();
|
||||||
} catch (RateLimitException e) {
|
} catch (RateLimitException e) {
|
||||||
failureCount.incrementAndGet();
|
failureCount.incrementAndGet();
|
||||||
}
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await();
|
||||||
|
assertEquals(requestLimit, successCount.get());
|
||||||
|
assertEquals(threadCount - requestLimit, failureCount.get());
|
||||||
|
} finally {
|
||||||
|
executor.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
latch.await();
|
|
||||||
assertEquals(requestLimit, successCount.get());
|
|
||||||
assertEquals(threadCount - requestLimit, failureCount.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void adaptiveLimiterShouldAdjustUnderLoad() throws Exception {
|
void adaptiveLimiterShouldAdjustUnderLoad() throws Exception {
|
||||||
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20);
|
AdaptiveRateLimiter limiter = new AdaptiveRateLimiter(10, 20, 100, TimeUnit.MILLISECONDS);
|
||||||
ExecutorService executor = Executors.newFixedThreadPool(20);
|
ExecutorService executor = Executors.newFixedThreadPool(20);
|
||||||
|
try {
|
||||||
// Flood with requests to trigger throttling
|
// Flood with requests to trigger throttling
|
||||||
for (int i = 0; i < 30; i++) {
|
for (int i = 0; i < 30; i++) {
|
||||||
executor.submit(
|
executor.submit(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
limiter.check("test", "op");
|
limiter.check("test", "op");
|
||||||
} catch (RateLimitException ignored) {
|
} catch (RateLimitException ignored) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
Thread.sleep(15000); // Wait for adjustment
|
|
||||||
|
|
||||||
// Verify new limit is in effect
|
|
||||||
int allowed = 0;
|
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
try {
|
|
||||||
limiter.check("test", "op");
|
|
||||||
allowed++;
|
|
||||||
} catch (RateLimitException ignored) {
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
assertTrue(allowed > 5 && allowed < 15); // Should be between initial and max
|
Thread.sleep(150); // Wait for adjustment
|
||||||
|
|
||||||
|
// Verify new limit is in effect
|
||||||
|
int allowed = 0;
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
try {
|
||||||
|
limiter.check("test", "op");
|
||||||
|
allowed++;
|
||||||
|
} catch (RateLimitException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(allowed > 5 && allowed < 15); // Should be between initial and max
|
||||||
|
} finally {
|
||||||
|
executor.shutdown();
|
||||||
|
limiter.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,12 @@ public class SingleTableInheritance implements CommandLineRunner {
|
|||||||
* @param args program runtime arguments
|
* @param args program runtime arguments
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(SingleTableInheritance.class, args);
|
var context = SpringApplication.run(SingleTableInheritance.class, args);
|
||||||
|
if (args.length > 0 && "test".equals(args[0])) {
|
||||||
|
// Close the context immediately during tests to prevent Tomcat/background threads from
|
||||||
|
// hanging the JVM
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ class SingleTableInheritanceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteWithoutException() {
|
void shouldExecuteWithoutException() {
|
||||||
assertDoesNotThrow(() -> SingleTableInheritance.main(new String[] {}));
|
assertDoesNotThrow(() -> SingleTableInheritance.main(new String[] {"test"}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user