Spring Boot Resilience4j: Retry, Bulkhead, Timeout Patterns | SC Guide

Spring Boot Application වලට ඔරොත්තු දීමේ හැකියාව (Resilience) එකතු කරමු: Resilience4j එක්ක SC Guide
ආයුබෝවන් කට්ටියට! කොහොමද ඉතින්? අද අපි කතා කරන්න යන්නේ මේ දවස් වල ගොඩක් කතාබහට ලක්වෙන, ඒ වගේම අපේ Spring Boot applications වලට අතිශයින්ම වැදගත් වෙන මාතෘකාවක් ගැන – ඒ තමයි Resilience නැත්නම් ඔරොත්තු දීමේ හැකියාව. විශේෂයෙන්ම, Resilience4j කියන Library එක පාවිච්චි කරලා කොහොමද අපේ applications වලට Retry, Bulkhead, සහ Timeout patterns එකතු කරන්නේ කියලා.
දැන් හිතන්නකෝ, ඔයාලා හදන application එක Microservices architecture එකක් පාවිච්චි කරලා හදපුවා කියලා. එතකොට, එක service එකක් තව service එකක් එක්ක communicate කරනවනේ. සමහර වෙලාවට, මේ services අතරේ communication එක හරියට වෙන්නේ නැති වෙන්න පුළුවන්. ඒ කියන්නේ, external service එක down වෙන්න පුළුවන්, network එක slow වෙන්න පුළුවන්, නැත්නම් response වෙලාවට එන්නේ නැති වෙන්න පුළුවන්. මේ වගේ අවස්ථාවලදී අපේ application එක කඩා වැටෙන්නේ නැතුව, හොඳ user experience එකක් දෙන්න නම්, අපි Resilience ගැන හිතන්නම ඕනේ.
ඇයි අපිට Resilience ඕනේ?
Distributed systems වලදී, errors කියන්නේ සාමාන්ය දෙයක්. Network failures, service unavailability, slow responses මේ හැමදේම අපේ application එකේ performance එකට බලපාන්න පුළුවන්. Application එකක් fail වුනොත් user ලාට ලොකු අපහසුතාවයක් වෙන්න පුළුවන්. උදාහරණයක් විදියට, bank application එකක payment එකක් fail වුනොත් ඒක ලොකු ප්රශ්නයක්. ඉතින්, මේ වගේ අවස්ථාවලදී අපේ application එක gracefully handle කරන්න පුළුවන් වෙන්න ඕනේ. ඒකට තමයි Resilience patterns පාවිච්චි කරන්නේ.
- වැඩිදියුණු කළ ස්ථායීතාවය (Improved Stability): System එකේ කොටසක් fail වුණත්, සම්පූර්ණ system එකම fail නොවී maintain වෙන්න උදව් වෙනවා.
- වැඩිදියුණු කළ පරිශීලක අත්දැකීම් (Better User Experience): Errors ඇති වුණත් user ට ඒක දැනෙන්නේ නැති වෙන්න හරි, අවම වශයෙන් හොඳ error message එකක් හරි දෙන්න පුළුවන්.
- ස්වයංක්රීයව යථා තත්ත්වයට පත්වීම (Automatic Recovery): සමහර වෙලාවට errors ස්වයංක්රීයව නිවැරදි කරගන්න පුළුවන්.
Resilience4j කියන්නේ මොකක්ද?
Resilience4j කියන්නේ Netflix Hystrix වලට හොඳ විකල්පයක් විදියට ආපු, lightweight, easy-to-use fault tolerance library එකක්. Circuit Breaker, Rate Limiter, Bulkhead, Retry, Time Limiter, Cache, සහ Fallback වගේ functional programming patterns ගොඩක් මේකෙන් support කරනවා. Spring Boot එක්ක මේක integration කරගන්න එකත් හරිම ලේසියි.
අපිට අවශ්ය dependencies:
pom.xml (Maven) එකට මේ ටික add කරගමු:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version> <!-- Latest stable version එක බලලා දාගන්න -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Retry Pattern: හිතුවට වඩා ලේසියි!
හිතන්නකෝ, ඔයා external service එකකට call එකක් දෙනවා. ඒ call එක fail වුණා. ඒත්, ඒක සමහරවිට temporary error එකක් වෙන්න පුළුවන්. Network glitch එකක් හරි, service එක momentary down වෙලා හරි වෙන්න පුළුවන්. Retry pattern එකෙන් කරන්නේ, මේ වගේ අවස්ථාවකදී call එක ආයෙත් automatic attempt කරන එක.
Resilience4j එක්ක Retry Implement කරමු:
මුලින්ම, `application.yml` එකට retry configuration එක add කරගමු:
resilience4j.retry:
instances:
myExternalServiceRetry:
maxAttempts: 3 # උපරිම retry වාර ගණන
waitDuration: 1s # retry අතර කාලය
retryExceptions:
- org.springframework.web.client.ResourceAccessException # මේ exceptions ආවොත් විතරක් retry කරන්න
- java.io.IOException
# ignoreExceptions: # මේ exceptions ආවොත් retry නොකර ඉන්න
# - com.example.MyCustomBusinessException
දැන්, අපේ service එකට `Retry` annotation එක add කරමු:
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class ExternalService {
private final RestTemplate restTemplate;
public ExternalService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Retry(name = "myExternalServiceRetry", fallbackMethod = "getFallbackData")
public String fetchDataFromExternalService() {
System.out.println("Calling external service...");
// External service එකට call කරනවා
// මේක fail වෙන්න පුළුවන්
return restTemplate.getForObject("http://localhost:8081/external-api/data", String.class);
}
private String getFallbackData(Throwable t) {
System.out.println("Fallback method activated for Retry: " + t.getMessage());
return "Fallback data after retries failed. Error: " + t.getMessage();
}
}
මේ උදාහරණයේදී, `fetchDataFromExternalService()` method එක `myExternalServiceRetry` කියලා define කරපු Retry configuration එකෙන් protect වෙනවා. ඒ කියන්නේ, external service call එක fail වුනොත්, `maxAttempts` (3) වාරයක්, `waitDuration` (1s) කාලයක් අතර retry කරනවා. මේ හැමදේම fail වුනොත්, `getFallbackData` method එක execute වෙනවා.
Bulkhead Pattern: Traffic Jam එකක් වගේ!
Bulkhead pattern එක කියන්නේ distributed systems වල තියෙන ගොඩක් වැදගත් concept එකක්. හිතන්නකෝ, cruise ship එකක තියෙන compartments වගේ. එක compartment එකකට වතුර ගියත්, අනිත් compartments වලට ඒක බලපාන්නේ නැහැ. ඒ වගේම, application එකක එක service එකකට එන requests ගොඩක් වැඩි වුණොත්, ඒකෙන් සම්පූර්ණ application එකම slow නොවී ඉතුරු service වලට සාමාන්ය විදියට වැඩ කරන්න පුළුවන්.
Resilience4j වල Bulkhead patterns දෙකක් තියෙනවා:
- Semaphore Bulkhead: මේකෙන් එකපාර execute වෙන්න පුළුවන් concurrent calls ගණන සීමා කරනවා.
- ThreadPool Bulkhead: මේකෙන් dedicated thread pool එකක් පාවිච්චි කරනවා.
Semaphore Bulkhead Implement කරමු:
`application.yml` එකට bulkhead configuration එක add කරගමු:
resilience4j.bulkhead:
instances:
myBulkhead:
maxConcurrentCalls: 5 # එකවර execute වෙන්න පුළුවන් උපරිම calls ගණන
maxWaitDuration: 100ms # call එකක් bulkhead එක free වෙනකන් බලන් ඉන්න පුළුවන් උපරිම කාලය
දැන්, අපේ service එකට `Bulkhead` annotation එක add කරමු:
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
@Service
public class AnotherExternalService {
@Bulkhead(name = "myBulkhead", fallbackMethod = "getFallbackBulkheadData")
public String processDataWithBulkhead() throws InterruptedException {
System.out.println("Processing data with Bulkhead protection...");
// Simulate a long-running operation
Thread.sleep(500); // 0.5 second delay
return "Data processed with Bulkhead protection.";
}
private String getFallbackBulkheadData(Throwable t) {
System.out.println("Fallback method activated for Bulkhead: " + t.getMessage());
return "Fallback data: Too many concurrent requests.";
}
}
මේකෙන් වෙන්නේ `processDataWithBulkhead()` method එකට එකවර එන්න පුළුවන් requests ගණන `maxConcurrentCalls` (5) වලට සීමා කරන එක. මේ සීමාව පැන්නොත්, `maxWaitDuration` එක ඇතුළත call එක block වෙලා ඉඳලා, ඒ කාලයත් ඉවර වුනොත් `BulkheadFullException` එකක් throw වෙනවා, ඒ වගේම `getFallbackBulkheadData` method එක activate වෙනවා.
Timeout Pattern: වෙලාවට වැඩේ වෙන්න ඕනේ!
Timeout pattern එකෙන් කරන්නේ, operation එකක් execute වෙන්න ගතවෙන උපරිම කාලය සීමා කරන එක. මේක විශේෂයෙන්ම external calls වලදී වැදගත්. External service එකක් slow වුනොත්, අපේ application එකත් ඒකට අහුවෙලා block නොවී ඉන්න මේක උදව් වෙනවා.
Resilience4j එක්ක Timeout Implement කරමු:
`application.yml` එකට timeout configuration එක add කරගමු:
resilience4j.timelimiter:
instances:
myExternalServiceTimeout:
timeoutDuration: 5s # උපරිම timeout කාලය
cancelRunningFuture: true # running future එක cancel කරන්නද කියලා
දැන්, අපේ service එකට `TimeLimiter` annotation එක add කරමු. මේක Asynchronous operations වලදී ගොඩක් ප්රයෝජනවත්.
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
@Service
public class AnotherServiceWithTimeout {
@TimeLimiter(name = "myExternalServiceTimeout", fallbackMethod = "getFallbackTimeoutData")
public CompletableFuture<String> callExternalServiceWithTimeout() {
System.out.println("Calling external service with timeout protection...");
// Simulate an asynchronous call that might take long
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(7000); // Simulate a 7-second operation (will exceed 5s timeout)
return "Data from external service (after long wait).";
} catch (InterruptedException e) {
System.out.println("External service call interrupted!");
throw new RuntimeException("External service call interrupted!", e);
}
}, Executors.newCachedThreadPool()); // A separate thread pool for async operations
}
private CompletableFuture<String> getFallbackTimeoutData(Throwable t) {
System.out.println("Fallback method activated for Timeout: " + t.getMessage());
return CompletableFuture.completedFuture("Fallback data: External service timed out.");
}
}
මෙහිදී, `callExternalServiceWithTimeout()` method එක `myExternalServiceTimeout` කියන configuration එකෙන් protect වෙනවා. ඒ කියන්නේ, method එක execute වෙන්න `timeoutDuration` (5s) වලට වඩා වැඩි කාලයක් ගියොත්, `TimeoutException` එකක් throw වෙලා `getFallbackTimeoutData` method එක execute වෙනවා.
External Call එකකට Resilience Patterns යොදමු (ප්රායෝගික උදාහරණයක්)
දැන් අපි බලමු, මේ patterns ටික එක external call එකකට කොහොමද apply කරන්නේ කියලා. අපි හිතමු, third-party API එකකින් product details ගන්නවා කියලා. මේ API එක සමහර වෙලාවට down වෙන්න පුළුවන්, slow වෙන්න පුළුවන්, නැත්නම් වැඩි requests වලට handle කරන්න බැරි වෙන්න පුළුවන්.
Spring Boot Controller එකක් සහ Service එකක් හදමු:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/details")
public String getProductDetails() {
try {
return productService.fetchProductDetails().join(); // .join() to wait for CompletableFuture result
} catch (Exception e) {
return "Error fetching product details: " + e.getMessage();
}
}
}
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
@Service
public class ProductService {
private final RestTemplate restTemplate;
public ProductService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// Resilience4j configurations defined in application.yml
// resilience4j.retry.instances.productServiceRetry
// resilience4j.bulkhead.instances.productServiceBulkhead
// resilience4j.timelimiter.instances.productServiceTimeLimiter
@Retry(name = "productServiceRetry", fallbackMethod = "getProductDetailsFallback")
@Bulkhead(name = "productServiceBulkhead", fallbackMethod = "getProductDetailsFallback")
@TimeLimiter(name = "productServiceTimeLimiter", fallbackMethod = "getProductDetailsFallbackCompletable")
public CompletableFuture<String> fetchProductDetails() {
System.out.println("Attempting to fetch product details from external API...");
return CompletableFuture.supplyAsync(() -> {
// Simulate external API call
String apiUrl = "http://localhost:8081/mock-product-api/details";
try {
// Simulate network issues / service unavailabilities
// For demonstration, you can make mock-product-api sometimes fail or delay
return restTemplate.getForObject(apiUrl, String.class);
} catch (ResourceAccessException e) {
System.err.println("Resource Access Exception during external call: " + e.getMessage());
throw e; // This will trigger retry/fallback
} catch (Exception e) {
System.err.println("General Exception during external call: " + e.getMessage());
throw new RuntimeException(e); // This will trigger retry/fallback
}
}, Executors.newCachedThreadPool()); // Use a separate thread pool for async execution
}
// Fallback for Retry and Bulkhead (Synchronous fallback). Note: This will be called if the CompletableFuture fails before completion.
// If TimeLimiter causes a timeout, the CompletableFuture will be cancelled and getProductDetailsFallbackCompletable will be invoked.
private String getProductDetailsFallback(Throwable t) {
System.out.println("Synchronous Fallback for Product Details. Error: " + t.getMessage());
return "Sorry, product details are currently unavailable due to: " + t.getClass().getSimpleName();
}
// Fallback for TimeLimiter (Asynchronous fallback)
private CompletableFuture<String> getProductDetailsFallbackCompletable(Throwable t) {
System.out.println("Asynchronous Fallback for Product Details (TimeLimiter). Error: " + t.getMessage());
return CompletableFuture.completedFuture("Sorry, product details timed out: " + t.getClass().getSimpleName());
}
}
`application.yml` configurations:
resilience4j:
retry:
instances:
productServiceRetry:
maxAttempts: 4
waitDuration: 2s
retryExceptions:
- org.springframework.web.client.ResourceAccessException
bulkhead:
instances:
productServiceBulkhead:
maxConcurrentCalls: 3
maxWaitDuration: 50ms
timelimiter:
instances:
productServiceTimeLimiter:
timeoutDuration: 3s
cancelRunningFuture: true
මේ උදාහරණයේදී, `fetchProductDetails()` method එක එකවර Retry, Bulkhead, සහ TimeLimiter කියන Patterns තුනෙන්ම protect වෙනවා. ඒ කියන්නේ:
- External API call එක fail වුනොත් (Network error වගේ), `productServiceRetry` configuration එක අනුව retry කරනවා.
- එකපාරට product details requests ගොඩක් ආවොත්, `productServiceBulkhead` එකෙන් calls 3කට සීමා කරනවා. ඊට වැඩි calls ටිකක් පොඩි වෙලාවක් (50ms) බලන් ඉඳලා block කරනවා.
- External API call එක `productServiceTimeLimiter` වල දීලා තියෙන 3s ඇතුළත response එකක් නොදුන්නොත්, Timeout වෙනවා.
- මේ හැම අවස්ථාවකදීම, අදාල Fallback method එක execute වෙනවා, ඒ නිසා user ට හිස් response එකක් වෙනුවට, meaningful message එකක් ලැබෙනවා.
මේක Run කරලා බලන්න නම්, `localhost:8081/mock-product-api/details` කියන URL එකේදී fail වෙන හරි, slow වෙන හරි mock API එකක් හදාගන්න පුළුවන්. ඒකට Spring Boot එකෙන්ම තව project එකක් හදලා simple controller එකක් ලියාගන්න පුළුවන්.
// Example Mock Product API Controller (in a separate Spring Boot project, running on port 8081)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@RestController
@RequestMapping("/mock-product-api")
public class MockProductApiController {
private final Random random = new Random();
private static int requestCount = 0; // Use static to keep count across requests
@GetMapping("/details")
public String getDetails() throws InterruptedException {
requestCount++;
if (requestCount % 3 == 0) { // Simulate failure every 3rd request
System.out.println("Mock API: Simulating failure for request #" + requestCount);
throw new RuntimeException("Mock API: Internal Server Error (simulated)");
}
if (requestCount % 5 == 0) { // Simulate slow response every 5th request
System.out.println("Mock API: Simulating slow response for request #" + requestCount);
Thread.sleep(4000); // Simulate 4-second delay, will cause timeout (if timeout is less than 4s)
return "Mock Product Data (slow response for #" + requestCount + ")";
}
System.out.println("Mock API: Sending successful response for request #" + requestCount);
return "Mock Product Data (success for #" + requestCount + ")";
}
}
මේ mock API එකෙන් සමහර වෙලාවට fail වෙන, සමහර වෙලාවට slow වෙන response දෙකක් හදනවා. මේක පාවිච්චි කරලා ඔයාලට Resilience4j Patterns වල වැඩ කරන විදිය හොඳටම test කරලා බලන්න පුළුවන්.
අවසාන වශයෙන්:
අද අපි කතා කළා Spring Boot applications වලට Resilience කියන එක කොච්චර වැදගත්ද කියලා, විශේෂයෙන්ම Microservices environments වලදී. ඒ වගේම, Resilience4j library එක පාවිච්චි කරලා Retry, Bulkhead, සහ Timeout patterns කොහොමද අපේ external calls වලට apply කරන්නේ කියලා ප්රායෝගික උදාහරණ එක්ක බැලුවා. මේ patterns පාවිච්චි කරන එකෙන් ඔයාලගේ applications වල reliability, availability, සහ stability ගොඩක් වැඩි කරගන්න පුළුවන්.
දැන් ඔයාලට පුළුවන් මේ concepts ඔයාලගේම projects වලට add කරලා test කරලා බලන්න. මොකද, theoretical knowledge එකට වඩා practical experience එක ගොඩක් වටිනවා. මේ ගැන මොනවා හරි ප්රශ්න තියෙනවා නම්, නැත්නම් තව මොනවා හරි දැනගන්න ඕනේ නම්, පහලින් comment එකක් දාන්න. අපි ඒ ගැන කතා කරමු. එහෙනම්, තවත් අලුත් දෙයක් අරගෙන ඉක්මනටම හම්බවෙමු! තෙරුවන් සරණයි!