Table of Contents
- Introduction to TimeLimiter
- Resilience4j TimeLimiter
- Guava TimeLimiter
- Spring @Timeout Annotation
- Custom TimeLimiter Implementation
- Async Timeout Control
- Testing Timeout Scenarios
- Best Practices and Patterns
Introduction to TimeLimiter
TimeLimiter is a critical component in building resilient applications that need to enforce execution timeouts. It helps prevent resource exhaustion and improves system responsiveness by limiting how long operations can take.
Key Benefits:
- Prevent resource exhaustion: Stop long-running operations
- Improve responsiveness: Fail fast instead of waiting indefinitely
- Circuit breaker integration: Work with other resilience patterns
- Graceful degradation: Provide fallbacks when timeouts occur
Resilience4j TimeLimiter
1. Basic Resilience4j Setup
<!-- Maven Dependencies --> <dependencies> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies>
2. Configuration Properties
# application.yml resilience4j: timelimiter: configs: default: timeout-duration: 3s cancel-running-future: true slow-service: timeout-duration: 5s cancel-running-future: true fast-service: timeout-duration: 1s cancel-running-future: false instances: externalService: base-config: slow-service timeout-duration: 10s databaseService: base-config: default timeout-duration: 2s cacheService: base-config: fast-service spring: cloud: circuitbreaker: resilience4j: enabled: true
3. Service with TimeLimiter
// ExternalService.java
@Service
public class ExternalService {
private static final Logger logger = LoggerFactory.getLogger(ExternalService.class);
private final TimeLimiterRegistry timeLimiterRegistry;
private final CircuitBreakerRegistry circuitBreakerRegistry;
public ExternalService(TimeLimiterRegistry timeLimiterRegistry,
CircuitBreakerRegistry circuitBreakerRegistry) {
this.timeLimiterRegistry = timeLimiterRegistry;
this.circuitBreakerRegistry = circuitBreakerRegistry;
}
public String callExternalApi(String request) {
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("externalService");
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("externalService");
Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> TimeLimiter.decorateFutureSupplier(
timeLimiter,
() -> CompletableFuture.supplyAsync(() -> executeExternalCall(request))
).get()
);
try {
return decoratedSupplier.get();
} catch (TimeoutException e) {
logger.warn("External service call timed out for request: {}", request);
throw new ServiceTimeoutException("External service timeout", e);
} catch (Exception e) {
logger.error("External service call failed for request: {}", request, e);
throw new ServiceException("External service error", e);
}
}
private String executeExternalCall(String request) {
// Simulate external API call
try {
// Simulate variable response time
long delay = ThreadLocalRandom.current().nextLong(1000, 8000);
Thread.sleep(delay);
return "Response for: " + request;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Operation interrupted", e);
}
}
}
4. Async Service with TimeLimiter
// AsyncExternalService.java
@Service
public class AsyncExternalService {
private final TimeLimiter timeLimiter;
private final ExecutorService executorService;
public AsyncExternalService(TimeLimiterRegistry timeLimiterRegistry) {
this.timeLimiter = timeLimiterRegistry.timeLimiter("externalService");
this.executorService = Executors.newFixedThreadPool(10);
}
public CompletableFuture<String> callExternalApiAsync(String request) {
return timeLimiter.executeCompletionStage(
executorService,
() -> CompletableFuture.supplyAsync(() -> executeExternalCall(request), executorService)
).toCompletableFuture();
}
public CompletableFuture<String> callWithFallback(String request) {
return timeLimiter.executeCompletionStage(
executorService,
() -> CompletableFuture.supplyAsync(() -> executeExternalCall(request), executorService)
).toCompletableFuture()
.exceptionally(throwable -> {
if (throwable instanceof TimeoutException) {
return "Fallback response for: " + request;
}
throw new CompletionException(throwable);
});
}
private String executeExternalCall(String request) {
try {
// Simulate external API call
long delay = ThreadLocalRandom.current().nextLong(1000, 8000);
Thread.sleep(delay);
return "Response for: " + request;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Operation interrupted", e);
}
}
@PreDestroy
public void cleanup() {
executorService.shutdown();
}
}
5. Spring AOP with TimeLimiter
// TimeLimiterAspect.java
@Aspect
@Component
public class TimeLimiterAspect {
private final TimeLimiterRegistry timeLimiterRegistry;
public TimeLimiterAspect(TimeLimiterRegistry timeLimiterRegistry) {
this.timeLimiterRegistry = timeLimiterRegistry;
}
@Around("@annotation(timeLimited)")
public Object timeLimitMethod(ProceedingJoinPoint joinPoint, TimeLimited timeLimited) throws Throwable {
String instanceName = timeLimited.value();
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter(instanceName);
Supplier<Object> supplier = () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
try {
return timeLimiter.executeSupplier(supplier);
} catch (TimeoutException e) {
throw new ServiceTimeoutException(
String.format("Method %s timed out after %s",
joinPoint.getSignature().getName(),
timeLimiter.getTimeLimiterConfig().getTimeoutDuration()
), e
);
}
}
}
// TimeLimited.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeLimited {
String value() default "default";
String fallbackMethod() default "";
}
Guava TimeLimiter
1. Guava SimpleTimeLimiter
// GuavaTimeLimiterService.java
@Service
public class GuavaTimeLimiterService {
private final SimpleTimeLimiter timeLimiter;
private final ExecutorService executorService;
public GuavaTimeLimiterService() {
this.executorService = Executors.newFixedThreadPool(10);
this.timeLimiter = SimpleTimeLimiter.create(executorService);
}
public String executeWithTimeout(Callable<String> task, Duration timeout)
throws TimeoutException, InterruptedException, ExecutionException {
return timeLimiter.callWithTimeout(task, timeout.toMillis(), TimeUnit.MILLISECONDS);
}
public <T> T callWithTimeout(Callable<T> task, Duration timeout, T fallbackValue) {
try {
return timeLimiter.callWithTimeout(task, timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.warn("Operation timed out, using fallback value");
return fallbackValue;
} catch (Exception e) {
logger.error("Operation failed", e);
throw new ServiceException("Operation failed", e);
}
}
public void runWithTimeout(Runnable task, Duration timeout) {
try {
timeLimiter.runWithTimeout(task, timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.warn("Operation timed out");
throw new ServiceTimeoutException("Operation timeout", e);
} catch (Exception e) {
logger.error("Operation failed", e);
throw new ServiceException("Operation failed", e);
}
}
@PreDestroy
public void cleanup() {
executorService.shutdown();
}
}
2. Guava TimeLimiter with Future
// FutureTimeLimiterService.java
@Service
public class FutureTimeLimiterService {
private final ExecutorService executorService;
private final SimpleTimeLimiter timeLimiter;
public FutureTimeLimiterService() {
this.executorService = Executors.newCachedThreadPool();
this.timeLimiter = SimpleTimeLimiter.create(executorService);
}
public CompletableFuture<String> executeWithFutureTimeout(String request, Duration timeout) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
return processRequest(request);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Operation interrupted", e);
}
}, executorService);
return future.orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.exceptionally(throwable -> {
if (throwable instanceof TimeoutException) {
return "Timeout fallback for: " + request;
}
throw new CompletionException(throwable);
});
}
public <T> CompletableFuture<T> executeWithCustomTimeout(
Callable<T> task,
Duration timeout,
T fallbackValue) {
return CompletableFuture.supplyAsync(() -> {
try {
return timeLimiter.callWithTimeout(task, timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.warn("Task timed out, returning fallback value");
return fallbackValue;
} catch (Exception e) {
throw new ServiceException("Task execution failed", e);
}
}, executorService);
}
private String processRequest(String request) throws InterruptedException {
// Simulate processing
long processingTime = ThreadLocalRandom.current().nextLong(500, 3000);
Thread.sleep(processingTime);
return "Processed: " + request;
}
@PreDestroy
public void cleanup() {
executorService.shutdown();
}
}
Spring @Timeout Annotation
1. Spring @Timeout Configuration
// SpringTimeoutService.java
@Service
public class SpringTimeoutService {
private static final Logger logger = LoggerFactory.getLogger(SpringTimeoutService.class);
@Async("taskExecutor")
@Timeout(duration = "5s")
public CompletableFuture<String> processWithTimeout(String request) {
return CompletableFuture.supplyAsync(() -> {
try {
// Simulate work
long workTime = ThreadLocalRandom.current().nextLong(1000, 10000);
logger.info("Processing request: {} for {} ms", request, workTime);
Thread.sleep(workTime);
return "Completed: " + request;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Processing interrupted", e);
}
});
}
@Async
@Timeout(duration = "2s")
public CompletableFuture<String> quickProcess(String request) {
return CompletableFuture.completedFuture("Quick: " + request);
}
}
// AsyncConfig.java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("timeout-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
2. Spring @Timeout with Fallback
// TimeoutWithFallbackService.java
@Service
public class TimeoutWithFallbackService {
@Async
@Timeout(duration = "3s")
public CompletableFuture<String> fetchDataWithFallback(String id) {
return CompletableFuture.supplyAsync(() -> fetchData(id))
.exceptionally(throwable -> {
if (throwable.getCause() instanceof TimeoutException) {
return "Fallback data for: " + id;
}
throw new CompletionException(throwable);
});
}
@Async
@Timeout(duration = "2s")
public CompletableFuture<Optional<String>> fetchOptionalData(String id) {
return CompletableFuture.supplyAsync(() -> {
try {
String data = fetchData(id);
return Optional.of(data);
} catch (TimeoutException e) {
return Optional.empty();
}
});
}
private String fetchData(String id) {
try {
// Simulate data fetching
long fetchTime = ThreadLocalRandom.current().nextLong(1000, 5000);
Thread.sleep(fetchTime);
if (fetchTime > 3000) {
throw new TimeoutException("Fetch operation timed out");
}
return "Data for: " + id;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Fetch interrupted", e);
}
}
}
Custom TimeLimiter Implementation
1. Custom TimeLimiter
// CustomTimeLimiter.java
@Component
public class CustomTimeLimiter {
private final ExecutorService executorService;
public CustomTimeLimiter() {
this.executorService = Executors.newCachedThreadPool();
}
public <T> T executeWithTimeout(Callable<T> callable, Duration timeout)
throws TimeoutException, Exception {
Future<T> future = executorService.submit(callable);
try {
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw e;
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
throw (Exception) cause;
} else {
throw new Exception(cause);
}
}
}
public <T> Optional<T> executeWithTimeoutAndFallback(
Callable<T> callable,
Duration timeout,
T fallbackValue) {
try {
return Optional.of(executeWithTimeout(callable, timeout));
} catch (TimeoutException e) {
logger.warn("Operation timed out after {}", timeout);
return Optional.ofNullable(fallbackValue);
} catch (Exception e) {
logger.error("Operation failed", e);
return Optional.empty();
}
}
public void executeRunnableWithTimeout(Runnable runnable, Duration timeout) {
Future<?> future = executorService.submit(runnable);
try {
future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new ServiceTimeoutException("Operation timed out", e);
} catch (Exception e) {
throw new ServiceException("Operation failed", e);
}
}
@PreDestroy
public void cleanup() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
2. Advanced Custom TimeLimiter
// AdvancedTimeLimiter.java
@Component
public class AdvancedTimeLimiter {
private final ScheduledExecutorService scheduler;
private final ExecutorService workerExecutor;
public AdvancedTimeLimiter() {
this.scheduler = Executors.newScheduledThreadPool(5);
this.workerExecutor = Executors.newCachedThreadPool();
}
public <T> CompletableFuture<T> executeWithTimeout(
Callable<T> task,
Duration timeout,
T timeoutResult) {
CompletableFuture<T> future = CompletableFuture.supplyAsync(() -> {
try {
return task.call();
} catch (Exception e) {
throw new CompletionException(e);
}
}, workerExecutor);
// Schedule timeout
ScheduledFuture<?> timeoutFuture = scheduler.schedule(() -> {
if (!future.isDone()) {
future.complete(timeoutResult);
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
// Cancel timeout if task completes normally
future.whenComplete((result, throwable) -> {
if (!timeoutFuture.isDone()) {
timeoutFuture.cancel(false);
}
});
return future;
}
public <T> CompletableFuture<T> executeWithDynamicTimeout(
Callable<T> task,
Supplier<Duration> timeoutSupplier,
Function<Throwable, T> exceptionHandler) {
Duration timeout = timeoutSupplier.get();
CompletableFuture<T> future = new CompletableFuture<>();
// Submit task
Future<T> taskFuture = workerExecutor.submit(task);
// Schedule timeout
ScheduledFuture<?> timeoutFuture = scheduler.schedule(() -> {
if (!taskFuture.isDone()) {
taskFuture.cancel(true);
future.completeExceptionally(new TimeoutException("Operation timed out after " + timeout));
}
}, timeout.toMillis(), TimeUnit.MILLISECONDS);
// Handle completion
workerExecutor.submit(() -> {
try {
T result = taskFuture.get();
if (!timeoutFuture.isDone()) {
timeoutFuture.cancel(false);
}
future.complete(result);
} catch (CancellationException e) {
// Task was cancelled due to timeout, ignore
} catch (Exception e) {
if (!timeoutFuture.isDone()) {
timeoutFuture.cancel(false);
}
T fallbackResult = exceptionHandler.apply(e);
future.complete(fallbackResult);
}
});
return future;
}
public <T> T executeWithRetryAndTimeout(
Callable<T> task,
Duration timeout,
int maxRetries,
Duration retryDelay) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return executeWithTimeout(task, timeout, null);
} catch (Exception e) {
if (attempt == maxRetries) {
throw new ServiceException("Operation failed after " + maxRetries + " attempts", e);
}
logger.warn("Attempt {} failed, retrying in {} ms", attempt, retryDelay.toMillis());
try {
Thread.sleep(retryDelay.toMillis());
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new ServiceException("Operation interrupted", ie);
}
}
}
throw new ServiceException("Operation failed after " + maxRetries + " attempts");
}
@PreDestroy
public void cleanup() {
scheduler.shutdown();
workerExecutor.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
if (!workerExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
workerExecutor.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
workerExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Async Timeout Control
1. CompletableFuture Timeout Patterns
// CompletableFutureTimeoutService.java
@Service
public class CompletableFutureTimeoutService {
private final ScheduledExecutorService timeoutExecutor;
public CompletableFutureTimeoutService() {
this.timeoutExecutor = Executors.newScheduledThreadPool(5);
}
public <T> CompletableFuture<T> withTimeout(
CompletableFuture<T> future,
Duration timeout,
T timeoutValue) {
return future.applyToEither(
timeoutAfter(timeout, timeoutValue),
Function.identity()
);
}
public <T> CompletableFuture<T> withTimeoutException(
CompletableFuture<T> future,
Duration timeout) {
return future.applyToEither(
timeoutAfterWithException(timeout),
Function.identity()
).exceptionally(throwable -> {
if (throwable instanceof TimeoutException) {
throw new ServiceTimeoutException("Operation timed out", throwable);
}
throw new CompletionException(throwable);
});
}
private <T> CompletableFuture<T> timeoutAfter(Duration duration, T timeoutValue) {
CompletableFuture<T> result = new CompletableFuture<>();
timeoutExecutor.schedule(() -> result.complete(timeoutValue),
duration.toMillis(), TimeUnit.MILLISECONDS);
return result;
}
private <T> CompletableFuture<T> timeoutAfterWithException(Duration duration) {
CompletableFuture<T> result = new CompletableFuture<>();
timeoutExecutor.schedule(() ->
result.completeExceptionally(new TimeoutException("Timeout after " + duration)),
duration.toMillis(), TimeUnit.MILLISECONDS);
return result;
}
public <T> CompletableFuture<T> executeWithDeadline(
Supplier<T> supplier,
Duration deadline) {
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier);
return withTimeoutException(future, deadline);
}
@PreDestroy
public void cleanup() {
timeoutExecutor.shutdown();
}
}
2. Reactive Timeout with Project Reactor
// ReactiveTimeoutService.java
@Service
public class ReactiveTimeoutService {
public Mono<String> executeWithTimeout(String request, Duration timeout) {
return Mono.fromCallable(() -> processRequest(request))
.timeout(timeout)
.onErrorResume(TimeoutException.class,
e -> Mono.just("Fallback for: " + request))
.doOnError(throwable ->
logger.error("Operation failed for request: {}", request, throwable));
}
public Flux<String> executeMultipleWithTimeout(List<String> requests, Duration timeout) {
return Flux.fromIterable(requests)
.flatMap(request ->
Mono.fromCallable(() -> processRequest(request))
.timeout(timeout)
.onErrorResume(TimeoutException.class,
e -> Mono.just("Timeout for: " + request))
)
.doOnNext(result -> logger.info("Processed: {}", result));
}
public Mono<String> executeWithRetryAndTimeout(String request, Duration timeout, int maxRetries) {
return Mono.fromCallable(() -> processRequest(request))
.timeout(timeout)
.retryWhen(Retry.fixedDelay(maxRetries, Duration.ofSeconds(1))
.onErrorResume(throwable ->
Mono.just("Final fallback for: " + request));
}
private String processRequest(String request) {
try {
// Simulate processing
long processTime = ThreadLocalRandom.current().nextLong(500, 4000);
Thread.sleep(processTime);
return "Processed: " + request;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ServiceException("Processing interrupted", e);
}
}
}
Testing Timeout Scenarios
1. Unit Tests for TimeLimiter
// TimeLimiterTest.java
@ExtendWith(SpringExtension.class)
@SpringBootTest
class TimeLimiterTest {
@Autowired
private TimeLimiterRegistry timeLimiterRegistry;
@Autowired
private ExternalService externalService;
@Test
void testTimeLimiterWithTimeout() {
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("fast-service");
Supplier<String> slowSupplier = () -> {
try {
Thread.sleep(3000);
return "Slow result";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
};
assertThatThrownBy(() -> timeLimiter.executeSupplier(slowSupplier))
.isInstanceOf(TimeoutException.class);
}
@Test
void testTimeLimiterWithinTimeout() {
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("slow-service");
Supplier<String> fastSupplier = () -> {
try {
Thread.sleep(1000);
return "Fast result";
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
};
String result = timeLimiter.executeSupplier(fastSupplier);
assertThat(result).isEqualTo("Fast result");
}
@Test
void testExternalServiceTimeout() {
String request = "test-request";
assertThatThrownBy(() -> externalService.callExternalApi(request))
.isInstanceOf(ServiceTimeoutException.class)
.hasMessageContaining("External service timeout");
}
@Test
void testTimeLimiterMetrics() throws Exception {
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("externalService");
TimeLimiter.Metrics metrics = timeLimiter.getMetrics();
// Execute some calls
for (int i = 0; i < 5; i++) {
try {
externalService.callExternalApi("request-" + i);
} catch (ServiceTimeoutException e) {
// Expected for some calls
}
}
assertThat(metrics.getNumberOfSuccessfulCalls()).isGreaterThan(0);
}
}
2. Integration Tests
// TimeLimiterIntegrationTest.java
@SpringBootTest
@TestPropertySource(properties = {
"resilience4j.timelimiter.instances.externalService.timeout-duration=1s"
})
class TimeLimiterIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ExternalService externalService;
@MockBean
private SomeDependency someDependency;
@Test
void testTimeoutIntegration() {
// Given
given(someDependency.slowMethod()).willAnswer(invocation -> {
Thread.sleep(5000); // Simulate slow operation
return "slow-response";
});
// When & Then
assertThatThrownBy(() -> externalService.callExternalApi("test"))
.isInstanceOf(ServiceTimeoutException.class);
}
@Test
void testControllerTimeout() {
// Given - simulate slow response
given(someDependency.slowMethod()).willAnswer(invocation -> {
Thread.sleep(5000);
return "slow-response";
});
// When & Then
assertThatThrownBy(() ->
restTemplate.getForObject("/api/external/test", String.class))
.isInstanceOf(HttpServerErrorException.class);
}
}
3. Performance Tests
// TimeLimiterPerformanceTest.java
@SpringBootTest
class TimeLimiterPerformanceTest {
@Autowired
private ExternalService externalService;
@Test
void testPerformanceUnderLoad() throws InterruptedException {
int numberOfThreads = 10;
int callsPerThread = 100;
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
CountDownLatch latch = new CountDownLatch(numberOfThreads * callsPerThread);
AtomicInteger successCount = new AtomicInteger();
AtomicInteger timeoutCount = new AtomicInteger();
AtomicInteger errorCount = new AtomicInteger();
long startTime = System.currentTimeMillis();
for (int i = 0; i < numberOfThreads; i++) {
executor.submit(() -> {
for (int j = 0; j < callsPerThread; j++) {
try {
String result = externalService.callExternalApi("request-" + j);
successCount.incrementAndGet();
} catch (ServiceTimeoutException e) {
timeoutCount.incrementAndGet();
} catch (Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
}
});
}
latch.await(2, TimeUnit.MINUTES);
long endTime = System.currentTimeMillis();
executor.shutdown();
System.out.printf("""
Performance Test Results:
Total Calls: %d
Successful: %d
Timeouts: %d
Errors: %d
Total Time: %d ms
Throughput: %.2f calls/second
""",
numberOfThreads * callsPerThread,
successCount.get(),
timeoutCount.get(),
errorCount.get(),
endTime - startTime,
(numberOfThreads * callsPerThread * 1000.0) / (endTime - startTime)
);
assertThat(successCount.get()).isGreaterThan(0);
assertThat(timeoutCount.get() + successCount.get() + errorCount.get())
.isEqualTo(numberOfThreads * callsPerThread);
}
}
Best Practices and Patterns
1. Configuration Management
// TimeLimiterConfig.java
@Configuration
public class TimeLimiterConfig {
@Bean
public TimeLimiterRegistry timeLimiterRegistry() {
TimeLimiterConfig defaultConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(3))
.cancelRunningFuture(true)
.build();
TimeLimiterConfig slowServiceConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(10))
.cancelRunningFuture(true)
.build();
TimeLimiterConfig fastServiceConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(1))
.cancelRunningFuture(false)
.build();
return TimeLimiterRegistry.of(Map.of(
"default", defaultConfig,
"slowService", slowServiceConfig,
"fastService", fastServiceConfig
));
}
@Bean
public TimeLimiterAspect timeLimiterAspect(TimeLimiterRegistry timeLimiterRegistry) {
return new TimeLimiterAspect(timeLimiterRegistry);
}
}
2. Global Exception Handling
// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ServiceTimeoutException.class)
public ResponseEntity<ErrorResponse> handleTimeoutException(ServiceTimeoutException ex) {
logger.warn("Service timeout occurred: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
"TIMEOUT_ERROR",
"Service timeout occurred",
HttpStatus.REQUEST_TIMEOUT.value()
);
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
}
@ExceptionHandler(TimeoutException.class)
public ResponseEntity<ErrorResponse> handleGenericTimeoutException(TimeoutException ex) {
logger.warn("Generic timeout occurred: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
"TIMEOUT_ERROR",
"Operation timed out",
HttpStatus.REQUEST_TIMEOUT.value()
);
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body(errorResponse);
}
}
// ErrorResponse.java
public class ErrorResponse {
private final String code;
private final String message;
private final int status;
private final Instant timestamp;
private final String path;
public ErrorResponse(String code, String message, int status) {
this.code = code;
this.message = message;
this.status = status;
this.timestamp = Instant.now();
this.path = getCurrentRequestPath();
}
private String getCurrentRequestPath() {
try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return request.getRequestURI();
}
} catch (Exception e) {
// Ignore
}
return "unknown";
}
// Getters
public String getCode() { return code; }
public String getMessage() { return message; }
public int getStatus() { return status; }
public Instant getTimestamp() { return timestamp; }
public String getPath() { return path; }
}
3. Monitoring and Metrics
// TimeLimiterMetrics.java
@Component
public class TimeLimiterMetrics {
private final MeterRegistry meterRegistry;
private final TimeLimiterRegistry timeLimiterRegistry;
public TimeLimiterMetrics(MeterRegistry meterRegistry, TimeLimiterRegistry timeLimiterRegistry) {
this.meterRegistry = meterRegistry;
this.timeLimiterRegistry = timeLimiterRegistry;
bindMetrics();
}
private void bindMetrics() {
timeLimiterRegistry.getAllTimeLimiters().forEach((name, timeLimiter) -> {
// Track successful calls
Counter.builder("resilience4j.timelimiter.calls")
.tag("name", name)
.tag("result", "successful")
.register(meterRegistry);
// Track timeout calls
Counter.builder("resilience4j.timelimiter.calls")
.tag("name", name)
.tag("result", "timeout")
.register(meterRegistry);
// Track configuration
Gauge.builder("resilience4j.timelimiter.timeout.duration")
.tag("name", name)
.register(meterRegistry, timeLimiter,
t -> t.getTimeLimiterConfig().getTimeoutDuration().toMillis());
});
}
public void recordCall(String timeLimiterName, boolean success) {
Counter.builder("resilience4j.timelimiter.calls")
.tag("name", timeLimiterName)
.tag("result", success ? "successful" : "timeout")
.register(meterRegistry)
.increment();
}
}
4. Best Practices Summary
// TimeLimiterBestPractices.java
@Component
public class TimeLimiterBestPractices {
/**
* 1. Always use appropriate timeout durations
* 2. Consider the business context when setting timeouts
* 3. Use fallback mechanisms for timeouts
* 4. Monitor timeout rates and adjust configurations
* 5. Test timeout scenarios thoroughly
* 6. Use meaningful names for TimeLimiter instances
* 7. Combine with other resilience patterns (Circuit Breaker, Retry)
* 8. Clean up resources properly
* 9. Log timeout occurrences for debugging
* 10. Consider the impact of timeouts on user experience
*/
public void demonstrateBestPractices() {
// Example of good practices
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(5)) // Reasonable timeout
.cancelRunningFuture(true) // Prevent resource leaks
.build();
// Use with circuit breaker
TimeLimiter timeLimiter = TimeLimiter.of("external-service", config);
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("external-service");
Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier(
circuitBreaker,
TimeLimiter.decorateSupplier(timeLimiter, this::externalCall)
);
try {
String result = decoratedSupplier.get();
// Process result
} catch (TimeoutException e) {
// Provide fallback
handleTimeoutFallback();
} catch (Exception e) {
// Handle other exceptions
handleOtherExceptions(e);
}
}
private String externalCall() {
// External service call
return "result";
}
private void handleTimeoutFallback() {
// Implement meaningful fallback
}
private void handleOtherExceptions(Exception e) {
// Handle other types of exceptions
}
}
Conclusion
TimeLimiter is an essential tool for building resilient applications that need to enforce execution timeouts. Key takeaways:
- Choose the right implementation based on your needs (Resilience4j, Guava, Spring, or custom)
- Configure appropriate timeouts based on business requirements and SLAs
- Always provide fallback mechanisms for timeout scenarios
- Monitor and adjust timeout configurations based on real-world performance
- Combine with other resilience patterns like Circuit Breaker and Retry
- Test thoroughly to ensure timeout behavior works as expected
- Clean up resources to prevent memory leaks and resource exhaustion
By implementing proper timeout control, you can build more robust, responsive, and reliable applications that gracefully handle slow or unresponsive dependencies.