Resilience4j
Tags: resilience4j, java, faulttolerance
Published: 2022-08-14 11:16:00

Resilience4j, birbirleri ile iletişim içerisinde bulunan sistemler için hata toleransını yöneterek daha esnek bir yapı içerisinde
işlemlerinı sürdürmelerine yardımcı olan bir kütüphanedir. Bir noktadaki hatanın bütün sistemi etkilememesi asıl amaçtır.

Gerçekleştirmesinde Decorator Pattern ve Fonksiyonel Programlama konseptlerinden faydalanır.

Bu esnek yapıyı oluşturmak için çeşitli temel parçalar içerir; 

- CircuitBreaker
- Retry
- TimeLimiter
- RateLimiter
- Bulkhead

---

## Circuit Breaker

Sistemdeki hata veya gecikmenin belirlenebilen bir eşik değerinin üzerine çıktığı durumlarda, kaynakların boşa harcanmaması için bağlantının kesilmesi olayıdır.

Bu oranı gözlemlemek için _**sliding window**_ kullanılır ve iki tipi vardır;

- **Count Based** : Son N çağrı göz önünde bulundurulur ve çıktılarını N boyutunda bir circular array içerisinde toplar. Bu çağrılarda gecikme veya hata oranı eşik değeri aşarsa, devre kesilir.
- **Time Based** : Son N saniyede gelen çağrıların çıktıları için N boyutunda bir circular array oluşturur. Her bir indeks bir saniyelik aralığa denk gelmektedir. Her bir indekste  **hatalı**, **gecikmeli** ve **toplam** çağrıları belirten 3 ayrı integer tutulur. Bu değerlerin oranına göre devrenin kesilmesine karar verilir.

Tanımlanan listelere veri girişi olduğu zaman yapılan hesaplama ile belirlenen hata ve gecikme eşik değerleri karşılaştırılır ve durum değişiklikleri yapılır.

- **failureRateThreshold** : Hata oranının eşik değeridir, varsayılan olarak %50 durumundadır.
- **slowCallRateThreshold** : Gecikme oranının eşik değeridir, varsayılan olarak %100 durumundadır. Bir çağrının gecikme olarak kabul edilmesi de (slow call) **_slowCallDurationThreshold_** değerine bağlıdır. Varsayılan olarak 60000 ms üzeri süren bir çağrı gecikme olarak kabul edilir.

Bu eşik değerleri ile ilgili bir diğer önemli bir nokta da, kontrole dahil edilmeleri, toplam istek sayısının **_minimumNumberOfCalls_** değerini aşmasından sonra gerçekleşir. Örneğin bu değer 10 olduğunda, gelen 9 istek de hatalı olsa dahi **_minimumNumberOfCalls_** değeri henüz aşılmadığı için devre kesilmez.

### Devre Durumları

![circuitbreakerstates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/omz81vtcavs3bpgzvjva.jpg)
 

Devrenin bulunabileceği 3 farklı durum söz konusudur;

- **CLOSED** : Devre kesiminin gerçekleşmediği, her şeyin normal seyrinde devam ettiği durumdur.
- **OPEN** : Hata veya gecikme oranlarının eşik değerleri aşıldığında sistemin bulunduğu durumdur. Bu durumda iken gelen istekler **_CallNotPermittedException_** hatası ile geri çevrilir. 
- **HALF-OPEN** : Kesilmiş bir devrenin, **_waitDurationInOpenState_** ile belirlenen süreyi beklemesinden sonra, direk olarak **CLOSED** durumuna geçmek yerinde geçtiği ara durumdur. Bu durumda 
  **_permittedNumberOfCallsInHalfOpenState_** ile belirtilen miktarda istek alınmasına izin verilir. Eğer bu isteklerdeki hata veya gecikme oranı eşik değerlerinden yüksek ise tekrar **OPEN** durumuna, değil ise **CLOSED** durumuna geçilir. Bu duruma geçişin otomatik bir şekilde yapılması için **_automaticTransitionFromOpenToHalfOpenEnabled_** değerinin **true** olarak ayarlanması gereklidir.

----

Bu işlemeler dahilinde varsayılan olarak, bütün Exception tipleri hata olarak sayılır. Ancak bu yaklaşım yerine kendi ayarlamalarımızı da yapabiliriz;

- **_recordExceptions_** listesi kullanarak hata olarak sayılmasını istediğimiz Exception sınıflarını tanımlayabiliriz. Bunlar haricindekiler başarılı olarak sayılacaktır. Ayrıca belirtilen sınıflardan miras almış sınıflar da aynı şekilde ele alınacaktır.
- **_ignoreExceptions_** listesi ile görmezden gelinecek Exception sınıflarını tanımlayabiliriz. Bu sınıflar ne başarılı ne de başarısız olarak kabul edilecektir.

## Retry

Hata ile sonuçlanan bir çağrının otomatik olarak yeniden gönderilmesi amacıyla kullanılır. 

Retry ile alakalı dikkat edilmesi gereken önemli bir nokta, çağrı sonucu çalışan operasyonun **idempotent** olması, yani tekrarlanan istekte gereksiz olan bulunuyorsa bunun görmezden gelinebilmesidir, aksi takdirde mantık hatalarına yol açabilir. Örneğin operasyon için çağrı yapıldığında, gerekli işlemler sağlıklı bir şekilde gerçekeştirilmiş olsun ancak geri dönüşte bir problem oluşmuş olsun, bu durumda istek tekrarlanacak ve eğer sistem buna hazırlıklı değilse aynı işlemler tekrar yapılmaya çalışılacaktır.

Retry kullanımında çeşitli değerleri konfigüre edebiliriz ; 

- **maxAttempts**: Hata durumunda maksimum deneme sayısını belirtir. 
- **waitDuration**: Her bir deneme arasında geçen sabit süreyi belirtir.
- **enableExponentialBackoff**: Denemeler arasındaki geçen sürenin sabit olmayıp, exponential şekilde artacağını belirtir. Örneğin, denemeler arasında geçen süreler 2-4-8 şeklinde artar

Yeniden deneme durumları istenen şekilde ayarlanabilir. 

- Atılan isteğe bir cevap dönülmüş ise konfigürasyon dosyasına eklenecek **resultPredicate** değeri ile hangi koşulda retry yapılacağı ayarlanabilir.

>
> resultPredicate: io.reflectoring.resilience4j.springboot.predicates.ConditionalRetryPredicate
>

>```
>ConditionalRetryPredicate implements Predicate<SearchResponse> {
>  @Override
>  public boolean test(SearchResponse searchResponse) {
>    if (searchResponse.getErrorCode() != null) {
>      return searchResponse.getErrorCode().equals("FS-167");
>    } 
>    return false;
>  }
>}
>```

- İstek gerçekleştirilirken hata gerçekleşmiş ise **retryExceptionPredicate** kullanılarak, hata incelenip retry yapılıp yapılmayacağına karar verilebilir veya **retryExceptions** ve **ignoreExceptions** değerleri kullanılarak direk olarak sınıflarına göre filtreleme de yapılabilir.

>
> retryExceptionPredicate: io.github.resilience4j.circuitbreaker.RecordFailurePredicate
>

>```
>public class RecordFailurePredicate implements Predicate<Throwable> {
>
>    @Override
>    public boolean test(Throwable throwable) {
>        return throwable instanceof IOException || throwable instanceof IgnoredException;
>    }
>}
>```

## Time Limiter

TimeLimiter, ilgili uca yapılan işlemin en fazla ne kadar zaman alacağını belirlemek için kullanılır.

- TimeLimiter sadece asenkron işlemin yapıldığı(**_CompletableFuture_** ile sağlanan) endpoint'lere uygulanabilir.

TimeLimiter kullanımında çeşitli değerleri konfigüre edebiliriz;

- **timeoutDuration**: Gelen isteğin zaman aşımı süresini belirtir.
- **cancelRunningFuture**: Hali hazırda çalışan future için iptal edilip edilemeyeceğini belirtir.

## Rate Limiter

Sisteme, belirtilen zaman aralığı içerisinde gelebilecek istek sayısını kontrol edebilmemizi sağlar.

Rate Limiter kullanımında çeşitli değerleri konfigüre edebiliriz;

- **limitForPeriod**: Belirtilen periyot için izin verilecek azami istek sayısır
- **limitRefreshPeriod**: Verilen limit değerinin geçerli olacağı süreyi belirtir. Süre sonunda istek sayısı sıfırlanır.
- **timeoutDuration**: Limit değere ulaşıldığında, blokalanan thread için kaç saniye bekleneceğini belirtir. 

## Bulkhead

Bulkhead ile gelen eşzamanlı çağrı sayısını sınırlayabiliriz.

İki şekilde gerçekleştirilebilir ;

- **SemaphoreBulkhead** : Gelen isteklerin semafor yardımı ile kilitleme mantığı ile yürütülmesi.
- **FixedThreadPoolBulkhead** : Gelen her istek için, oluşturulan thread havuzundan uygun bir thread alınır ve izole bir şekilde işlemler gerçekleştirilir..

Semafor yaklaşımı için iki değer konfigüre edilebilir;

- **maxConcurrentCalls**: Bulkhead tarafından aynı anda yürütülebilecek, azami istek sayısını belirtir.
- **maxWaitDuration**: Belirlenen **maxConcurrentCalls** sayısının aşımına sebep olan isteğin ne kadar süre bloklanacağını belirtir.


Thread havuzu yaklaşımı için çeşitli değerler konfigüre edilebilir;

- **coreThreadPoolSize** : Oluşturlan havuz için sabit(minimum) thread miktarını belirtir.
- **maxThreadPoolSize** : **coreThreadPoolSize** miktarından daha fazla thread kullanımı gerektiğinde, geçici şekilde oluşturulabilecek yeni threadlerin azami miktarını belirtir. 
- **queueCapacity** : Thread havuzunda uygun thread bulunmadığında gelen isteklerin bekletileceği sıranın kapasitesini belirtir.
- **keepAliveDuration** : Geçici oluşturulan threadlerin boşta ne kadar süre bekleyebileceğini belirtir. Bu süre aşımında thread sonlandırılır.