Das External-Task-Pattern kehrt den Push-Mechanismus bei der Ausführung von Service-Tasks mit der Camunda Platform zu einem Pull-Prinzip um. Dafür holen sich eine oder mehrere separate Anwendungen auszuführende Aufgaben über die Rest-API der Prozess-Engine ab, führen sie aus und geben das Ergebnis zurück. In einem vorherigen Artikel haben wir bereits potentielle Vor- und Nachteile diskutiert, die sich durch die Verwendung von External Tasks ergeben. Wir haben anhand eines Beispiels auch gezeigt, was es bei der Verwendung des offiziellen Java-Clients von Camunda zu beachten gibt. In diesem Beitrag gehen wir etwas detaillierter auf das Fehlerverhalten von External Tasks ein, wie man sich die Arbeit durch Auslagerung von allgemeinem Verhalten vereinfachen kann und stellen dafür einen Spring-Boot-Starter bereit.
Fehler? Fehler kommen nicht vor! (Der Gutfall)
Im Idealfall ist es so, dass bei der Ausführung einer Software keine Fehler passieren und der Code ohne Fehlerverhalten auskommt. So sehr sich ein:e Entwickler:in jedoch daran versucht: Fehler können niemals ausgeschlossen werden. Aus diesem Grund ist es wichtig Software auf mögliche Fehler vorzubereiten, seien es Fehler aus dem Inneren der Anwendung selbst, ungültige Eingaben der Benutzer:innen oder Nichtverfügbarkeit von Umsystemen, auf die man tatsächlich nur noch einen sehr eingeschränkten Einfluss hat. Das Stichwort ist "Resilienz".
Unser Beispiel ist ein Prozess, über welchen eine E-Mail verschickt werden soll. Dazu wird in einer Benutzeraufgabe sowohl E-Mail-Adresse als auch Inhalt angegeben. Der anschließende ServiceTask "E-Mail senden" wird, wie es sein Name verrät, zum Versand der E-Mail benutzt.
External-Task-Worker
Der zugehörige External-Task-Worker zu dem skizzierten Szenario könnte wie folgt funktionieren. Aus dem Prozesskontext werden die Werte der E-Mail Adresse sowie der E-Mail Inhalt geladen, welche zuvor in einem User-Task angelegt worden sind. Der Mail-Service verschickt die Daten und anschließend wird die Aufgabe abgeschlossen: taskService.complete(task). Diese Implementierung ist valide und funktioniert - jedenfalls solange keine Fehler auftreten.
Hinweis
Unsere Beispiele sind auf Basis des offiziellen Java-Clients von Camunda umgesetzt, d.h. für eigene Implementierungen muss man die folgenden Fehlerszenarien ebenfalls bedenken, die Umsetzung wird jedoch variieren. Seit Version 7.15 stellt Camunda neben dem Java-Client für External Tasks selbst zusätzlich auch einen Spring-Boot-Starter für diesen Client bereit. Mit diesem Starter wird die zuvor noch notwendige Konfiguration obsolet, und es lassen sich External-Task-Worker durch die application-yaml und die Annotation @ExternalTaskSubscription steuern. Weitere Informationen gibt es im Camunda-Blog. In unserem Code-Beispiel verwenden wir ebenfalls dieses Verfahren.
Was kann denn schon passieren?
Im vorherigen Beispiel ist kein Fehlerverhalten vorgesehen. Dies kann dazu führen, dass der ServiceTask abgeschlossen wird, obwohl die E-Mail gar nicht verschickt worden ist, sofern auf Fehler des Mail-Service nicht angemessen reagiert wird und die complete-Methode fälschlicherweise aufgerufen wird. Es könnte aber auch dazu führen, dass der Task nie abgeschlossen wird und in diesem Fall die E-Mail sogar mehrfach verschickt wird. Denn falls es zwischen dem E-Mail Versand und der complete-Methode zu einem Abbruch kommt, wird der External-Task nach dem konfiguriertem Lock-Intervall durch die Prozess-Engine wieder freigegeben, ein External-Task-Worker führt ihn aus, jedoch passiert derselbe Fehler erneut: Endlosschleife - zumindest bis der Fehler erkannt und behoben wird. Um die Brücke in die Java-Delegate-Variante zu schlagen: Dort ist ein explizites Fehlerverhalten zwar ebenfalls eine gute Idee, aber es ist nicht zwingend erforderlich, denn im Fehlerfall erzeugt die Prozess-Engine zumindest einen Incident, welcher im Cockpit angezeigt wird.
Fehlerverhalten implementieren
Für einen Umgang mit technischen Fehlern könnte ein Fehlerverhalten wie folgt aussehen: Der fachliche Programmiercode steht in einem try-Block. Fehler aus dem Mail-Service werden gefangen und im catch-Block behandelt. In der handleFailure-Methode müssen neben einer Fehlerursache auch Werte für die verbleibende Anzahl an Wiederholungen und die Wartezeit bis zur nächsten Wiederholung angeben werden. Im Gegensatz zur Java-Delegate-Variante muss man sich in einem External-Task-Worker selber darum kümmern - es genügt nicht mehr das Retry-Verhalten im Prozessmodell zu konfigurieren. Sobald es keine Wiederholungen mehr gibt, wird ein Incident an dem entsprechenden Prozessschritt erzeugt. Die Anzahl der verbleibenden Wiederholungen lässt sich am Task-Objekt mit der getRetries-Methode abfragen (Achtung: Dieser Wert kann null sein). Darauf aufbauend ist in unserem Beispiel ein festes, aufsteigendes Fehlerverhalten implementiert, welches maximal fünf Wiederholungen erzeugt, jeweils um eine zusätzliche Minute verzögert.
E-Mail-Adresse unbekannt
Neben technischen Fehlern können natürlich auch fachliche Fehler passieren, welche auch durch mehrfache Wiederholung nicht behoben werden und die ein Anwendungssystem nicht von selber korrigieren kann. In diesen Fällen lässt sich auf das Retry-Verhalten verzichten, und der External-Task-Worker kann mit einem Bpmn-Error abschließen. In unserem Beispiel erweitern wir den Prozess um eine Benutzeraufgabe, falls eine E-Mail-Adresse vom Mail-Service nicht gefunden wird.
Fachliche Fehler berücksichtigen
Das abweichende Fehlerverhalten bei z. B. einer ungültigen E-Mail-Adresse wird über einen eingeschobenen catch-Block realisiert: Sobald der Mail-Service eine RecipientNotFoundException meldet, wird nicht die Logik zur Ermittlung der nächsten Wiederholung angesprochen, sondern der External-Task wird mit der handleBpmnError-Methode abgeschlossen. Hierfür ist weder die Anzahl verbleibender Wiederholungen noch eine Wartezeit vorgesehen, stattdessen lassen sich optional ein Fehler-Code und eine Fehlernachricht sowie Prozess-Variablen zurückgeben. Achtung: BPMN-Fehler müssen im Prozessmodell behandelt werden, wie hier durch das ausgelöste Boundary-Event. Wird ein BPMN-Fehler erzeugt, der nicht im Modell abgebildet ist, so verläuft diese Prozessinstanz im 'Nichts'.
Fehlerverhalten konfigurieren
Bisher waren Entwickler:innen und Prozessbeteiligte es gewöhnt das Verhalten bei technischen Fehlern im Prozessmodell zu spezifizieren. Sobald ein ServiceTask als "Asynchronous Before" markiert wird, lässt sich in das eingeblendete Textfeld "Retry Time Cycle" ein Wert nach ISO 8601 eintragen um festzulegen wann und wie oft die Durchführung eines Tasks erneut versucht werden soll, falls die Ausführung der Aufgabe fehlschlägt. Dieser kann beispielsweise lauten "R3/PT5M", was soviel heißt wie drei Wiederholungen im Abstand von jeweils fünf Minuten. Auch möglich: "PT5M,PT30M,PT1H" - Erste Wiederholung nach fünf Minuten, dann ggf. eine Wiederholung nach einer halben Stunde und zuletzt noch eine Wiederholung nach einer Stunde.
Dieses Feature lässt sich auch für External Tasks nachahmen, indem die Erweiterungen eines Tasks genutzt werden. Sofern für einen Task im Prozessmodell unter den "Extensions" das Fehlerverhalten mit einem vereinbarten Wert angegeben wird, lässt es sich in der Implementierung des External-Task-Workers mit der getExtensionProperties-Methode auslesen und entsprechend darauf reagieren. Je nachdem, ob es eine eigene oder die offizielle ISO-8601 Notation ist, muss natürlich eine Logik vorgesehen sein, die aus dem angegeben Wert und den bisherigen Retries die Werte für die nächste Wiederholung berechnet.
Hinweis
Das Verhalten die angegebenden Extensions aus dem Prozessmodell zu laden muss je Worker aktiviert sein. Entweder in der application.yaml (in der Spring-Boot-Starter Variante verfügbar ab Version 7.15) oder in der herkömmlichen Worker-Konfiguration der Spring-Anwendung.
camunda.bpm.client:
base-url: http://localhost:8080/engine-rest
subscriptions:
send-mail:
include-extension-properties: true
Fehlerverhalten automatisieren (Retry-Aspect)
Die zuvor beschriebene Methode haben wir genutzt, um das gewünschte Verhalten in einem eigenen Spring-Boot-Starter zu generalisieren. Dadurch muss das Fehlerverhalten nicht in jedem External-Task-Worker einzeln und als Boilerplate-Code hinzugefügt werden muss, sondern es wird durch die folgende Abhängigkeit im Projekt für jeden External-Task-Handler des Typs "ExternalTaskHandler" (Camunda-Interface) ergänzt.
Weitere Informationen zu diesem Projekt gibt es auf GitHub: https://github.com/viadee/external-task-retry-aspect
<dependency>
<groupId>de.viadee.bpm.camunda</groupId>
<artifactId>external-task-retry-aspect-spring-boot-starter</artifactId>
<version>${version.retry-aspect}</version>
</dependency>
External-Task-Retry-Aspect
Durch Benutzung unseres Spring-Boot-Starters führen alle Fehler bei der Ausführung in einem External-Task-Worker ohne weiteres Zutun zu einem Fehlerverhalten. Standardmäßig sind dies drei Versuche im zeitlichen Abstand von jeweils fünf Minuten. Sowohl das Standardverhalten als auch das Fehlerverhalten je Task ist natürlich konfigurierbar.
Des Weiteren können fachliche Fehler als ExternalTaskBusinessError erzeugt werden, was dem Aufruf der handleBpmnError-Methode von oben entspricht. Zusätzlich ist es mit einer InstantIncidentException möglich das Fehlerverhalten zu überspringen, um unmittelbar einen Incident zu erzeugen. Beide Varianten sind optional, sodass sich ein:e Entwickler:in ohne try-catch-Block auch ausschließlich auf die Fachlichkeit konzentrieren kann. Das Fehlerverhalten wird dadurch nicht vergessen, denn es findet auf jeden Fall eine rudimentäre Behandlung im Hintergrund statt und Endlosschleifen werden vermieden. Natürlich kann man Fehler auch immer noch explizit behandeln, falls die grundlegende Variante einmal nicht ausreichend ist.
Fazit
Der Einsatz des External-Task-Pattern bietet nach wie vor viele Potentiale in ganz unterschiedlichen Kategorien. Dies haben wir in unserem vorherigem Artikel zu diesem Thema bereits herausgestellt. Eine Entkopplung zwischen Prozess-Engine und Ausführung durch External Tasks ist zukunftsfähig und findet immer weitere Verbreitung. Darauf deutet auch der neue, von Camunda zur Verfügung gestellte Spring-Boot-Starter für External Tasks Clients hin. Speziell bei der Fehlerbehandlung kommt allerdings etwas mehr Verantwortung auf die Entwickler:innen zu, als es noch bei der klassischen Variante mit Java-Delegates der Fall war: Eine vergessene Fehlerbehandlung endete bisher schlimmsten Falls in unnötigen Wiederholungen und schlussendlich in einem im Cockpit sichtbaren Incident. Mit External Tasks führt ein vergessenes oder fehlerhaftes Fehlerverhalten jedoch ggf. zu Endlosschleifen. Eine Abhilfe für diese Herausforderung stellen wir mit unserem External-Task-Retry-Aspect ebenfalls als Spring-Boot-Starter zur Verfügung, mit dessen Hilfe ein generelles Fehlerverhalten zu einem External-Task-Worker hinzugefügt wird.
Code-Bespiele auf GitHub: https://github.com/viadee/bpmnExternalTaskWorkerExample
External-Task-Retry-Aspect: https://github.com/viadee/external-task-retry-aspect
zurück zur Blogübersicht