„Play it again, Sam“ – Spring Retry

Bei der Arbeit mit verteilten Systemen geschieht es immer wieder, dass manche Services nicht erreichbar sind. Dies kann an einem ungeplanten Wartungsfenster, einer unterbrochenen Verbindung oder einer Fehlfunktion liegen. In solchen Fällen wünscht man sich als Entwickler eine einfache Möglichkeit die fehlgeschlagene Aktion zu wiederholen. Im Spring Boot Umfeld wird diese Möglichkeit durch Spring Retry bereitgestellt.

Bevor Spring Retry genutzt werden kann, müssen die entsprechenden Anhängigkeiten ins Projekt aufgenommen werden.

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.9</version>
</dependency>

Um eine fehlgeschlagene Aktion zu wiederholen gibt es mit Spring Retry einen deklarativen und einer imperativen Ansatz. Der hier vorgestellte deklarative Ansatz nutzt Annotationen an den Methoden, die bei einem Fehlschlag erneut ausgeführt werden sollen.

@Service 
public class ArchiveService {
  @Retryable
  public boolean checkRemoteAncestorArchive(String archive, Person person) {
    // check person 
  }
}

Die Methode checkRemoteAncestorArchive im ArchiveService prüft eine Person in der Ahnenverwaltung gegen ein anderes Ahnenarchiv. Dies geschieht immer dann, wenn eine Person mit einem externen GINA Verweis versehen wurde.

Die Annotation @Retryable markiert die Methode für den Retry-Mechanismus. Damit der Mechanismus arbeitet muss er über die Annotation @EnableRetry an einer Konfigurationsklasse aktiviert werden. wird nun in dieser Methode eine Exception geworfen, dann wird diese Methode erneut aufgerufen. Dies geschieht standardmäßig drei mal mit einem Abstand von einer Sekunde.

Üblicherweise soll die Methode aber nicht bei jeder Art von Fehler wiederholt werden. Es sollte sich nur um temporäre Störungen in der Infrastruktur handeln. Verarbeitungsfehler durch falsche Daten werden sich durch erneute Versuche nicht ausmerzen lassen.

Only fools repeat the same things over and over, expecting to obtain different results.

George Bernard Shaw

Um Fehlerquellen zu unterscheiden können passende Exception Klassen als Parameter der Annotation hinzugefügt werden.

@Retryable(ArchiveNotReachableException.class)
public boolean checkRemoteAncestorArchive(String archive, Person person) {
  // check person 
}

In diesem Fall wird die Methode nur erneut aufgerufen, wenn sie aufgrund einer ArchiveNotReachableException fehlschlug. In allen anderen Fällen wird kein weiterer Aufruf der Methode eingeleitet.

@Retryable(include=ArchiveNotReachableException.class, exclude=UnauthorizedOnArchiveException.class, maxAttempts = 5, backoff = @Backoff(delay = 2000, multiplier=2))
public boolean checkRemoteAncestorArchive(String archive, Person person) {
  // check person 
}

Das Verhalten des Retry-Mechanismus kann durch die Annotation aber noch weiter parametrisiert werden. In dem hier dargestellten Beispiel wird der Mechanismus nicht angewendet, wenn es sich bei der Exception um eine UnauthorizedOnArchiveException handelt. In diesem Fall kann ein Fehlschlag nur durch die Anpassung der Authentisierung korrigiert werden. Durch den Parameter maxAttempts wird der erneute Aufruf der Methode auf 5 Versuche erhöht. Der Parameter backoff sorgt für eine geänderte Wartezeit zwischen den Versuchen. Während standardmäßig jeweils eine Sekunde gewartet wird, sind es hier 2, 4, 8, 16 und 32 Sekunden.

In jedem der bisher dargestellten Beispiele endet die Reihe erfolgloser Versuche schlussendlich mit einem Fehler durch eine Exception. Der Retry-Mechanismus liefert jedoch auch die Möglichkeit für einen sauberen Abschluss einer fehlschlagenden Aktion durch die @Recover Annotation.

@Recover
public boolean markOutstandingArchiveCheck(ArchiveNotReachableException exception, String archive, Person person) {
  // mark outstanding check
}

Die @Recover Annotation markiert eine Methode, die nach dem letzten und erfolglosen Versuch aufgerufen werden soll. Dabei muss diese Methode den identischen Rückgabewert wie die Originalmethode besitzen. Außerdem kann sie deren Parameter in korrekter Reihenfolge als eigene Parameter definieren. Zusätzlich sollte sie als ersten Parameter den Typ der Exception besitzen. All diese Parameter werden genutzt um die Recover-Methode der richtigen Retry-Methode zuzuordnen. Der Rückgabewert der Recover-Methode ist darüber hinaus auch der Rückgabewert der Retry-Methode im Fehlerfall.

Im hier dargestellten Beispiel wird bei einer erfolglosen Überprüfung die entsprechende Person als noch nicht überprüft markiert. Solche Personeneinträge können dann zu einem späteren Zeitpunkt erneut zur Prüfung gegeben werden.

Damit ist der deklarative Ansatz von Spring Retry auch schon vorgestellt. Ein einfacher und eleganter Weg um temporäre Probleme einer Anwendung durch Wiederholungen zu korrigieren.