This is an old revision of the document!
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 wichtiges.
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 Prozess3) nach einem unerwarteten Abbruch den Befehl ein zweites mal sendet.
Befehle sind nicht auf natürliche Art idempotent6). 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 einem singleton Command Handler 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.
Befehle müssen eindeutig gemacht werden, damit sie in einem System, dass sie garantiert schließlich liefert, sie dabei aber eventuell mehrmals liefert, nur genau einmal ausgeführt werden. Somit muss der Command Handler7) 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 Befehlseindeutigkeit 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ührt. 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 Prozesse8) 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 eindeutig macht.
Es scheint, als wäre eine GUID 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 machen9). Für mich gilt somit erstens
Ein Befehl wird nicht nur über die ID identifiziert, sondern auch über den Absender, das Datum/Uhrzeit und den Inhalt.
Wenn die ID auch noch frei konfigurierbar ist10), dann sind alle Systemanforderungen für eine fast lückenlose Sicherheit gegeben11).
Der Befehl wird identifiziert über
Wenn ein Befehl also ankommt, wird er authentifiziert. Dann wird geprüft, ob es die ID bereits gibt13). Gibt es sie bereits14), werden alle anderen Aspekte des Befehls geprüft. Nur, wenn er noch nicht ausgeführt wurde, wird er zu der ID serialisiert gespeichert und danach ausgeführt15).
Gibt es die ID16) bereits, aber die anderen Aspekte stimmen nicht überein, muss das System davon ausgehen, dass es nicht derselbe Befehl ist. Den Befehl in dem Fall einfach aus zu führen - es ist ja ein neuer - ist nicht korrekt. Die ID muss als Primärschlüssel dienen. Ansonsten besteht die Gefahr, dass derselbe Befehl parallel mehrmals gesendet wird. In beiden Fällen würde er bei der Suche nach der ID nicht gefunden werden, und würde somit zwei mal verarbeitet werden. Wenn die ID kein Primärschlüssel ist, würde das klappen, d.h. die doppelte Verarbeitung würde erlaubt werden und nicht erkannt werden - schlecht wenn der Befehl nicht in sich idempotent ist.
Somit muss folgendes passieren:
Die DuplicateCommandIDException sagt dem aufrufenden Client, dass er eine ID verwendet hat, die bereits von einem anderen Befehl verwendet wurde. Wäre es derselbe Befehl gewesen, wäre die Exception nicht ausgelöst worden. Der Client kann somit die Idempotency-ID einfach neu setzen und den Befehl erneut versenden. Das kann völlig gefahrlos geschehen, der Befehl wurde mit Sicherheit noch nicht verarbeitet.
Es gibt einen Schwachpunkt. Wenn Befehle über einen Bus versendet werden, würde der Client die DuplicateCommandIDException nicht sofort und synchron erhalten. Es müsste eine entsprechende Nachricht auf dem Bus zurück geschickt werden, damit der Client den Fehler mitbekommt. Das Problem ist natürlich, worauf sich die Nachricht bezieht, die zurück gemeldet wird, da die Idempotency-ID ja nicht zwingend eindeutig ist17).
Die Eindeutigkeit eines Befehls wird also nicht über die ID hergestellt, sondern über den gesamten Befehl18). Wenn man also z.B. eine Anfrage schicken möchte, ob ein Befehl ausgeführt wurde, schickt man nicht einfach die ID, sondern den kompletten Befehl.
Angenommen eine Saga will einen Prozess koordinieren, und erzeugt dazu Befehle. Die Saga kann wie folgt eingerichtet werden, damit sie eindeutige IDs erzeugt19) - was dann zur Folge hat, dass auch die mindestens einmalige Ausführung des Befehls garantiert ist:
Die Saga hat die Sicherheit, dass die ID eindeutig ist. Alle anderen Agents haben eigene GloballyUniqueAgentIDs. Wenn jemand von Außen einen Agent baut, wird er nicht die Anmeldedaten des Systemkontos haben, dass die Saga verwendet23). Auch wenn dieser Agent zufällig dieselbe Befehls-ID erzeugt wie die Saga, ist es trotzdem ein anderer Befehl, weil ein anderes Konto verwendet wurde um ihn zu versenden! Genauso können keine Endanwender in einen Konflikt mit der Befehls-ID geraten, weil sie kein Systemkonto, sondern Benutzerkonten verwenden, um Befehle abzuschicken.
Wenn ein Endanwender einen Befehl absendet, braucht es i.d.R. gar keine CommandID. 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 ist24), kann der Client eine Befehls-ID schreiben. Damit das einfach bleibt, sollte der Client einfach eine GUID verwenden. Eine GUID, zusammen mit einem Zeitstempel und der Identität des angemeldeten Users, sowie dem gesamten Inhalt des Befehls ist mehr als ausreichende Sicherheit, dass der Befehl eindeutig ist.
Mehr als ausreichend? Für Systemagenten25) ist die Sicherheit vollständig, solange das Konto nicht gehackt wird. Wieso nicht für Benutzer? Nun, eine GUID gilt in anderen System als ausreichend. Ich verwende zusätzlich einen Zeitstempel, das Konto des Absenders und den Inhalt des Befehls26). Das ist so sicher wie es nur möglich ist.
Es gibt soviele Möglichkeiten die Eindeutigkeit der ID der Befehle von Endanwendern eindeutig zu machen. Man kann eindeutige Sitzungs-IDs verwenden, die IDs vom Endpunkt erzeugen lassen, an den der Client ihn letztendlich sendet, usw. Alle diese Möglichkeiten sind recht komplex, weil die Client Anwendungen nicht kontrolliert eingerichtet und konfigurert werden, sondern von jedem Anwender beliebig oft und an beliebiger Stelle installiert werden kann.
Wozu also dieser Umstand für Endanwender? So, wie es gebaut ist, ist es doch bereits sicherer als sich morgens die Zähne zu putzen - die Wahrscheinlichkeit, dass man bei Zähneputzen von einem Meteoriten getroffen wird ist deutlich größer als die Möglichkeit, dass ein Befehl identisch erzeugt wird. Und wenn doch? Dann ist ein Meteoriteneinschlag schlimm. Ein nicht ausgeführter Befehl aber nicht. Der Anwender führt die Aktion einfach nochmal durch wenn das Ergebnis nicht zu sehen ist. Kann ja nichts passieren, das System stellt ja sicher, dass es (schließlich) konsistent ist, und das weil
die Befehlseindeutigkeit für System Agenten sicher gestellt werden kann, was die (schließliche) Konsistenz des Systems garantiert.
Das war mir wichtig. Hier dürfen keine Fehler passieren. Der Anwender kann Fehler immer selbst korrigieren.
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