Failsafe – Arbeiten mit unsteten Gesellen

“Es gibt keine Sicherheit, nur verschiedene Grade der Unsicherheit.”

Anton Pawlowitsch Tschechow

Gerade beim Arbeiten mit entfernten Services ist es ein Ärgernis, wenn diese sich flatterhaft verhalten. Der Programmierer ist ein überschaubares, binäres Verhalten seiner verwendeten Helfer gewohnt, sie liefern das korrekte Ergebnis oder einen Fehler.

Entfernte Services können ausfallen, die Verbindung getrennt werden, eine hohe Last  das Antwortverhalten des Services beeinträchtigen. All diese Sonderfälle im Code zu berücksichtigen ist ein Graus für jeden Entwickler. Die vielen Sonderfälle und ihre Behandlung bläht die Code Basis auf und die tatsächliche Businesslogik verliert sich darin.

Eine gutes Design-Pattern zur Berücksichtigung solcher Probleme ist das Circuit-Breaker Pattern. Der Circuit Breaker schütz, wie die namensgebende Sicherung, die eigene Anwendung vor dem problematischen Service. Solange der Service in vertretbaren Maße arbeitet, bleibt der Circuit-Breaker geschlossen und die Zugriffe erfolgen wie gewohnt. Stellt der Circuit-Breaker eine Häufung von Fehler fest, dann wechselt er in den offenen Modus und die Zugriffe auf den Service werden für eine gewisse Zeit umgeleitet. Danach ist der Circuit-Breaker im halboffenen Modus, den er bei funktionierendem Service in den geschlossen Modus verlässt. Ist der Service weiterhin nicht korrekt ansprechbar, dann wechselt er wieder in den offenen Modus.

Mit der Bibliothek Failsave können Circuit-Breaker recht einfach verwendet werden.

CircuitBreaker breaker = new CircuitBreaker()
  .withFailureThreshold(5)
  .withSuccessThreshold(3)
  .withDelay(1, TimeUnit.MINUTES);

Failsafe.with(breaker).run(this::loadTree);

In diesem Beispiel nutzen wir einen Circuit-Breaker, der sich nach 5 fehlerhaften Versuchen öffnet und mindestens eine Minute geöffnet bleibt und sich nach 3 erfolgreichen Versuchen wieder schließt.

Der Aufruf der Methode loadTree wird in der letzten Zeile mit dem Circuit-Breaker abgesichert. Außerhalb von solchen einfachen Beispielen würde der Circuit-Breaker von verschiedenen Code Abschnitten, die auf die selbe Resource zugreifen, geteilt werden. Üblicherweise als  Dependency Injection oder traditionelle Member Variabel. 

Manchmal möchte man einen fehlgeschlagenen Versuch wiederholen, bevor man am Sicherungskasten herumschraubt. Dafür bietet Failsafe die Retry Policies. Die Retry Policies werden ausgeführt, bevor Failsafe den Versuch als Erfolg oder Misserfolg für den Circuit-Breaker bewertet, Für unser fiktives Szenario mit der loadTree Methode könnten wir eine Retry Policy definieren, die im Falle einer FileNotFoundException, maximal drei weitere Versuche startet und dabei eine immer größere Zeitspanne wartet.

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(FileNotFoundException.class)
  .withBackoff(5, 30, TimeUnit.SECONDS);
  .withMaxRetries(3);

Failsafe.with(retryPolicy).with(breaker).run(this::loadTree);

Die Failsafe Bibliothek bietet eine Menge Möglichkeiten mit Circuit-Breaker und Retry Policy um Fehler bei  synchronen und asynchronen Funktionsaufrufe zu behandeln und dies in einer kompakten und eleganten  Form. Die schwierige Aufgabe, die nun dem Entwickler noch bleibt, ist es die passende Form der Fehlerbehandlung für das eigene Szenario  zu finden.