Heiko Maaß Software-Entwicklung und Elektronische Musik

TCP- und Http-Retries mit Apache Commons HttpClient (Teil 1)

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

var httpClient = HttpClientBuilder.create()
  .setRetryHandler(
    new DefaultRequestRetryHandler(
       1,    // Max. Anzahl der Retries

       false // Retries auch bei idempotenten Requests

      )
   ).build();

Im nächsten Post schauen wir uns die ServiceUnavailableRetryStrategy an.