Nachdem die Antwort des aufgerufenen Services erfolgreich angekommen ist, kommt die ServiceUnavailableRetryStrategy ins Spiel. Über dieses Interface kann der Apache HttpClient instruiert werden, den Request zu wiederholen.
Die DefaultServiceUnavailableRetryStrategy wiederholt einen Request bei Erhalt des Statuscodes 503 (Service Unavailable). Die maximale Anzahl der Retries sowie das Delay kann konfiguriert werden. Neben einem Retry bei HTTP 503 empfiehlt sich im Kubernetes-Umfeld auch ein Retry bei Erhalt eines HTTP 502 (Bad-Request), der auftreten kann, wenn der aufrufende Service neu deployed wurde, und noch eine persistente Connection zum bereits abgebauten Pod besteht. Hierzu muss man das ServiceUnavailableRetryStrategy Interface selber implementieren.
Jedes Retry sollte geloggt werden, um beobachten zu können, wie häufig so Retry stattfindet.
Wenn ein Netzwerk-Request eines Webservices auf einen anderen Webservice scheitert, kann es sinnvoll sein, den Request zu wiederholen. Die Umsetzung sollte dabei wenn möglich auf Ebene der verwendeten HTTP-Library erfolgen, um Mehrfachserialisierungen zu vermeiden. Ansonsten kann das bei besonders großen Objekten viel CPU-Rechenleistung kosten.
Der Apache HttpClient bietet die Konfiguration von Retries auf zwei Ebenen an:
Zum einen gibt es den HttpRequestRetryHandler, zum anderen die ServiceUnavailableRetryStrategy. Leider wird aus dem Klassennamen nicht klar, worin sie sich unterscheiden.
In diesem Blogpost sehen wir uns zunächst den HttpRequestRetryHandler an.
Retries auf TCP-Ebene: HttpRequestRetryHandler
Bei einem TCP-Verbindungsaufbau kann es zu vielen Fehlerfällen kommen. Beispielsweise wird eine NoHttpResponseException geworfen, wenn der Server eine sog. “persistent connection” geschlossen hat, die der Client jedoch noch als nutzbar betrachtet.
Wenn der Server eine initiale TCP-Verbindung nicht rechtzeitig annimmt, kommt es zur einer ConnectTimeoutException, bei einem Read-Timeout dagegen zu einer SocketTimeoutException.
All diese Exceptions erben von java.io.IOException. Der HttpRequestRetryHandler kann aufgrund des Klassentyps entscheiden, ob ein Retry ausgeführt werden soll.
Auszuschließende Exceptions
Die Standardimplementierung DefaultRequestRetryHandler definiert eine Liste von Exceptions für die kein Retry stattfinden soll:
java.io.InterruptedIOException (Superklasse der o.g. ConnectTimeoutException),
java.net.ConnectException,
java.net.UnknownHostException,
javax.net.ssl.SSLException
In einem Kubernetes-basiertem Projekt hat es sich als hilfreich erwiesen, diese Liste anzupassen. Auch bei einer java.net.ConnectException und java.io.InterruptedIOException kann ein Retry Sinn machen, da Kubernetes Services über eine virtuelle IP-Adresse angesprochen werden und hinter der IP-Adresse ein IP-Tables basiertes Routing passiert. Um die Antwortzeiten eines Services nicht zu gefährden, sollte man durch Read-Timeouts fehlgeschlagene NICHT wiederholen, somit sollte die java.net.SocketTimeoutException in die Liste der auszuschließenden Exceptions aufgenommen werden.
java.net.SocketTimeoutException
java.net.UnknownHostException
javax.net.ssl.SSLException
Um diese Exception-Liste anzupassen muss eine Subklasse von DefaultRequestRetryHandler erstelle.
Keine Retries bei idempotenten Retries
Retries sollten nur dann ausgeführt werden, wenn der Request als idempotent betrachtet wird (mehrfach ausführbar mit dem gleichen Endergebnis). Ein POST-Request ist normalerweise nicht idempotent, in Sonderfällen kann aber auch er wiederholt werden, wenn die aufzurufende Schnittstelle eher einen Remote-Procedure-Call abbildet.
Konfiguration im HttpClient
Im nächsten Post schauen wir uns die ServiceUnavailableRetryStrategy an.