© Excellent backgrounds/Shutterstock.com
Teil 3: Microservices mit Java EE, Fehlerbehandlung und Konfiguration

Ein robuster(er) Taschenrechner


Im letzten Teil dieser Serie hatten wir zwei kommunizierende Thin WARs implementiert: calcu­la­tor. war mit 5,4 KB und ein addition.war mit 4,6 KB. Der Fokus lag auf der Funk­tionalität. Fehlerbehandlung und nicht funktionale Anforderungen haben wir überhaupt nicht berücksichtigt. In diesem Artikel werden wir uns nun mit Fehlerbehandlung und ­Konfigurierbarkeit beschäftigen.

Ab dem Zeitpunkt der Einführung des addition.war-Service hatten wir uns bewusst für die Verteilung einer Anwendung auf mehrere Prozesse entschieden. Damit fängt der Ärger an: Ein verteiltes System verhält sich anders als ein lokales. Partielle Ausfälle, Time-outs, langsame Verbindungen oder Kommunikationsfehler können innerhalb einer JVM nicht auftreten. In verteilten Anwendungen rechnet man nicht nur mit solchen Fehlern, man muss sogar davon ausgehen, dass sich unsere Kommunikationspartner schlecht benehmen.

Der calculator-Service muss mit einem instabilen addition-Service umgehen können. Und ein addition-Service muss in der Lage sein, sich vor einem übereifrigen calculator-Service zu schützen. Denn alles, was schiefgehen kann, geht auch schief. Man betrachtet jeden Service einzeln und traut der Außenwelt nicht. Der Glaube an Verschwörungstheorien und eine paranoide Angst vor Ausfällen kann bei der Entwicklung von Microservices helfen.

Mit Time-outs klarkommen

Was passiert, wenn die Methode Addition#add blockiert?

@Path("addition") public class AdditionResource { @Inject Addition addition; @POST public JsonObject addition(JsonObject input) { int a = input.getJsonNumber("a").intValue(); int b = input.getJsonNumber("b").intValue(); int result = addition.add(a, b); return Json.createObjectBuilder(). add("result", result). build(); } }

Eine zu langsame Ausführung der Methode add würde zum Blockieren des aktuellen Ausführungsthreads und auch des Aufrufers führen. Schnelle Requests können seriell abgearbeitet werden. Je langsamer die Methode, desto mehr parallele Threads werden gleichzeitig ausgeführt. Da jeder Thread auch Speicherressourcen beansprucht, wird ein zu großzügig konfigurierter Threadpool zum Absturz der JVM durch ein OutOfMemoryError führen. Das Problem lässt sich mit der Einführung eines festen Time-outs entschärfen. Der aktuelle Thread des Servers wird dabei von der Response entkoppelt. Nach dem Verstreichen des Time-outs erhält der Client den Statuscode 503:

 @Inject Addition addition; @POST public void addition(@Suspended AsyncResponse response, JsonObject input) { response.setTimeout(500, TimeUnit.MILLISECONDS); int a = input.getJsonNumber("a").intValue(); int b = input.getJsonNumber("b").intValue(); int result = addition.add(a, b); JsonObject payload = Json.createObjectBuilder(). add("result", result). build(); response.resume(payload); }

Der konventionelle 503-HTTP-Status kann mit einer dedizierten TimeoutHandler-Implementierung angereichert oder auch überschrieben werden:

 @Inject Addition addition; @POST public void addition(@Suspended AsyncResponse response, JsonObject input) { response.setTimeout(500, TimeUnit.MILLISECONDS); response.setTimeoutHandler(this::handleTimeout); //... } void handleTimeout(AsyncResponse response) { Response info = Response. status(Response.Status.SERVICE_UNAVAILABLE). header("reason", "too lazy"). build(); response.resume(info); } }

In einem TimeoutHandler kann neben der Bereitstellung von zusätzlichen Informationen an den Benutzer die Information in ein CDI-Event verpackt und verschickt werden:

 @Inject Event<String> timeoutEscalations; @POST public void addition(@Suspended AsyncResponse response, JsonObject input) { response.setTimeout(500, TimeUnit.MILLISECONDS); response.setTimeoutHandler(this::handleTimeout); //... } void handleTimeout(AsyncResponse response) { timeoutEscalations.fire("addition is too lazy today"); //... } }

Die Time-out-Events werden an @Observes-Methoden ausgeliefert. Die AdditionTimeoutResource ist gleichzeitig ein Observer und ein Monitoringendpunkt:

@Path("timeouts") @ApplicationScoped public class AdditionTimeoutResource { private AtomicInteger timeoutCounter; @PostConstruct public void init() { this.timeoutCounter = new AtomicInteger(); } @GET public JsonObject timeouts() { return Json.createObjectBuilder(). add("addition-timeouts", this.timeoutCounter.intValue()). build(); } public void onNewTimeout(@Observes String timeout) { this.timeoutCounter.incrementAndGet(); } }

In unserem Beispiel werden die Time-outs hochgezählt und via REST-Schnittstelle veröffentlicht. In der Praxis kann ein Time-out in kritischen Systemen zu einer Eskalation durch die Weitergabe der Daten an ein Emergency-Management-System führen.

Sieht gut aus, funktioniert leider nicht

Die Addition-Klasse benötigt für die Berechnung von 42 über eine Sekunde:

@Stateless public class Addition { public int add(int a, int b) { int result = a + b; if (result == 42) { thinkLonger(); } return result; } void thinkLonger() { try { Thread.sleep(1000); } catch (InterruptedException ex) {} } }

Dabei wird der aktuelle Thread schlafengelegt. Beim aktuellen Thread handelt es sich allerdings um einen HTTP-Thread mit dem konfigurierten Time-out. Ein blockierter Thread lässt sich aber in Java kaum beenden. Der Applikationsserver hat also keine Chance, den blo...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang