---
# System prepended metadata

title: Workflow-Engine

---

# Workflow-Engine

## Kurzbeschreibung

Implementierung einer Workflow-Engine:
Benutzer sollen einen Prozess oder einen Lebenszyklus definieren können, der dann durch die Workflow-Engine gestartet, überwacht und ausgeführt wird. Dazu gehören beispielsweise die Überprüfung  des aktuellen Zustands und die Autorisierung von Benutzern vor dem Ausführen einer Aktion und die Abarbeitung einer Aufgabe.

Die einzelnen Workflows bzw. die Aktionen werden über verschiedene Microservices verwaltet. Zur Implementierung der Microservices können dabei verschiedenste Programmiersprachen verwendet werden.


Wir Teilen unsere Dokumentation in zwei Bereiche auf. Zum einen in den theoretischen Teil, wo wir über den Design-Prozess und unsere ursprünglichen Überlegungen zu diesem Thema berichten. Zum anderen eine technischere  Seite, die unsere eingeschränkte Referenz-Implementierung darstellt.
Am Ende gibt es dann noch eine Übersicht über einen angewanden Use-Case.


## Glossar der Fachbegriffe

| **English** | **Definition**                                               |
| ----------- | ------------------------------------------------------------ |
| workflow    | Der *Workflow* ist die Abbildung eines gesamten Prozesses im System. Über eine Konfiguration kann dieser definiert werden. Er kann sowohl mehrere Start-, als auch Endpunkte besitzen. |
| task        | Der *Task* beschreibt eine einzelne Aufgabe in einem Workflow. Dieser umfasst jeweils mindestens die Punkte Trigger, Action und Output und liegt im Core-Modul der Applikation. Hier wird unter anderem die Ressource beschrieben, die eine Aufgabe abgearbeitet und schickt beim Starten eine Nachricht zu dieser. |
| parameter   | Im Bereich *Parameter* bei der Task-Definition können ein oder mehrere Parameter angegeben werden, die Daten bei der Ausführung der Aufgabe mitgeben. |
| trigger     | Im Bereich *Trigger* werden jene Event-IDs angegeben, die einen Task instanzieren.  Es können mehrere Schlüssel angegeben werden, wobei beim Auftreten eines Schlüssels der Task ausgelöst wird. |
| action      | In der *Action* ist die Aktion eines Task definiert, also die Adresse jener Ressource, die dann tatsächlich ausgeführt werden soll. Wir schränken die Definition auf jeweils eine Aktion pro Task ein, da über mehrere Tasks eine ähnliche Logik abgebildet werden kann. |
| output      | Im *Output* liegt die Definition der Rückgabewerte einer Ressource. |
| token       | Das Token definiert jene Stelle, an der sich der Workflow im Moment befindet. |

## Theorie

### Beispiel (Book Request)

![](docs/img/book_request.png)

Aufteilung der Tasks im Sinne unserer Definitionen:

![](docs/img/book_request_update.png)

Die grundlegende Idee dieser Workflow-Engine ist absolut Event-orientiert, im Gegensatz zu geschäftsprozess-orientierten Anwendungen. Damit müssen Zustände zum Teil etwas anders angegangen werden.

## Taskdefinition (Workflow Definition Language)

```json
{
    "parameters": {},
    "triggers": {},
    "actions": {},
    "outputs": {},
}
```

## Parameters

Das wichtigste im Bereich der Parameter ist natürlich, dass diese nicht von der jeweiligen Programmiersprache abhängig sein dürfen. Das Ziel unseres Ansatzes ist es, eine absolut polyglote Engine zu entwickeln. Wenn eine Programmiersprachen mit den wichtigsten Protokollen umgehen kann (z.B.: HTTP), dann sollten Microservices in dieser Sprache implementierbar sein.

## Triggers

Triggers sind jene Schlüssel, durch die ein Task ausgeführt wird. Bei uns gibt es zwei verschiedene Arten davon:

- Tag Trigger
- timebased Trigger

Ein Tag Trigger zeichnet sich durch eine einfache Zeichenkette aus. Kommt ein Event mit jener Zeichenkette als Tag bei der Core-Komponente an, so wird der Task ausgelöst.

Ein timebased Trigger löst nach gewissen Zeitpunkten aus. Möchte man beispielsweise einen Task nach `x`-Tagen oder nach 5 Sekunden auslösen ist das die richtige Wahl. Gerade um Ablaufdatum oder ähnliche Situationen zu modellieren, wird dieser Trigger benötigt.
Im konkreten Fall kann man folgendes definieren:

- Startzeit
- Endzeit
- Intervall
- maximale Anzahl an Auslösungen

### Unterscheidung zwischen *active* und *passive*

*Active Triggers* senden eine Nachricht, auf die man direkt hören kann (Subscriber-Prinzip).

*Passive Triggers* hingegen sollen bei einem bestimmten Zustand auslösen. Das kann entweder durch einen *Active Trigger* ersetzt werden (Beispielsweise in einem Endpoint beim Persisitieren eines Objektes direkt auslösen) oder man müsste selbständig in einem Intervall diesen Zustand pullen.


Da der erste Fall nicht universal umsetzbar ist und der zweite eine deutliche Erhöhung der Komplexität im Core-Modul erfordert, haben wir uns für eine alternative Umsetzung entschieden. Diese Art der Zustandsüberprüfung kann entweder in der Ressource selbst durchgeführt werden oder man zieht einfach einen weiteren Helper-Task heran, der diese Aufgabe übernimmt.


### Tigger-Logik

Ein Task enthält einen oder mehrere Tigger. Sobald einer feuert, wird die Aktion ausgeführt.

### Abbildung der BPMN-Gateways

* **AND** (Parallel Gateways): Der gleiche Trigger wird für unterschiedliche Tasks verwendet. Dadurch werden mehrere Aktionen bei einem Event gestartet.
* **OR** (Inclusive Gateways): Für einen Task werden mehrere Trigger definiert. Dadurch kann jeder dieser Trigger die Aktion auslösen.
* **XOR** (Exclusive Gateways): Es wird eine Helper-Task eingezogen, der die Aufgabe der Entscheidung des weiteren Vorgehens übernimmt. Genauso wird für **Conditional Gateways** vorgegangen.
* **Event-based Gateways**: Dieses Gateway blockiert den Prozess so lange, bis des erste Folge-Event auftritt. Dafür wird es eine eigene Logik im Core-Modul geben, die temporär einen eigenen Task anlegen kann. Eine genauere Beschreibung dessen wird weiter unten folgen.

## Actions

Die Art der Aktionen kann beliebig erweitert werden. In unserem Fall haben wird uns beispielsweise auf die `ApiConnection` konzentriert, die eine Addresse/URI deklariert, um die Ressource auszuzeichnen. Damit kann schon vieles gut abgebildet werden.
Eine weitere Implementierung einer `DebugAction` haben wir ebenfalls im Einsatz, die einfach Daten in der Konsole ausgibt.

## Outputs und Inputs

Bezüglich Inputs und Outputs haben wir das in der Implementierung so gehanndhabt, dass wir unniversale Payloads eines Outputs als Input für die nächste Aktion weitergeben.

Eine bessere Methodik wäre es, in der Konfigurationsdatei selbst definieren zu können, welche Daten man als Inputs für den Service benötigt und welche mach zurückgibt. Dazwischen kann man einen Store senden, der jene Möglichkeiten umsetzt.

## Architektur

![](doc/img/meeting1_01.png)

## Implementierung

### Übersicht über die  Komponenten

![](doc/img/components.png)

#### WorkflowCore

Der **WorkflowCore** ist die Kernkomponente der Anwendung. Diese erstellt einen Consumer für die RabbitMQ-Queue: `event_queue`. 
Damit werden alle versendeten Events direkt von dieser Komponente behandelt. Die Events aus der Queue liegen im JSON-Format for und müssen erst in die äquivalenten C#-Klassen geparsed werden.
Wenn das Event nun als C#-Objekt vorhanden ist, wird für jeden gespeicherten Task überprüft ob einer der dazugehörigen Trigger durch das Event ausgelöst wird. Wenn ein Trigger ausgelöst wird, werden alle Aktionen des Tasks ausgeführt.
Dabei gibt es zwei verschiedene Arten von Aktionen: Aktionen die direkt von der Core-Komponente ausgeführt werden und welche die zu lange dauern und deswegen delegiert werden.
Ein Beispiel für eine direkt ausführbare Aktion wäre die `DebugAction`, welche eine gewünschte Nachricht als Konsolenausgabe ausgibt.

Wenn eine Aktion die Kernkomponente länger blockieren würde (z.B. Api-Request), wird diese in eine Rabbit-MQ Queue gegeben wo diese dann von der `ActionExecution` Anwendung abgearbeitet wird.

In unserem Fall haben wir eine `ApiConnectionAction` implementiert welche einen HTTP-Post-Request an eine beliebige Adresse versendet.

#### ActionExecution

Diese Anwendung wird als separate Konsolenapplikation entwickelt, da diese komplett unabhängig von den anderen Teilen ist.
Die auszuführenden Aktionen werden von einer speziellen Action-Queue aus dem RabbitMQ-Broker geladen.
Von jeder eingetroffenen Message wird die Adresse und eine etwaige Payload geparsed um anhand dieser Daten dann einen POST-Request zusammenzubauen.

#### GitService

Der Git-Service wird verwendet, um einen einfachen Git-Workflow abzubilden.

Dabei wird in einem leeeren Ordner ein neues Git-Repository mit `git init` erstellt. Anschließend wird über die [gitignore.io-API](https://gitignore.io/) der Inhalt einer `.gitignore`-Datei geholt, dieseer in eine Datei geschrieben und anschließend zum Repository-Index hinzugefügt und committed.
Als letzten Punkt kann man noch die bisherigen Commit (vgl. `git log`) auslesen.

Diese Logik ist mithilfe von folgenden Bibliotheken implementiert worden:
- [Express](https://expressjs.com/)
- [fs](https://nodejs.org/api/fs.html)
- [amqplib](https://www.google.com/search?client=firefox-b-d&q=amqplib)
- [node-fetch](https://www.google.com/search?client=firefox-b-d&q=node-fetch)
- [simple-git](https://github.com/steveukx/git-js)

Der Express-Server bietet folgende drei Post-Endpunkte:
- `init-repo`
- `add-gitignore`
- `log`

Definition des dazugehörigen Git-Workflows:

```json
[
    {
        "$schema": "./task-definition-schema.json",
        "name": "InitRepositoryCommand",
        "description": "Send command to init new repository for given path",
        "actions": {
            "init-repo": {
                "type": "ApiConnection",
                "address": "http://localhost:8080/init-repo"
            }
        },
        "triggers": {
            "default": {
                "tag": "init-repo"
            }
        }
    },
    {
        "$schema": "./task-definition-schema.json",
        "name": "AddGitignoreFile",
        "description": "Define .gitignore file in repository and commit it",
        "actions": {
            "add-gitignore": {
                "type": "ApiConnection",
                "address": "http://localhost:8080/add-gitignore"
            }
        },
        "triggers": {
            "default": {
                "tag": "add-gitignore"
            }
        }
    },
    {
        "$schema": "./task-definition-schema.json",
        "name": "GetGitLog",
        "description": "Returns console output from git log command",
        "actions": {
            "log": {
                "type": "ApiConnection",
                "address": "http://localhost:8080/log"
            }
        },
        "triggers": {
            "default": {
                "tag": "log"
            }
        }
    }
]

```

### Konfiguration

Wie  man im vorigen Bereich sehen kann, wird ein Workflow über eine Json-Datei definiert. Beim Starten der Applikation wird die Datei eingelesen, mitteels `Json.Schema` validiert und ein neues WorkflowCore-Objekt daraus erstellt.


## Use-Case

Git-Service:

![git-workflow](doc/img/successful-git-workflow.png)

Logging:

![logging-workflow](doc/img/succesful-log-workflow.png)

## Quellen

### Links

- [Workflow-Management-System](https://de.wikipedia.org/wiki/Workflow-Management-System)
- [Workflow engine](https://en.wikipedia.org/wiki/Workflow_engine)
- [(Azure) Workflow Definition Language Schema - Part 1](https://docs.microsoft.com/en-us/azure/logic-apps/quickstart-create-logic-apps-visual-studio-code)
- [(Azure) Workflow Definition Language Schema - Part 2](https://docs.microsoft.com/en-us/azure/logic-apps/logic-apps-workflow-definition-language)
- [Implementing a distributed Workflow Engine using RabbitMQ](https://blog.noser.com/distributed-workflow-engine-using-rabbitmq/)
-  [Let’s make workflow engines fun again](https://medium.com/@arik.cohen/lets-make-workflow-engines-fun-again-73c4ad5eb428)
- [awesome-workflow-engines](https://github.com/meirwah/awesome-workflow-engines)
- [Architecture options to run a workflow engine](https://blog.bernd-ruecker.com/architecture-options-to-run-a-workflow-engine-6c2419902d91)

### Referenzen

- UML Aktivitätsdiagramm
- BPMN
- Json Schema

## Eingesetzte Technologien

- Core-Komponenten als C#-Konsolenapplikation
- RabbitMQ
- ReST
- Node/NPM für Git-Operationen als Microservice
- JSON Schema