diff --git a/spring-scheduling/pom.xml b/spring-scheduling/pom.xml index bf7c922e1ed1..9ea5de1a5ccf 100644 --- a/spring-scheduling/pom.xml +++ b/spring-scheduling/pom.xml @@ -10,7 +10,7 @@ com.baeldung - parent-boot-3 + parent-boot-4 0.0.1-SNAPSHOT ../parent-boot-3 @@ -26,19 +26,20 @@ ${spring-retry.version} - org.springframework - spring-aspects - ${spring-aspects.version} + org.springframework.boot + spring-boot-starter-aop org.springframework.boot spring-boot-starter-web + - org.springframework - spring-test + org.springframework.boot + spring-boot-starter-test test + org.apache.commons commons-lang3 diff --git a/spring-scheduling/src/main/java/com/baeldung/springretry/MyService.java b/spring-scheduling/src/main/java/com/baeldung/springretry/MyService.java index 25364442c9fe..3d13676fd2df 100644 --- a/spring-scheduling/src/main/java/com/baeldung/springretry/MyService.java +++ b/spring-scheduling/src/main/java/com/baeldung/springretry/MyService.java @@ -5,7 +5,7 @@ import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; - +import org.springframework.resilience.annotation.ConcurrencyLimit; // NOTE: Assumes Spring Framework 7.0+ package public interface MyService { @@ -19,11 +19,15 @@ public interface MyService { void retryServiceWithCustomization(String sql) throws SQLException; @Retryable(retryFor = SQLException.class, maxAttemptsExpression = "${retry.maxAttempts}", - backoff = @Backoff(delayExpression = "${retry.maxDelay}")) + backoff = @Backoff(delayExpression = "${retry.maxDelay}")) void retryServiceWithExternalConfiguration(String sql) throws SQLException; @Recover void recover(SQLException e, String sql); void templateRetryService(); + + // **NEW Method with Concurrency Limit** + @ConcurrencyLimit(5) + void concurrentLimitService(); } diff --git a/spring-scheduling/src/main/java/com/baeldung/springretry/MyServiceImpl.java b/spring-scheduling/src/main/java/com/baeldung/springretry/MyServiceImpl.java index 44c1d3e1db35..232cf1b5c328 100644 --- a/spring-scheduling/src/main/java/com/baeldung/springretry/MyServiceImpl.java +++ b/spring-scheduling/src/main/java/com/baeldung/springretry/MyServiceImpl.java @@ -55,4 +55,17 @@ public void templateRetryService() { logger.info("throw RuntimeException in method templateRetryService()"); throw new RuntimeException(); } + + // **NEW Implementation for Concurrency Limit** + @Override + public void concurrentLimitService() { + logger.info("Concurrency Limit Active. Current Thread: " + Thread.currentThread().getName()); + // Simulate a time-consuming task to observe throttling + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + logger.info("Concurrency Limit Released. Current Thread: " + Thread.currentThread().getName()); + } } diff --git a/spring-scheduling/src/test/java/com/baeldung/springretry/SpringRetryIntegrationTest.java b/spring-scheduling/src/test/java/com/baeldung/springretry/SpringRetryIntegrationTest.java index f2ab3d2c5d8e..0e959506cc1a 100644 --- a/spring-scheduling/src/test/java/com/baeldung/springretry/SpringRetryIntegrationTest.java +++ b/spring-scheduling/src/test/java/com/baeldung/springretry/SpringRetryIntegrationTest.java @@ -18,6 +18,11 @@ import java.sql.SQLException; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import com.baeldung.springretry.logging.LogAppender; @@ -88,6 +93,59 @@ public void givenTemplateRetryServiceWithZeroAttempts_whenCallWithException_then myService.templateRetryService(); return null; }); - verify(myService, times(1)).templateRetryService(); + verify(myService, times(1)).templateRetryService(); + } + + // ------------------------------------------------------------------ + // NEW TEST FOR @ConcurrencyLimit + // ------------------------------------------------------------------ + @Test + public void givenConcurrentLimitService_whenCalledByManyThreads_thenLimitIsEnforced() throws InterruptedException { + int limit = 5; + int totalThreads = 10; + // Latch to hold all threads until we're ready to start + CountDownLatch startLatch = new CountDownLatch(1); + // Latch to wait for all threads to finish + CountDownLatch finishLatch = new CountDownLatch(totalThreads); + // Counter for the number of threads that started execution (should equal the limit) + AtomicInteger activeThreads = new AtomicInteger(0); + + ExecutorService executor = Executors.newFixedThreadPool(totalThreads); + + for (int i = 0; i < totalThreads; i++) { + executor.submit(() -> { + try { + // Wait until all threads are created and ready + startLatch.await(); + + // Call the method with the concurrency limit + activeThreads.incrementAndGet(); // Increment before method call + myService.concurrentLimitService(); + activeThreads.decrementAndGet(); // Decrement after method call + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + finishLatch.countDown(); + } + }); + } + + // 1. Release all threads simultaneously + startLatch.countDown(); + + // Give the initial threads time to enter the method and block the rest (Service sleeps for 1000ms) + Thread.sleep(200); + + // 2. Assert that only 'limit' number of threads are active + assertEquals(limit, activeThreads.get()); + + // 3. Wait for all threads to finish execution (up to 2 seconds) + finishLatch.await(2, TimeUnit.SECONDS); + + // 4. Final verification that all threads completed + assertEquals(0, activeThreads.get()); + + executor.shutdownNow(); } }