External Task Worker entkoppeln durch einen Pull-Mechanismus die Bearbeitung von Tasks von der Prozess-Orchestrierung durch die Process Engine.
Bei der Prozessautomatisierung mit Camunda werden Service Tasks meist direkt von der Process Engine mit dafür implementierten Java-Methoden abgearbeitet. Mit dem External Task Worker Pattern gibt es hierzu eine Alternative, bei der sich eine separate Anwendung Tasks selbstständig aus einer Liste holt, diese bearbeitet und anschließend das Ergebnis der Bearbeitung an die Process Engine zurückgibt. Wir beschreiben die Anwendung dieses Patterns und diskutieren Vorteile, Nachteile und Einsatzszenarien.
External Task Worker bearbeiten Service Tasks
Das Kernstück eines jeden Prozessmodells sind seine Tasks bzw. Aktivitäten. Im folgenden Beispielprozess wollen wir einen hübschen Hut besorgen. Dies läuft bei uns vollautomatisiert ab und ist somit als Service Task modelliert.
Nutzt die Implementierung einer entsprechenden Prozessanwendung Camundas Process Engine, so steuert diese den Prozessablauf und kümmert sich insbesondere auch darum, dass die Service Tasks automatisiert erledigt werden. Das in der Praxis wohl häufigste Vorgehen dafür ist, der Engine eine Java-Klasse als Delegate zur Verfügung zu stellen, in der die Abarbeitung der Aktivität implementiert ist – in diesem Fall das Besorgen eines Huts, z.B. durch entsprechende REST-Aufrufe an das interne Hut-System. Hierzu implementiert das HutHolenDelegate das JavaDelegate-Interface.
JavaDelegate
public class HutHolenDelegate implements JavaDelegate { @Override public void execute(final DelegateExecution execution) { // Implementierung des Hutholens // Ergebnis im Prozesskontext ablegen execution.setVariable("hut", "Hübscher Hut"); } }
Wenn der Prozessfluss an dem Service Task ankommt, delegiert die Engine die entsprechende Aufgabe an das Delegate und führt hierzu die Implementierung der execute-Methode aus, erledigt dadurch den Service Task und fährt dann fort im Prozess. Die Aufgabenbearbeitung wird also direkt und synchron von der Process Engine ausgeführt und das Delegate ist eng an die Engine bzw. die Prozessanwendung gekoppelt.
Eine Alternative hierzu bietet die Implementierung mit einem External Task Worker. Dabei bearbeitet die Process Engine die Tasks nicht selbst mithilfe eines Delegates, sondern schreibt sie zunächst in eine Liste. Ein solcher Worker ist eine eigenständige Anwendung, die sich die Tasks per REST-Aufruf aus dieser Liste holt, sie selbstständig bearbeitet und dann das Ergebnis an die Engine zurückgibt, wie in der Grafik angedeutet.
Hier erfolgt die Bearbeitung der Aktivität also per Pull-Mechanismus und damit asynchron und weniger stark an die Engine gekoppelt. An dieser Stelle wollen wir kurz darauf hinweisen, dass die Kommunikation mit der Process Engine nicht zwingend per REST erfolgen muss: Es gibt auch die Möglichkeit, externe Worker als Teil der Applikation zu implementieren, in der die Process Engine läuft, und dann per Java-API mit dieser zu interagieren. Die Vorteile des External Task Worker-Patterns liegen aber bei der Kommunikation per REST klarer auf der Hand und wir werden uns im Folgenden hierauf konzentrieren.
Natürlich kann man die Grundlagen für die Interaktion mit der Process Engine (z. B. Kommunikation mit der REST-API, geeignete DTOs, Pull-Verhalten ...) selbst implementieren. Alternativ übernimmt diesen Overhead ein geeigneter Client, z. B. der Java-ExternalTaskClient von Camunda. Auch dann findet aber sämtliche Interaktion mit der Process Engine über deren REST-API statt. Nutzt unser Worker jetzt Camundas ExternalTaskClient für Java, so wird die tatsächliche Bearbeitung von Aufgaben in Form eines ExternalTaskHandlers implementiert, analog zu obigen JavaDelegates. Am Code ändert sich dann nur wenig, allerdings muss der ExternalTaskClient zuvor konfiguriert und initialisiert werden:
ExternalTaskHandler
public class HutHolenHandler implements ExternalTaskHandler { @Override public void execute(final ExternalTask externaltask, final ExternalTaskService externalTaskService) { // Implementierung des Hutholens analog zum Delegate // Ergebnis mittels ExternalTaskService zurückmelden und Task abschließen externalTaskService.complete(externaltask, Variables.createVariables().putValue("hut", "Hübscher Hut")); } }
Config
@Configuration public class Config { private final static long BACKOFF_INIT_TIME = 500L; private final static long BACKOFF_LOCK_FACTOR = 2L; private final static long BACKOFF_LOCK_MAX_TIME = 600L; private final static long WORKER_LOCK_DURATION = 2000L; private final static String WORKER_ID = "externalHutWorker"; private final static String WORKER_TOPIC = "HutHolen"; private final static String PROCESS_ENGINE_REST_URL = "http://localhost:8080/rest/"; private ExternalTaskClientBuilder externalTaskClientBuilder() { return new ExternalTaskClientBuilderImpl(); } private BackoffStrategy backoffStrategy() { return new ExponentialBackoffStrategy(BACKOFF_INIT_TIME, BACKOFF_LOCK_FACTOR, BACKOFF_LOCK_MAX_TIME); } private ExternalTaskClient externalTaskClient() { return this.externalTaskClientBuilder() .baseUrl(PROCESS_ENGINE_REST_URL) .backoffStrategy(this.backoffStrategy()) .lockDuration(WORKER_LOCK_DURATION) .workerId(WORKER_ID) .build(); } @PostConstruct public void registerHandler() { this.externalTaskClient() .subscribe(WORKER_TOPIC) .handler(new HutHolenHandler()) .open(); } }
Vorteile des External Task Workers
Warum aber sollte der Mehraufwand in Kauf genommen werden, der dadurch entsteht, dass die External Task Worker-Applikation sich selbst Arbeit besorgen muss, statt wie bisher die komplette Orchestrierung der Process Engine zu überlassen?
Die Verwendung von externen Workern bietet eine Reihe von Vorteilen gegenüber der Implementierung z.B. mit Java Delegates, die wir im Folgenden beleuchten wollen.
- Skalierbarkeit: Nicht alle Service Tasks lassen sich schnell erledigen und bei aufwendigen Aktivitäten kann die Abarbeitung mithilfe eines Java Delegates schnell zum Flaschenhals werden. Das gleiche Problem kann auftreten, wenn die Aktivität an sich zwar zügig zu erledigen ist, dafür aber häufig auftritt. Bei der Implementierung des Workers als separate Applikation ist es kein Problem, mehrere Instanzen davon hochzufahren und so die anfallende Arbeitslast zu bewältigen.
- Verteilte Systeme: Die wenigsten Prozesse werden heute auf einer einzigen Maschine abgewickelt. (Micro-)Services werden überall verwendet und setzen zwingend Kommunikation voraus. In unserem Beispiel könnte ein solcher Service das Holen von Hutinformationen aus dem Hutsystem realisieren. Ein solches System wird wahrscheinlich hinter einer Firewall betrieben. Nun arbeitet aber die Process Engine nicht unbedingt hinter derselben Firewall; vielleicht arbeiten wir on-site in getrennten Bereichen, oder wir haben den Betrieb der Process Engine in eine externe Cloud ausgelagert. Bei der direkten Bearbeitung durch die Process Engine müsste nun die Firewall so konfiguriert sein, dass Anfragen von und ggf. Antworten an die Engine durchgelassen werden. Können wir den External Task Worker innerhalb der Firewall betreiben, so entfällt ein Teil dieser Konfiguration und wir müssen nur die Kommunikation nach außen, zur Process Engine, freigeben. Insbesondere bei den immer häufiger anzutreffenden Hybrid-Cloud-Systemen (Verteilung auf mehrere Clouds, ggf. nicht alle bei demselben Anbieter) kann diese nur einseitige und ausgehende Kommunikation von Vorteil sein.
- Freie Technologiewahl: Die direkte Einbindung von Delegates funktioniert bei der Camunda Process Engine nur durch Java-Klassen. Da die Kommunikation mit dem Worker schlicht über REST funktioniert, kann man ihn im Wesentlichen in einer beliebigen, selbst gewählten Sprache programmieren. Insbesondere ist es so möglich, auch Teams, die sich z. B. eher mit C# oder anderen Sprachen als mit Java auskennen, mit der Implementierung von Workern zur Erledigung von Aktivitäten zu betrauen. Analog zu dem oben schon erwähnten ExternalTaskClient für Java gibt es mittlerweile eine Reihe weiterer Clients für diverse Sprachen, die die REST-Interaktion mit der Process Engine vereinfachen und es Entwicklern so ermöglichen, sich auf die wesentliche Bearbeitung des Tasks zu konzentrieren. Bisher werden Clients für Java und JavaScript von Camunda unterstützt, es gibt allerdings auch weitere, momentan (noch) nicht unterstützte Varianten für z. B. Python, Ruby oder .Net (siehe https://github.com/camunda/awesome-camunda-external-clients).
- Retry-Verhalten und Verfügbarkeit: Ein großer Vorteil beim Einsatz von Camunda ist das Verhalten in einer Fehlersituation. Scheitert die Durchführung einer Aktivität mittels Java Delegate, so versucht es die Process Engine erneut und folgt dabei dem konfigurierten Retry-Verhalten. Gerade wenn die Erledigung der Aktivität von Systemen abhängt, die nicht durchgehend erreichbar sind (z. B. weil nachts oder über das Wochenende Wartungsarbeiten oder Updates durchgeführt werden), führt dies oft zu wenig eleganten Lösungen. Zum Beispiel wird die Ausführung einer Aktivität mehrere Male in Abständen von 13 Stunden versucht, um die Nacht oder das Wochenende zu überbrücken. Dies führt zu einer Reihe von unnötigen Aufrufen an ein bekanntermaßen nicht erreichbares System und eine zutiefst enttäuschte Process Engine.
Betrauen wir hingegen einen externen Worker mit der Aktivität, so steuert dieser sein Retry-Verhalten selbst. Außerdem könnten wir ihn z. B. immer nur dann hochfahren, wenn wir davon ausgehen, dass seine benötigten Systeme auch verfügbar sind. Bis dahin sammeln sich die Aktivitäten in der Task List der Process Engine und werden alle abgearbeitet, wenn der Worker online ist. - Entkopplung: Bei der Implementierung mit Java Delegates übernimmt die Prozessanwendung nicht nur die Orchestrierung des Prozesses, sondern auch die Bearbeitung der Aktivitäten. Moderne Softwarearchitekturen sind modular aufgebaut, wobei die Module möglichst lose aneinandergekoppelt sind, um z. B. zu verhindern, dass Änderungen an einer einzigen Stelle Re-Deployments aller anderen Komponenten erfordern. Dem Ideal lose gekoppelter Services entspricht der External Task Worker-Ansatz deutlich eher als jener mit Java Delegates.
External Task Worker sind nicht in jedem Szenario die perfekte Lösung
Natürlich sind auch externe Worker kein Allheilmittel: Ist synchrone Kommunikation notwendig, zum Beispiel, um das Ergebnis einer Aktivität direkt in einem Webformular anzuzeigen, so ist der Worker ungeeignet, da seine Kommunikation per Design asynchron verläuft. Auch muss man bedenken, dass bei der Ersetzung von Java Delegates durch Worker die Anzahl der hochzufahrenden Applikationen steigt und durch die REST-Anfragen des Workers an die Process Engine mehr Kommunikation über das Netzwerk zu erwarten ist.
Um letzteres Problem zu reduzieren, bietet die REST-API der Process Engine eine hübsche Möglichkeit: Offensichtlich ist es ungünstig, wenn der Worker ständig bei der Engine nach Arbeit fragt und mit leeren Händen heimkehrt. Es bietet sich an, die Anfragen mit einer entsprechenden Backoff-Strategie zu fahren und immer seltener zu fragen, wenn keine Aktivitäten vorliegen. Dieses Vorgehen hat aber den Nachteil, dass der Worker ggf. sehr lange braucht, um zu reagieren, wenn dann doch einmal eine Aktivität reinkommt. Dieses Latenzproblem wird durch long polling reduziert: Hierbei teilt der Worker der Engine mit, dass er nicht unbedingt auf eine sofortige Beantwortung seiner REST-Anfrage angewiesen ist und gewillt ist, länger zu warten, falls gerade keine Arbeit anliegt. Hierdurch können die Reaktionszeiten des Workers deutlich verbessert werden, ohne dass mehr REST-Aufrufe notwendig wären. Dennoch wird auch mit dem long polling die Reaktionszeit eines Workers stets langsamer sein als die eines Java Delegates.
Fazit
External Task Worker entkoppeln durch einen Pull-Mechanismus die Bearbeitung von Tasks von der Prozess-Orchestrierung durch die Process Engine. In vielen Szenarien bietet dies eine Reihe von Vorteilen, es ist aber stets neu abzuwägen, ob eine solche asynchrone Bearbeitung für den jeweiligen Task geeignet ist. Zumindest die Implementierung von Workern ist bei der Verwendung der ExternalTaskClients kaum komplizierter als die Implementierung entsprechender Java Delegates. Somit steht der Verwendung der externen Arbeiter in allen möglichen denkbaren Einsatzszenarien nichts mehr im Wege. Eine Beispielimplementierung, um Java Delegates und externe Worker zu vergleichen, findet sich auf GitHub. Die Anwendung basiert auf Camundas Maven Archetype.
Mehr Dazu
Wie oben angedeutet kann das Retry- und Fehlerverhalten von External Task Workern explizit gesteuert werden. In einem zweiten Blogbeitrag erklären wir, warum man dies auf jeden Fall tun sollte, wie man vorgehen kann und was es hierbei zu beachten gilt.
zurück zur Blogübersicht