Workflows

Ein Unterbaustein einer Geschäftsprozesskomponente sind die Workflows. Ein typischer Workflow in Schleupen.CS besteht aus einer Reihe von Schritten, die in einer Workflow-Engine koordiniert werden und von angebundenen Bausteinen ausgeführt werden. Im allgemeinen ergeben Workflows, Business Events, Aufgaben, Dialogabläufe und natürlich Services einen Geschäftsprozess in der Schleupen-Terminologie.

Workflows sind also strukturierte Abläufe von Aufgaben oder Prozessen, die genutzt werden, um komplexe Abläufe zu orchestrieren und die Interaktion zwischen verschiedenen Komponenten zu steuern. Dabei kommt es häufig vor, dass der Ablauf von Workflows unterbrochen wird, weil der entsprechende Workflow auf Ereignisse von außen wartet. Während dieser Wartezeit können Workflows automatisch persistiert und aus dem Speicher entfernt werden, so dass sie die Systemressourcen nicht unnötig belasten. Beim Eintreffen von passenden Ereignissen werden die zugehörigen Workflows dann wieder reaktiviert (der letzten Zwischenzustand aus dem Persistenzmedium gelesen) und weiter ab der vorher unterbrochenen Stelle fortgeführt. Workflows stellen also langlaufende Prozesse dar, die persistiert werden können und nur vergleichsweise kurze, aktive Laufzeiten haben. Sie können Ereignisse vom System empfangen und darauf reagieren, was es ihnen ermöglicht, auf Änderungen in der Umgebung oder auf bestimmte Bedingungen zu reagieren. Workflows können auch Gateways (im Sinne der BPMN) enthalten, die den Ablauf anhand von Bedingungen steuern.

Workflows werden mithilfe des Web-Tools WebModeler in BPMN modelliert und um Informationen wie WSDLs und Mapping-Code angereichert, so dass diese ausführbar sind. Gehostet werden Workflows über ein Executable, das pro Workflow-Typ ein Message-Consumer des Message-Bus ist.

Workflows sind ein Teilbereich von GP-Komponenten

Entsprechend sind Workflows derart konzipiert, dass diese analog zu Services möglichst autonom sind. Dementsprechend haben diese ein eigenes DB-Schema wie Länder auch, in dem insbesondere Informationen für die Workflows abgelegt sind. Dieses DB-Schema wird, genauso wie bei Ländern auch, implizit über die Zuordnung von Serviceimplementierungsgruppen in der Systemstruktur einer Datenbank unter einem Systemstrukturelement zugeordnet.

Workflows können

  • über den BrokerService, wie in Kommunikationsarten beschrieben, mithilfe der Angabe der ServiceId des Process Service und
  • über das Auslösen eines Events, das der Process Service (= implementiert als Workflow) abonniert (Subscription, siehe Gateways und Resilienz),

gestartet werden.

Eine GP-Komponente besitzt folgende Arten von Klassen

  • Workflow - eine mittels BPMN generierte Saga in C#
  • Assembler - übersetzt Datenverträge eines oder mehrerer Service-Responses in einen Service-Request

Workflow-Start

Der Start eines Workflows erfolgt durch einen Service-Aufruf des Brokers über den Endpunkt http://localhost/Schleupen/Service Bus/Broker/BrokerService.svc mit Angabe einer ServiceId (Schritt 1 in nachfolgender Abbildung). Der Broker erkennt dann mithilfe der Service-Registry, dass diese Nachricht in die Queue der Ziel-GP-Komponente im zweiten Schritt eingestellt werden soll.

Schritte beim Direkt-Start eines Workflows

Die GP-Komponente wird über den Prozess "WorkflowHostCli.exe" gehostet und fungiert als Message-Consumer an der Queue, die der GP-Komponente gehört. Dieser Message-Consumer ist ein Standard-Mechanismus von MassTransit (Consumers | MassTransit (masstransit-project.com))

IBusControl busControl = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
    cfg.Host(new Uri(RabbitMqConstants.Endpoints.HostAddress), "<a workflow type>", h =>
    {
        ...
    });

    cfg.ReceiveEndpoint("<a workflow type>", e =>
    {
        ...
    });
    ...
});

und startet den Workflow. Die Implementierung ist als Sagas | MassTransit (masstransit-project.com) mit MassTransit implementiert und der dazugehörige Code hierzu wird, wie unten beschrieben, generiert.

Der Workflow selber ist eine State-Machine, die per Events an RabbitMQ angeschlossen ist (siehe unten). Nach dem Auslösen eines Requests um den Ziel-"Service" aufzurufen, wird ein Statuswechsel ausgeführt, und auf ein Reply gewartet. Jeder Reply wird im Schema der GP-Komponente für die aktuell ausgeführte Instanz persistiert, so dass an diesen Stellen Workflow-typisch wieder aufgesetzt werden kann. Auf diese Weise kann eine Serie von "Services" in Serie, d.h. orchestrierend, ausgeführt werden.

Workflows können auch über Events gestartet werden, indem diese als Event-Subscriber fungieren. Hierzu stellt, wie in der folgenden Abbildung dargestellt, ein Message-Client (z.B. über eine Transactional-Outbox) in die Ziel-Queue der GP-Komponente ein.

Schritte beim Event-Start eines Workflows

Die nachfolgende Verarbeitung erfolgt dann wie in obigem Fall des Aufrufs per Service.

Ausführung von Workflows

Die Funktionsweise bei der Ausführung eines Workflows ist wie folgt, wobei ein per Request-Reply angeschlossener Service betrachtet wird:

Aufrufe eines Service aus einem Workflow
  1. MassTransit setzt einen Request ab, persistiert diesen und legt diesen als Message in der Ziel-Queue des Landes ab.
  2. Der Business Event Dispatcher liest diese Nachricht aus der Queue und ruft unter Zuhilfenahme der Service Registry den Ziel-Service auf. Dieser Ziel-Service fungiert als Event-Subscriber-Service und ist wie jeder andere Service auch codiert.
  3. Nach Ausführung der eigentlichen Applikationslogik in dem Land wird eine Antwortnachricht (der Service-Response) als Reply in die Queue der GP-Komponente eingestellt.
  4. Ein Message-Consumer der GP-Komponente holt die Nachricht aus der Queue geht in den nächsten Status über und erzeugt einen neuen Request der persistiert und abgesetzt wird.

In der "neuen" Workflow-Lösung auf Basis von MassTransit werden alle Services per Request-Reply angeschlossen.

In Schleupen.CS werden sogenannte MassTransits zur Umsetzung der Sagas vewendet: "A saga is a long-lived transaction managed by a coordinator. Sagas are initiated by an event, sagas orchestrate events, and sagas maintain the state of the overall transaction. Sagas are designed to manage the complexity of a distributed transaction without locking and immediate consistency. They manage state and track any compensations required if a partial failure occurs." (Quelle: Sagas | MassTransit (masstransit-project.com)).

Das für Workflows benötigte DB-Schema wird per Namenskonvention automatisch generiert: Heißt die GP-Komponente Schleupen.CS.mal.bal, so wird das Schema mal_bal_wf samt der relevanten Tabellen zur Laufzeit erstellt.

Modellierung

Workflows werden in Schleupen CS 3.0 durch in BPMN modellierte Prozesse generiert. Hierzu wird mit dem Schlepuen.CS-Tool WebModeler das BPMN-Modell erstellt und dabei Services angebunden, Mapping-Code erstellt usw. 

Modellierung eines Workflows mit dem WebModeler

Aktuell werden folgende Sprachelemente unterstützt:

  • Signal Start-Event
  • Untypisiertes Start-Event
  • Exklusives Gateway
  • Event-basiertes Gateway
  • Benutzer-Task
  • Skript-Task
  • Service-Task
  • Zeit-Zwischenereignis
  • Signal-Zwischenereignis (abfangend)
  • Signal-Zwischenereignis (auslösend)
  • Angeheftetes Zeitzwischenereignis (an Benutzer / Service-Task)
  • Angeheftetes Signalzwischenereignis (an Benutzer / Service-Task)
  • Angeheftete Fehlerzwischeneignis (an Benutzer / Service-Task)
  • Signal-End-Ereignis
  • Untypisiertes End-Ereignis
  • Datenobjekt
  • Dateninput
  • Datenoutput

Zum Teil der Modellierung gehören ferner:

  • Erzeugen von Aufgaben
  • Anschluss des Geschäftsprozessprotokolls
  • weitere Konfiguration für die Laufzeit

In speziellen Spezifikationsdialogen werden die Tasks konfiguriert. Das folgende Beispiel zeigt dies für ein Service-Task:

Anschluss eines Service per Request-Reply

Service-Tasks rufen wie beschrieben Services per Request-Reply (unter Zuhilfenahme des Business Event Dispatchers) auf - daher sollte der Haken "Request-Reply Service" gesetzt sein, um dies für MassTransit-Workflows zu dokumentieren.

Der entsprechende Haken in der vorstehenden Benutzeroberfläche dient der "alten" Workflow-Generierung!

Damit wird dann während der Installation alles notwendige an Infrastruktur wie beispielsweise Queues und Einträge in der Service-Registry erstellt.

Workflow Einstellungen

Im Werkflow selbst können noch verschiedene Einstellungen vorgenommen werden. Der Knopf für die Einstellungen lässt sich oben rechts bei der Verwaltung finden. Hier kann zum einen eine Serviceimplementierungsgruppe (SIG) vergeben werden und zm anderen die Deploymentrolle des Workflows ausgewählt werden.

Deploymentrollen

Zur Auswahl stehen zwei Arten von Deploymentrollen, WorkflowInteractiveServer und WorkflowBackendServer. Anhand dieser Rollen wird entschieden auf welchen Servern der Workflow installiert und ausgeführt wird. Welcher Server, welche Rolle besitzt kann mithilfe des PowerShell CmdLets Select-DeploymentRole überprüft werden.

Die Auswahl der richtigen Rolle ist dem Entwickler des Workflows überlassen. Im Standard sollte immer die Rolle "WorkflowBackendServer" gewählt werden.

Generierung und Codierung

Aus diesem BPMN-Modell wird dann C#-Code generiert. Dabei wird insbesondere eine MassTransit-State-Machine generiert, die nach dem Deployment innerhalb des Hostings instanziiert werden kann.

Schritte von Modellierung zur Bereitstellung von WFs einer GP-Komponente

Innerhalb der State-Machine wird das Verhalten und die Reaktionen auf Zustände und Ereignisse mithilfe eines MassTransit-APIs in einer Fluent-API deklariert. Dabei werden Zustände (State), Signale (Events) und die möglichen Zustandsübergänge definiert. Diese bilden durch die Verknüpfung von Signalen und Bedingungen die Abläufe des Workflows ab.

Der folgende Code stellt in vereinfachter Form den generierten Code dar.

public sealed class BuchBestellungStateMachine : MassTransitStateMachine<BuchBestellenState>
{
  public BuchBestellungStateMachine()
  {
    InstanceState(x => x.State);
    DeclareRequests();
    DeclareEvents();
    DeclareSchedules();
    Initially(When(BuecherAusleihen)
      .Then(x =>
      {
        x.Instance.Buecher = x.Data.Buecher;
      })
      .Request(BuecherAusleihen, CreateBuecherAusleihenRequestAsync)
      .TransitionTo(Genehmigen.Pending));
    During(Genehmigen, When(BuecherAusleihenGenehmigt)
      .Then(x =>
      {
        x.Instance.Buecher = x.Data.Buecher;
      })
      .Request(...)
      .TransitionTo(AbgeschlossenState.Pending));
    ...
  }
  ...
}

Der initiale Zustand wird im Initially() hergestellt. During() wird für Zustandsübergänge verwendet, wenn sich der Workflow in einem bestimmten Zustand befindet nachdem eine Workflow-Instanz in der Datenbank angelegt worden ist. Zustandsübergänge erfolgen stets nachdem eine Message empfangen worden ist und werden im Code über TransitionTo() definiert.

Daten werden in der jeweiligen Instanz des Workflows über Propertys gekapselt - hier in BuchBestellungStateMachine - und sind somit in den Übergängen und Aktivitäten zur Verarbeitung verfügbar. Weiterhin wird die Instanz über die Mechanismen von MassTransit serialisiert und persistiert. Hier enthaltene Daten sind also auch bei Wiederanlauf eines Workflows verfügbar. Dies gilt beispielsweise auch für Requests, die eine Nachricht mit Nutzdaten abschicken und eine dazu korrelierte Antwort erwarten. Die dazu benötigten States, Events und Daten werden in der Klasse gekapselt.

Die Request- und Reply-Typen der angeschlossenen Services werden durch Einbindung von WSDLs mithilfe des WebModelers als C#-Code generiert, sodass der Glue-Code für das Mapping der Datentypen von Serviceaufruf zu Serviceaufruf in Form von Assemblern einfach implementiert werden kann. Für die Implementierung werden dazu im WebModeler sogenannte DataObjects modelliert, insbesondere in Form von Requests und Replys von Service-Aufrufen.

Sind Service-Tasks im BPMN-Modell vorhanden, so müssen die Requests für die einzelnen Service-Aufrufe erstellt werden. Dies erfolgt über andere DataObjects die als Request dem Service mitgegeben werden. Diese kann man in der Abbildung anhand des gestrichelten Pfeils zu einem Service-Task erkennen. Ein eingehendes DataObject kann durch Replys vorheriger Service-Aufrufe erstellt werden, indem von diesen die notwendigen Informationen selektiert werden. Der dazu benötigte Code wird als Mapping-Code in C# für die zu erstellenden Requests codiert.

Für die Implementierung von Assembler gelten folgende Konventionen:

Pro anzubindenem Service wird ein Assembler erstellt.
Namenskonvention: <Name des Service ohne Servicekategorie>Assembler
Beispiel: Augerufen werden soll der BuecherAusleihenActivityService. Dann wird der Assembler BuecherAusleihenRequestAssembler genannt.

Dementsprechend sollte natürlich die Aktivität Bücher ausleihen lauten.

public class BuecherAusleihenRequestAssembler
{
  public AusleihenRequest ToAusleihenRequest(StartRequest startRequest)
  {
    if (startRequest == null) { throw new ArgumentNullException("startRequest"); }
    var ausleihenRequest = new AusleihenRequest();
    ausleihenRequest.SessionToken = startRequest.SessionToken;
    ausleihenRequest.Buecher = ToBuchListe(startRequest.BuchListe);
    return ausleihenRequest;
  }
  private List<Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService.V3_3.BuchContract>
    ToBuchListe(List<Schleupen.AS.mal.bal.bap.BuecherAusleihenProcessService.V3_1.BuchContract> startRequestBuchListe)
  {
    if (startRequestBuchListe == null)
    {
      return new List<Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService.V3_3.BuchContract>();
    }
    var buchListe =
      new List<Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService.V3_3.BuchContract>();
    foreach (Schleupen.AS.mal.bal.bap.BuecherAusleihenProcessService.V3_1.BuchContract buchContract in startRequestBuchListe)
    {
      buchListe.Add(ToBuch(buchContract));
    }
    return buchListe;
  }
  private Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService.V3_3.BuchContract
    ToBuch(Schleupen.AS.mal.bal.bap.BuecherAusleihenProcessService.V3_1.BuchContract startRequestBuch)
  {
    if (startRequestBuch == null) {  throw new ArgumentNullException("startRequestBuch"); }
    var resultBuch = new Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService.V3_3.BuchContract();
    resultBuch.Id = startRequestBuch.Id;
    return resultBuch;
  }
}

Intermediate-Event-Subscriptions

Unter dem Begriff Intermediate-Event-Subscriptions verstehen wir Zwischen-Events innerhalb eines Workflows. Diese sind aus folgendem Grund besonders zu beachten:

Gegeben sei folgender Workflow ('Buchbestellung'), der in der Geschäftsprozesskomponente mal.bal implementiert sei:

Workflow mit Zwischen-Event

In dem Prozess sei über die Service-Aktivität ein Service des Landes VWT.EIN (Verwaltung.Einkauf - ein Baustein des Architekturbeispiels Bibliotheksverwaltung) angeschlossen, das automatisiert die Bestellung genehmigt oder nicht genehmigt. Das Ergebnis wird über jeweils ein Event propagiert, die beide in obigem Prozess konsumiert werden. Dabei wartet der Prozess also bei der Ausführung (Laufzeit!) einer Instanz auf das Event 'Bestellung genehmigt'. Wenn nun mehrere Instanzen laufen, muss eine Zuordnung derart erfolgen, dass nicht jede Workflow-Instanz jedes Event konsumiert. Man benötigt also eine Korrelation der aufrufenden Instanz zu dem zu empfangenden Event.

Die obige Modellierung kann im allgemeinen zu Race-Conditions führen, da das konsumieren als separater Schritt nach dem Serviceaufruf modelliert ist. Dies lässt sich lösen, indem ein angeheftetes Zwischenereignis modelliert wird, an dem eine Korrelation mithilfe eines Spezifikationsdialogs spezifiziert wird.

Modellierung ohne etwaige Race-Conditions

ComponentTests

Zum Erstellen von ComponentTest für Workflows wird eine Bibliothek bereitgestellt. Diese bietet die Möglichkeit, eine Workflow-Instanz im Code zu starten und dessen Ablauf zu kontrollieren. Mit ihr werden unter anderem verschiedene Methoden zum Prüfen des aktuellen Status, sowie die Kontrolle der durchgeführten Schritte, mitgeliefert. Dabei ist zu beachten, dass nur der Ablauf des Workflows, sowie der Mapping-Code getestet wird. Die aufgerufenen Services werden gemockt.

Der Vorteil hierbei ist, dass die gesamte Workflow-Instanz in-Memory ausgeführt wird. Dadurch wird kein komplett eingerichtetes System benötigt, sondern nur die Abhängigkeiten innerhalb der Visual Studio Solution. Des weiteren bedeutet dies, dass der Workflow, sowie sein Mapping-Code isoliert von den restlichen Komponenten getestet werden kann.

Cookie Consent mit Real Cookie Banner