Table of Contents

Befehlseindeutigkeit

In einem schwach zusammenhängenden System1), das asynchron kommuniziert, ist es äußerst wichtig, dass Befehle idempotent sind. Wenn ein Prozess einen Befehl absendet, muss er die Gewissheit haben, dass der Befehl schließlich2) ankommt, entweder indem er den Empfänger abfragen kann oder den Befehl einfach nochmal sendet mit der Gewissheit, dass er nur einmal ausgeführt wird. Das Thema der Befehlseindeutigkeit ist somit ein sehr wichtiges3).

Befehle können mehrmals beim Command-Handler ankommen. Das liegt einmal an der eingesetzten Bus-Architektur, die in diesem Fall at-least-once-delivery für Nachrichten garantiert. Das kann aber auch daran liegen weil ein Prozess4) nach einem unerwarteten Abbruch den Befehl ein zweites mal sendet.

Ereignisse sind auf natürliche Art idempotent, weil sie immer eine Aggregate ID und eine Version haben5). Event Handler sollten also bei nicht idempotenten Ereignisinhalten auf die ID/Version prüfen6).

Befehle sind nicht auf natürliche Art idempotent7). Auch wenn ein Befehl für eine Aggregate ID und Version gesendet wurde, bedeutet die Kombination [Befehlstyp|Aggregate ID|Version] nicht, dass der Befehl ein und derselbe ist. Ereignisse werden von dem Aggregate erzeugt, da ist das immer der Fall. Befehle können von Anwendern, Sagas und Event Handlern erzeugt werden. Es besteht also die Möglichkeit, dass die Kombination von Typ/ID/Version von mehreren Akteuren verwendet werden kann, und der zweite Befehl mit diesen Werten sollte als Konflikt abgelehnt werden und nicht als bereits erhalten.

Ziel der Befehlseindeutigkeit

Befehle müssen eindeutig gemacht werden, damit sie in einem System, das sie garantiert schließlich liefert, sie dabei aber eventuell mehrmals liefert, nur genau einmal ausgeführt werden. Somit muss der Command Handler8) sich merken, welche Befehl er bereits ausgeführt hat, und wenn er einen Befehl ein zweites Mal erhält, kann er prüfen, ob er bereits ausgeführt wurde oder nicht.

Über eine Befehls-ID9) kann man einen Teil der Abmachung garantieren: Ein Befehl wird niemals mehrmals ausgeführt, also maximal einmal. Eine Saga kann also denselben Befehl eine Million mal abschicken, er wird nur einmal ausgeführt10). Was aber nicht garantiert werden kann, ist das ein Befehl mindestens einmal ausgeführt wird.

Es besteht die Gefahr, dass ein neuer Befehl vom Command Handler als bereits erhalten angesehen wird, weil er mit einem vorherigen identisch ist, obwohl es sich eigentlich um einen neuen Befehl handelt.

Diese Gefahr muss beseitigt werden, vor allem für Prozesse11) und Event Handler, die verantwortlich dafür sind, schließliche Konsistenz herbei zu führen. Würden Befehle nicht durchgeführt werden, kann diese schließliche Konsistenz nicht mehr garantiert werden. Die Gefahr wird dadurch beseitigt, indem man den Befehl global12) eindeutig macht.

Wie mache ich den Befehl eindeutig

Es scheint, als wäre eine GUID als Idempotenz-ID für Befehle für viele ausreichend. Es wird sich darauf verlassen, dass die GUID global eindeutig ist. Wenn die GUIDs nach einiger Zeit auch noch verfallen, ist die Wahrscheinlichkeit so gering, dass dieselbe GUID für zwei unterschiedliche Befehle verwendet wird, dass es unwirtschaftlich ist, komplexere Lösungen zu bauen.

Ich meine, man kann komplexere Lösungen bauen, die kaum etwas zusätzliches kosten, das System aber noch robuster machen13). Für mich gilt somit erstens

Ein Befehl wird nicht nur über die ID identifiziert, sondern auch über den authentifizierten Absender. Es wird dann darauf vertraut, dass der Absender darauf achtet, dass seine Befehle eine eindeutige ID erhalten. Da die Idempotenz-ID eine Kombination aus dem Absender und der vom Absender erzeugten ID ist, wäre der Befehl global eindeutig.

Wer ist verantwortlich

Wenn ich als Client eindeutige Befehle versende, möchte ich davon ausgehen können, dass sie auch als solche erkannt werden. Ich kann mich dabei nicht darauf verlassen, dass sich alle Clients korrekt verhalten, vor allem dann nicht, wenn die Clients nicht alle aus einer Hand kommen. Daher ist der Server dafür verantwortlich, das Verhalten der Clients zu überprüfen. Dafür muss der Server dem Client vertrauen können. Die erste Regel ist somit

Der Server akzeptiert idempotente Befehle nur von Clients, die vom Server identifiziert werden können.

Das macht man entweder, indem sich der Client gegen den Server authentifiziert, z.B. über ein Zertifikat. Das klappt aber nicht, wenn die Befehle über einen Bus versendet werden. Alternativ kann man das über die mit gesendeten Authentifizierungsdaten des Users machen. Wenn man sicher sein kann, dass nur ein bestimmter Client die Anmeldedaten eines bestimmten Users kennt, dann kann sich der Client darüber dem Server gegenüber identifizieren. Eine Saga kann z.B. ein bestimmtes Systemkonto verwenden, dessen Anmeldedaten nur der Saga bekannt sind. Kommt ein Befehl mit den Anmeldedaten dieses Kontos an, kann der Server davon ausgehen, dass der Befehl von der Saga kommt.

Wenn diverse Clients gebaut werden, und das von unterschiedlichen, unabhängigen Firmen, kann es durchaus sein, dass der Algorithmus für die Erzeugung der IDs, die die Firmen für die Befehlseindeutigkeit verwenden, mehrfach verwendet wird. Zwei Clients könnten eine identische ID erzeugen. Auch wenn der Server den beiden vertraut, kann es so zu unbeabsichtigten Konflikten kommen. Es reicht also nicht aus, dass der Server nur idempotente Befehle von authentifizierten Clients akzeptiert. Es gilt zusätzlich

Damit die Firmen ihre Algorithmen nicht koordinieren müssen, muss ein Teil der Idempotenz-ID die Identität des Absenders enthalten, gemeinsam mit einer Kennung der verwendeten Authentifizierungsmethode.

Angenommen, ich verwende eine simple Benutzername|Passwort Authentifizierung. PRO•M hat für diese Authentifizierungsmethode die Kennung UN14) vergeben. Ich bin eine Saga, die Aufträge importiert. Die Saga hat ein eigenes Benutzerkonto OrderImportSagaAccount, kann sich also mit dem Konto gegen den Server identifizieren. Die Saga kann auf unterschiedlichen Partitionen laufen, läuft immer im Kontext des Kontos, das durch den Benutzername OrderImportSagaAccount identifiziert wird. Jede Partition erhält eine eigene Partitions-ID, z.B. P1. Nun kann jede Saga auf jeder Partition eindeutige IDs erzeugen, z.B. indem ein einfacher Zähler verwendet wird. Die zusammengesetzte ID ist dann global eindeutig. Angenommen, der Zähler ist bei 5547. Meine eindeutige ID wäre dann

5547_P1#OrderImportSagaAccount@UN

Die eindeutige Identifizierung der Saga läuft über das Konto. Der Server analysiert die ID, sieht, dass UN = Username zur Identifizierung verwendet werden soll, und prüft, ob das mit dem Typ der mitgesendeten Authentifizierungsdaten übereinstimmt15). Dann erkennt der Server, dass der Benutzername OrderImportSagaAccount sein soll, und vergleicht das mit dem Inhalt der mitgesendeten Authentifizierungsdaten16). Wenn alles stimmt, wird die ID als global eindeutig akzeptiert.

Kein anderes Konto kann einen Befehl mit OrderImportSagaAccount@UN hinter der Raute17) senden, bzw. versucht ein anderes Konto das, wird es eine Nichtübereinstimmung mit den gesendeten Authentifizierungsdaten geben, und der Befehl wird mit einer UnauthorizedIdempotencyIdException abgelehnt. Solange die Sagas in den unterschiedlichen Partitionen richtig konfiguriert sind, wird jede ihren eigenen Nummernkreis haben, d.h. die Saga in der P1 Partition kann sich sicher sein, dass niemand anderes _P1#OrderImportSagaAccount@UN verwendet18). Die Saga muss somit nur noch für die eigenen Befehle eine eindeutige Kennung erzeugen19), und wir haben eine globale Eindeutigkeit für die Idempotenz-ID der Befehle.

Was identifiziert einen Befehl

In dem oben beschriebenen Mechanismus werden die ersten beiden Punkte berücksichtigt. Wenn man davon ausgeht, dass die Kennung vom Versender eindeutig erzeugt wird, braucht man den Inhalt und Zeitstempel nicht mehr vergleichen. Man könnte das noch machen, und eine DuplicateIdempotencyIdException auslösen wenn eine Idempotenz-ID ankommt, die bereits erhalten wurde, deren Inhalt aber ein anderer war. Das wäre aber übertrieben, bzw. kann auch immer noch dazu gebaut werden.

Beispiel Endanwender

Wenn ein Endanwender einen Befehl absendet, braucht es i.d.R. gar keine Idempotenz-ID. Wenn man aber z.B. einbaut, dass sich die Client Anwendung die gesendeten Befehle merkt, damit nach einem unerwarteten Fehler und einem Neustart geprüft werden kann, was mit dem Befehl passiert ist20), kann der Client eine Befehls-ID schreiben. Damit das einfach bleibt, sollte der Client einfach eine GUID verwenden, vielleicht gemeinsam mit einem Zeitstempel21).

Für Systemagenten22) ist die Eindeutigkeit Pflicht, weil die schließliche Konsistenz und auch die Synchronisierung mit anderen Systemen davon abhängt. Die Sicherheit ist vollständig gegeben, solange das Konto nicht gehackt wird, das von den Systemagenten verwendet wird. Für einen Endanwender wird es i.d.R. gar nicht notwendig sein, eine eindeutige ID zu setzen. Wenn doch, gibt es auch noch andere Methoden, das sicher zu machen, z.B. indem man dem Konto TANs gibt, und der Anwender für idempotente Befehle die TANs verwendet.

Impersonation

Eine Saga führt u.U. einen Prozess fort, der durch einen Endanwender angestoßen wurde. Damit der Prozess im Rechtekontext des Endanwenders läuft, kann die Saga den Endanwender impersonieren, also im Rechtekontext des (Benutzer)Kontos des Endanwenders laufen, obwohl sie sich mit ihrem Systemkonto authentifiziert. Das Systemkonto der Saga muss dafür eine Impersonationsrecht auf das Benutzerkonto des Endanwenders erhalten.

Für die Befehlseindeutigkeit heißt das, dass das Konto, das zur Herstellung der Identität des Befehls verwendet wird, nicht das Konto des Endanwenders ist, sondern das Konto der Saga. Ansonsten wäre ja nicht mehr sicher gestellt, dass dieser Befehlt eindeutig ist, weil nur die Systemkonten Mechanismen für eine absolute Sicherheit für eindeutige Befehls IDs bieten. Bei Impersonierten Konten heißt das also

Umstellungen im laufenden Betrieb

Wenn die Authentfizierungsmethode umgestellt wird, und eine Saga bereits einen Befehl mit der ID 5547_P1#OrderImportSagaAccount@UN versendet hat, aber nicht weiß, ob er angekommen ist, würde sie ihn nochmal absenden. Damit das so reibungslos wie nur möglich klappt, muss folgendes passieren:

Mit diesen einfachen Regeln wird die Saga ihre Arbeit immer fortsetzen können.

1) loosely coupled system
2) eventually
3) Eigentlich Befehlsidempotenz. In den Fällen, in denen ein Befehl nicht in sich idempotent ist, also in dem f(x) != f(f(x)) ist, muss die Idempotenz über die Eindeutigkeit hergestellt werden
4) Saga oder UI Client
5) und einen Ereignistyp, der den Typ des Aggregates eindeutig bestimmt
6) oder auf die Publisher Sequenz, die einfach pro Veröffentlicher eine laufende Nummer ist
7) außer möglicherweise die Auswirkung des Befehls
8) der den Befehl letzten Endes ausführt
9) oder Idempotenz-ID
10) weil der Command Handler einen Befehl mit einer ID, für die bereits ein Befehl ausgeführt wurde, kein zweites mal ausführt
11) die von Sagas durchgeführt werden
12) räumlich und zeitlich
13) Murphys Gesetz sagt ja, dass, wenn etwas schief läuft, es so richtig schief läuft - frei gedeutet
14) für UserName
15) # und @ sind dabei reservierte Zeichen und dürfen nicht woanders in der ID vorkommen. Wenn doch, wird der Befehl mit einer InvalidIdempotencyIdException abgelehnt
16) nachdem die Daten erfolgreich authentifiziert wurden
17) #
18) anstatt P1 kann auch ein Merkmal der Computerhardware verwendet werden, damit man das gar nicht konfigurieren muss
19) Hier reicht ein Zähler
20) erhalten, ausgeführt?
21) und wie für jede ID gefordert mit der Identität des angemeldeten Users
22) Sagas, Event Handler
23) oder beiden, aber das impersonierende erzeugt mit Sicherheit eindeutige IDs, also reicht das
24) autorisiert
25) OrderImportSagaAccount@UN