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.