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();
}
}