Kenal Lebih Dekat dengan Library Resilience4j di Java

Ichwan Sholihin
6 min readOct 13, 2023
Photo by Pedro Sanz on Unsplash

Halo semua, pada artikel kali ini saya akan membahas tentang sebuah library di Java yang cukup powerful untuk mengelola kesalahan pada aplikasi, yakni Resilience4j.

Dalam dunia pengembangan perangkat lunak, keandalan dan ketahanan aplikasi adalah aspek kunci yang harus dipertimbangkan dengan serius. Terutama dalam sistem yang melibatkan panggilan ke layanan eksternal, kegagalan dalam komunikasi dapat memiliki dampak besar pada pengalaman pengguna. Itulah sebabnya, pengembang sering mencari cara untuk membuat aplikasi mereka lebih tahan terhadap kesalahan. Salah satu alat yang sangat berguna dalam mencapai tujuan ini adalah Resilience4j, sebuah library Java yang dirancang khusus untuk meningkatkan ketahanan aplikasi.

Resilience4j adalah library yang menyediakan alat-alat untuk membangun sistem yang tahan terhadap kesalahan (resilient systems) di lingkungan Java. Dengan mengintegrasikan Resilience4j ke dalam aplikasi Anda, Anda dapat mengelola kesalahan, waktu timeout, dan toleransi kesalahan dengan lebih efektif. Dibandingkan dengan library lainnya, Resilience4j memiliki beberapa fitur yang membuatnya menonjol.

Mengapa Harus Menggunakan Resilience4j?

  1. Peningkatan Ketahanan Aplikasi: Dengan menggunakan Resilience4j, Anda dapat membangun aplikasi yang lebih tangguh dan dapat bertahan terhadap kesalahan dan beban tinggi.
  2. Peningkatan Pengalaman Pengguna: Dengan mengelola kesalahan dengan baik, aplikasi Anda akan memberikan pengalaman pengguna yang lebih baik dengan waktu respons yang lebih cepat dan lebih sedikit kesalahan yang terlihat.
  3. Pengurangan Downtime: Dengan mengatasi kesalahan dan membatasi dampaknya, Resilience4j membantu mengurangi waktu tidak aktif (downtime) aplikasi Anda.
  4. Kustomisasi Fleksibel: Resilience4j memungkinkan pengembang untuk mengkustomisasi konfigurasi dengan mudah sesuai dengan kebutuhan aplikasi mereka.

Implementasi Resilience4j di Java

Pada library resilience4j, terdapat 5 module yang bisa Anda gunakan terutama ketika aplikasi Anda bekerja menggunakan thread maupun multithread (Baca juga: Mengenal Class Executors untuk Management Threading serta Implementasinya di Java). Untuk menerapkan resilience4j di Java, Anda harus menginstal terlebih dahulu library resilience4j yang terdapat pada dokumentasi resminya https://resilience4j.readme.io/docs.

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-all</artifactId>
<version>2.0.0</version>
</dependency>

Untuk implementasi berikutnya, saya akan menggunakan beberapa library tambahan seperti lombok dan slf4j.

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>

(Baca juga: Implementasi Library Lombok untuk Efisiensi Kode di Java dan Implementasi Log History Menggunakan SLF4J di Java)

Pastikan juga project Anda sudah menerapkan library junit5, karena pada artikel ini saya akan mengimplementasikan kode nya pada unit test.Berikut adalah penjelasan singkat tentang beberapa modul utama di Resilience4j beserta implementasi dan contoh kasus penggunaannya:

1. Circuit Breaker

Circuit Breaker adalah mekanisme yang menghentikan panggilan ke layanan eksternal jika layanan tersebut mengalami kesalahan berulang. Jika jumlah kesalahan melewati ambang batas tertentu, Circuit Breaker akan membuka sirkuit, menghentikan panggilan, dan memberi waktu layanan untuk pulih sebelum membuka kembali sirkuit.

@Slf4j
public class CircuitBreakerTest {

void callMe(){
log.info("try call me");
throw new IllegalArgumentException("Error in callMe()");
}

@Test
void testCircuitBreaker() {

CircuitBreaker braker = CircuitBreaker.ofDefaults("ichwan");

for (int i = 0; i < 200; i++) {
try {
Runnable runnable = CircuitBreaker.decorateRunnable(braker, () -> callMe());
runnable.run();
} catch (Exception e){
//error : CircuitBreaker 'ichwan' is OPEN and does not permit further calls
log.error("error : {}", e.getMessage());
}
}
}
}

Method callMe() adalah contoh operasi yang mungkin gagal. Dalam contoh ini, method callMe() mencoba menjalankan sesuatu (dipresentasikan dengan pesan log "try call me") dan kemudian melempar throw IllegalArgumentException dengan pesan "Error in callMe()". Ini mensimulasikan operasi yang mungkin mengalami kesalahan atau kegagalan.

Method testCircuitBreaker() adalah method pengujian yang menggunakan Circuit Breaker. Pada awalnya, objek CircuitBreaker dibuat menggunakan konfigurasi default. Selanjutnya, ada loop yang mencoba menjalankan method callMe() sebanyak 200 kali. Dalam setiap iterasi, metode CircuitBreaker.decorateRunnable() digunakan untuk men-decorate method callMe(), membuatnya kompatibel dengan Circuit Breaker. Kemudian, method tersebut dijalankan dengan menggunakan runnable.run(). Jika terjadi kesalahan (dalam hal ini, karena callMe() melempar throw), CircuitBreaker akan membuka sirkuitnya, dan pesan kesalahan akan dicetak ke log dengan menggunakan log.error().

Kode ini mencoba mengilustrasikan cara menggunakan Circuit Breaker untuk melindungi aplikasi dari kesalahan beruntun yang dapat terjadi saat memanggil suatu operasi. Circuit Breaker membantu menghentikan panggilan ke operasi yang mungkin gagal secara berulang-ulang, memberikan aplikasi waktu untuk pulih sebelum mencoba panggilan lagi. Dengan demikian, menggunakan Circuit Breaker seperti ini dapat meningkatkan ketahanan dan keandalan aplikasi terhadap kesalahan. Hasilnya, ketika method testCircuitBreaker() dijalankan, 100 thread pertama berhasil menjalankan method callMe() dan 100 berikutnya akan terjadi error ketika Circuit breaker mengakses method non-return value tersebut. Karena default dari thread yang bisa dijalankan pada circuit breaker adalah 100 thread. Anda dapat mengubah default dari setiap module yg ada di resilience4j ini dengan menggunakan config.

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.failureRateThreshold(10f)
.slidingWindowSize(10)
.minimumNumberOfCalls(10)
.build();

Setiap module memiliki methodnya masing-masing untuk mengkostumisasi eksekusi thread, Anda dapat mempelajarinya lebih lanjut pada dokumentasi resmi library resilience4j https://resilience4j.readme.io/docs.

Contoh Kasus: Misalnya, dalam aplikasi e-commerce, jika layanan pembayaran mengalami kesalahan terus-menerus, Circuit Breaker dapat menghentikan panggilan ke layanan pembayaran dan menggunakan logika fallback untuk menunjukkan pesan kesalahan kepada pengguna.

2. Retry

Retry adalah mekanisme yang memungkinkan panggilan ke layanan eksternal untuk dicoba kembali jika panggilan awal gagal. Ini membantu mengatasi kesalahan sementara dan memastikan bahwa panggilan berhasil setelah beberapa percobaan.

@Slf4j
public class RetryTest {

void callMe(){
log.info("try call me");
throw new IllegalArgumentException("Error in callMe()");
}

@Test
void testRetry() {
Retry retry = Retry.ofDefaults("ichwan");
retry.getEventPublisher().onRetry(event -> {
log.info("try to retry");
});

//jika method callMe() mengembalikan nilai, gunakan decorateSupplier()
Runnable runnable = Retry.decorateRunnable(retry, () -> callMe());
runnable.run();
}
}

Contoh kasus: Dalam aplikasi email, jika server email mengalami kesalahan koneksi, mekanisme Retry dapat mencoba mengirim email lagi setelah beberapa detik untuk meningkatkan kemungkinan keberhasilan.

3. Rate Limiter

Rate Limiter membatasi jumlah panggilan ke layanan eksternal dalam satu periode waktu tertentu. Ini membantu mencegah panggilan berlebihan ke layanan yang mungkin tidak dapat menangani beban tinggi.

@Slf4j
public class RateLimiterTest {

private final AtomicLong counter = new AtomicLong(0L);

@Test
void testRateLimiterRegistry() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(100)
.limitRefreshPeriod(Duration.ofMinutes(1))
.timeoutDuration(Duration.ofMinutes(3))
.build();

RateLimiterRegistry registry = RateLimiterRegistry.ofDefaults();
registry.addConfiguration("config", config);

RateLimiter rateLimiter = registry.rateLimiter("ichwan", "config");

for (int i = 0; i < 500; i++) {
Runnable runnable = RateLimiter.decorateRunnable(rateLimiter, () -> {
long result = counter.incrementAndGet();
log.info("Result {}", result);
});

runnable.run();
}
}
}

Contoh kasus: Dalam aplikasi pencarian, Rate Limiter dapat digunakan untuk membatasi jumlah pencarian yang pengguna dapat lakukan dalam satu menit, mencegah penyalahgunaan dan mengurangi beban pada server pencarian.

4. Time Limiter

Time Limiter di Resilience4j adalah salah satu komponen penting yang memungkinkan Anda mengatur waktu maksimum yang diberikan untuk sebuah operasi sebelum dianggap gagal

@Slf4j
public class TimeLimiterTest {

@SneakyThrows
public String slow(){
log.info("Slow");
Thread.sleep(5000L);
return "Done";
}

@Test
void timeLimiter() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> slow());

TimeLimiter timeLimiter = TimeLimiter.ofDefaults("ichwan");
Callable<String> callable = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> future);

callable.call();
}
}

Contoh kasus: Ketika aplikasi Anda melakukan panggilan ke layanan eksternal, misalnya API HTTP, Anda dapat menggunakan Time Limiter untuk memastikan bahwa panggilan tersebut tidak memakan waktu terlalu lama. Jika panggilan tersebut tidak mendapatkan respons dalam batas waktu yang ditetapkan, Anda dapat mengambil tindakan pemulihan, seperti mencoba panggilan ke server cadangan atau memberikan respons default kepada pengguna.

5. Bulkhead

Bulkhead memisahkan bagian-bagian dari sistem untuk memastikan bahwa kegagalan atau overload di satu bagian tidak mempengaruhi bagian lainnya. Ini membantu melindungi bagian yang sehat dari sistem dari dampak negatif bagian yang tidak sehat.

private AtomicLong counter = new AtomicLong(0L);

@SneakyThrows
public void slow(){
long value = counter.incrementAndGet();
log.info("Slow : "+value);
Thread.sleep(5_000L);
}

@Test
void testSemaphoreBulkhead() throws InterruptedException {
Bulkhead bulkhead = Bulkhead.ofDefaults("ichwan");

for (int i = 0; i < 100; i++) {
Runnable runnable = Bulkhead.decorateRunnable(bulkhead, () -> slow());
new Thread(runnable).start();
}

Thread.sleep(10_000L);
}

@Test
void testThreadPoolBulkhead() {

ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.ofDefaults("ichwan");

for (int i = 0; i < 100; i++) {
Supplier<CompletionStage<Void>> supplier = ThreadPoolBulkhead.decorateRunnable(bulkhead, () -> slow());
supplier.get();
}
}
}

Contoh Kasus: Dalam sistem reservasi hotel, jika sistem pembayaran mengalami overload, Bulkhead dapat memisahkan panggilan reservasi dari panggilan pembayaran, memastikan bahwa reservasi tetap dapat dilakukan meskipun layanan pembayaran mengalami masalah.

Resilience4j menyediakan banyak modul lainnya dan dapat dikombinasikan bersama untuk menciptakan aplikasi yang tahan terhadap kesalahan, tangguh, dan responsif. Penggunaan modul-modul ini sangat tergantung pada kebutuhan aplikasi dan situasi penggunaan yang spesifik. Dengan menggabungkan modul-modul Resilience4j ini, pengembang dapat membangun aplikasi Java yang handal dan berkualitas tinggi dalam menghadapi situasi yang kompleks dan tidak terduga.

--

--