Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions spring-scheduling/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-3</artifactId>
<artifactId>parent-boot-4</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent-boot-3</relativePath>
</parent>
Expand All @@ -26,19 +26,20 @@
<version>${spring-retry.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring-aspects.version}</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}