This is an old revision of the document!
Wikipedia sagt
Idempotenz ist ein Begriff aus der Mathematik und Informatik. Man bezeichnet ein Element einer Menge (also auch eine Funktion), das mit sich selbst verknüpft wieder sich selbst ergibt, als idempotent.
Der Befehl “setze den Kontostand auf € 1.000,-” ist idempotent, weil die mehrfache Ausführung des Befehls zu demselben Ergebnis führen, nämlich dass der Kontostand danach € 1.000,- ist. Der Befehl “erhöhe den Kontostand um € 10,-” wiederum ist nicht idempotent. Wenn der Befehl 5 mal ausgeführt wird, wurde der Kontostand nicht um € 10,- erhöht, sondern um € 50,-1).
Man kann den Befehl “erhöhe den Kontostand um € 10,-” idempotent machen, indem man das Argument 2) um eine ID erweitert, den Operanden um die Menge aller angewandten IDs, und die Funktion um eine Prüfung auf das Vorhandensein der ID im Operanden erweitert. Wenn der Befehl also lautet ”{ID=ADD331-EJ944F-411DFI-3311IO}, erhöhe den Kontostand um € 10,-”, der Operand3) eine Liste aller angewendeten IDs hält, dann wird die mehrfache Anwendung des Befehls dazu führen, dass er nur einmal ausgeführt wird, weil die ID nach der ersten Ausführung bereits in der Liste der angewendeten ID enthalten ist.
Diese Methode, einen Befehl idempotent zu machen, hat auch Nebenwirkungen. Die Idempotenz wird dadurch bewirkt, dass Befehle mit einer bereits ausgeführten ID gar nicht mehr ausgeführt werden. Das könnte man dadurch korrigieren, dass nicht alle angewendeten IDs gemerkt werden, sondern immer nur die letzte. In dem Fall gilt f(f(f(f(f(x))))). Aber f(g(f(g(f(x))))) würde anders behandelt werden als in dem Fall der kompletten Liste aller ausgeführten Befehle. Im Fall der Liste wäre das dasselbe wie f(g(g(x))), weil die zweite und dritte Anwendung von f(x) nicht angewendet wird. Wenn aber immer nur die letze ID gemerkt wird, würden alle 5 Befehle ausgeführt werden.
In einem System, dass at-least-once-delivery für Nachrichten garantiert, kann es sein, dass Nachrichten mehrmals beim Command-Handler ankommen. Das kann passieren, weil die Bus Technologie so gebaut ist, oder 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 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.
Zudem könnten zwei Befehle zum Erzeugen eines Aggregates mit ein und derselben Aggregate ID gesendet werden, vielleicht 10 Jahre auseinander. Der Befehlstyp ist in beiden Fällen auch derselbe, so wie auch die Version8). Der zweite Befehl sollte also nicht einfach übersprungen werden.
Wenn man Befehlskennungen9) für Befehle verwendet, hat man auf jeden Fall die Garantie, dass derselbe Befehl kein zweites mal ausgeführt wird. Es gilt also
at-most-once-handling für Befehle
Das ist gut, denn das ist es ja, was wir erreichen wollten. Die einzige Gefahr, die besteht, ist, dass ein neuer Befehl eine alte ID trifft, und somit gar nicht abgearbeitet wird, weil das System meint, er wurde bereits ausgeführt. Es gilt somit auch
keine Garantie der at-least-once-handling für Befehle
Gewünscht wäre eine exactly-once-handling für Befehle, aber das ist nicht hin zu bekommen, man kann die Wahrscheinlichkeit dafür höchstens so nahe an 110) ran bekommen, wie möglich11).
Die erste Gefahr wurde gerade aufgezeigt. Wenn ein neues Objekt erzeugt werden soll, und zwei Clients, die zeitlich und örtlich weit auseinander liegen können, dieselbe ID für das Aggregate erzeugen, müssen die beiden Befehle als eigene Befehle erkannt werden. Ist das nicht der Fall, wird für den zweiten Befehl das Ergebnis des ersten gemeldet, also
Die Gefahren sind also
Eine Neuanlage eines Aggregates wird als erfolgreich gemeldet, obwohl das nicht stimmt. Hinzu kommt, dass das Aggregate, das man als neu angelegt annimmt, ein ganz anderes ist, und wenn man damit weiter arbeitet, arbeitet man mit dem falschen Aggregate.
Wird der Befehl fälschlicherweise als bereits erhalten angesehen, wird er erneut ausgeführt. Die Frage ist, welchen der beiden Befehle man nun ausführt. Nimmt man den Alten, dann führt man eventuell einen Befehl aus, der gar nicht zu dem aktuellen passt, nimmt man den Neuen, dann bleibt die Möglichkeit, dass ein anderer Prozess den alten Befehl erneut sendet, und dann gemeldet bekommt - Befehl bereits durchgeführt, also zurück zur ersten Gefahrenquelle
Wenn der erste Befehl als abgelehnt angesehen wurde, würde die Neuanlage mit derselben Begründung wie der erste abgelehnt werden.
Dieselben Gefahren bestehen für Änderungen vs. Neuanlagen.
Invarianten der Domäne werden nie verletzt. Wenn andere Regeln, die schließlich eingehalten werden müssen, verletzt werden, werden Event Handler und andere Mechanismen dazu führen, dass die Regeln wieder eingehalten werden. Das System kann also im Grunde niemals in einen anhaltenden inkonsistenten Zustand versetzt werden. Aber gucken wir uns das im Einzelnen an
Wenn ein Event Handler ein Ereignis abfängt und als Ergebnis einen Befehl ausführen soll, und dieser als bereits erhalten erkannt wird, würde der Befehl nicht ausgeführt werden. Das ist einfach mit dem zweiten Punkt der im folgenden Abschnitt beschriebenen Gefahrenabwehr zu lösen. Event Handler laufen im Kontext eines Systemkontos. Das Konto ist Teil der Prüfung auf die Identität des Befehls, also kann es zu keinen nicht erkannten Konflikten mit den Befehlen anderer Anwender kommen14). Der Event Handler merkt sich also einfach die Befehls-IDs, die er erzeugt und verwendet um sicher zu stellen, dass dieselbe ID kein zweites mal verwendet wird. So kann es niemals zu unerkannten Konflikten führen - so lange alle Event-Handler, die dieselben Befehlstypen versenden, sich daran halten; und das Systemkonto nicht geknackt wird und von jemanden anderen missbraucht wird.
Was ist mit Systempartitionen? Wenn derselbe Event Handler auf zwei unterschiedlichen Rechnern läuft, ist die eindeutige BefehlsID Erzeugung pro Systemkonto und EventHandler nicht mehr gegeben. Die Befehls-ID muss also die Partitions-ID beinhalten!
Stimmt nicht!
Ein Befehl erhält einfach ein weiters Feld Partition15). Anstatt die Partition neben der Befehls-ID in den PK des Befehls aufzunehmen, wird die Partition zur Überprüfung für die DuplicateCommandException verwendet. Wenn alles andere gleich ist, die Partition aber unterschiedlich, wird diese Exception ausgelöst. Dann weiß der Absender, dass die ID bereits vergeben wurde und setzt eine neue16).
Was bringt uns das? Das SELECT auf die bereits erhaltenen Befehle soll einfach gehalten werden, und wenn man auf ein Feld abfragt, ist das wesentlich schneller als auf einen kombinierten Schlüssel. Ein Konflikt kostet nun zwar mehrere round trips anstatt, dass die neue BefehlsID/Partition-Kombination einfach so akzeptiert wird. So ein Konflikt sollte aber nur alle Jubeljahre passieren, dann ist ein round trip nicht so teuer17).
Da ja auch das SecurityPrincipal des authentifizierten Users für die DuplicateCommandException geprüft wird, könnte man pro Event Handler Partition ein eigenes Konto verwenden. Wofür man sich letztendlich entscheidet ist egal, beide Mechanismen werden in den Command Handlern eingebaut - man muss sich mindestens für einen entscheiden.
Warum ist das so wichitg? Angenommen ein Event Handler synchronisiert zwei Bounded Contexts. Wenn im ersten eine Änderung passiert, muss sie schließlich im zweiten ankommen. Wenn der Befehl idempotent ist18) ist das alles kein Problem, dann kann der Event Handler den Befehl so oft absenden, bis er erfolgreich ist, ohne eine eigene Befehls-ID zu setzen19).
Ist der Befehl aber nicht intrinsisch idempotent, muss eine Befehls-ID vom Event Handler gesetzt werden. Das darf aber nicht dazu führen, dass der Befehl nicht ausgeführt wird20). Wenn er nicht ausgeführt wird, werden zwar keine Geschäftsregeln verletzt, das System wäre aber nicht schließlich konsistent, weil die Änderung nie ankommt.
Das ist noch wichtiger bei Event Handlern, die tatsächliche Aggregate übergreifende Geschäftsregelverletzungen kompensieren sollen.
OK, die Wahrscheinlichkeit, dass so was passiert, wenn man einfache GUIDs verwendet, ist so gering, dass ein manueller Eingriff alle 10 Jahre akzeptabel wäre. Aber ich finde, die Sicherheitsmechanismen zur Gefahrenabwehr sind nicht so teuer.
Sagas verwenden eine CorrelationID um den Prozess zu koordinieren. Wenn die Saga die ID sicher erzeugt21), dann ist die Kombination [SagaTyp|PartitionID22)|CorrelationID] eindeutig.
Die BefehlsIDs haben mit der CorrelationID aber nichts zu tun. Bei der Überprüfung auf eine DuplicateCommandException kann die CorrelationID mit ran gezogen werden. Das muss aber nicht passieren, wenn die Saga die BefehlsID genauso sicher erzeugt wie die Event Handler.
Bei korrekter Konfiguration und entsprechendem Schutz der Systemkonten können die Befehlskennungen, die von Event Handlern und Sagas gesetzt werden, eindeutig gemacht werden, bzw. können Konflikte23) mit absoluter Sicherheit gelöst werden24). Das funktioniert, weil man die Systemkonto/Partition Kombination, die von den Event Handlern und Sagas verwendet werden, eindeutig machen kann, und schützen kann, so dass sie von keinem anderen Client verwendet werden kann.
Wie kann man das für Endanwender erreichen, die unterschiedliche Clients verwenden können? Muss man diesen Grad an Sicherheit überhaupt für Endanwender erreichen?
Befehle von Endanwendern benötigen nicht den Grad an garantierter Sicherheit der Eindeutigkeit wie die von Event Handlern oder Sagas. Event Handler und Sagas garantieren u.a. die schließliche Konsistenz, daher sollte diese Garantie so gut es geht sicher gestellt werden. Endanwender sind da ein wenig flexibler und treffen Entscheidungen nicht nach fest gelegten Schemata, sondern nach Bedarf.
Ich nehme an, dass ein Benutzerkonto auch Personengebunden verwendet wird. Somit wird i.d.R. immer nur ein Benutzerkonto gleichzeitig angemeldet sein25). Wenn man im Befehl also noch den Zeitstempel der Befehlserzeugung setzt, kann davon ausgegangen werden, dass die Kombination [Befehls-ID|Benutzerkonto|Zeitstempel|Befehlsinhalt] keine zweimal erzeugt werden.
Nicht vergessen, die Befehls-ID sollte global eindeutig sein. Es geht nun nur noch um den möglichen Fall, dass dieselbe Befehls-ID ein zweites Mal erzeugt wurde. Nur in diesem Fall werden noch die weiteren Daten [Benutzerkonto|Zeitstempel|Befehlsinhalt] geprüft, um sicher zu stellen, ob es wirklich derselbe Befehl war, oder ob es ein Konflikt der ID darstellt. Wenn es sich um einen Konflikt handelt, erhält der Client eine DuplicateCommandException und muss nur die ID ändern und den Befehl erneut abschicken.
Wenn Befehle nicht sofort behandelt werden, sondern einfach mit Bus.Send(command) versendet werden, weiß man nicht, ob der Command Handler den Befehl akzeptiert oder nicht. Man muss das Ergebnis Pollen. ABER WOMIT? Polling ID? Was ist mit deren Eindeutigkeit? Das Problem ist, dass wenn der command vom Bus an den Command Handler übergeben wird, die Antwort
Als erstes wird neben der ID des Befehls der Inhalt des Befehls verglichen. Wenn die ID dieselbe ist, der Inhalt aber ein anderer, dann erhält der Client eine eigens dafür definierte DuplicateCommandException. Er weiß dann, dass er zufällig eine bereits verwendete ID getroffen hat, dass aber der Inhalt des Befehls ein anderer ist. Er sendet den Befehl einfach ein weiteres mal ab nachdem die ID ersetzt wurde.
Zweitens wird der Command Handler Befehle nur speichern und ausführen, wenn der Anwender authentifiziert ist. Das SecurityPrincipal wird auch zu dem Befehl und der ID gespeichert, und wenn diese nicht übereinstimmt, wird ebenfalls ein DuplicateCommandException ausgelöst26). So können Angreifer zudem nicht einfach sinnlose Befehle senden um die IDs zu füllen.
Drittens wird eine mitgesendete PartitionID im Befehl genauso verwendet wie das SecurityPrincipal des authentifizierten Users: Bei gleicher Befehls-ID aber unterschiedlicher PartitionID eine DuplicateCommandException melden. Das ist für Event Handler und Sagas, die verteilt werden, wichtig.
Sind Befehle zur Neuanlage von aggregates oder entities auf natürliche Weise idempotent? Wenn ich zwei mal den Befehl zur Anlage einer Person sende, beide mit derselben Personen-ID27), ist das dann ein idempotenter Befehl? Die Antwort ist, dass es darauf ankommt. Wenn das Ergebnis des zweiten Befehls ist, dass nichts passiert, dann ist der Befehl idempotent. Wenn das Ergebnis des zweiten Befehls ist, dass ein Fehler ausgelöst wird, ist der Befehl nicht idempotent, weil es den Fehler beim ersten Mal nicht gab.
Wie also damit umgehen? Auch wenn die Wahrscheinlichkeit verschwindend gering ist, dass eine Guid zwei mal erzeugt wird, und dann noch auf dieselbe Funktion angewendet wird, halte ich es für wichtig, dass der zweite Befehl nicht stillschweigend ignoriert wird, denn was ist, wenn es gar nicht beabsichtigt war dieselbe Guid zu verwenden? Ich sehe die Idempotenz somit an anderer Stelle.
Ein Command Handler behandelt einen Befehl indem im Aggregate eine Methode aufgerufen wird. Es werden also zwei Funktionen aufgerufen, einmal eine im Command Handler, dann eine im Aggregate. Die Methoden im Aggregate sollten immer ausgeführt werden und Konflikte als solche melden. Der Command Handler hingegen sollte die Möglichkeit haben, Befehle als bereits erhalten zu kennzeichnen. In unsicheren Bus-Technologien gilt oft die Regel at-least-once-delivery28). Also ist die einmalige Behandlung von Befehlen schon durch den29) Einsatz von Bus-Technologien bedingt.
Ein Befehl wird für ein Aggregate30) abgesendet. Er kann für eine bestimmte Version bestimmt sein, muss es aber nicht. Einfach eine GUID als eindeutige ID zu verwenden halte ich für zu unsicher. Wenn das System weite Anwendung hat, können mehrere Millionen Befehle von diversen Clients - nicht immer unter direkter/eigener Kontrolle - versendet werden. In dem Fall ist die Wahrscheinlichkeit, dass ein neuer Befehl eine bereits verwendete Idempotenz ID verwendet, vielleicht signifikant31).
Ich schlage somit vor, dass die Idempotenz ID (idempotency ID) eine einfache GUID ist. Sie muss aber nicht eindeutig sein! Um zu prüfen, ob der Befehl bereit erhalten wurde, fragt der Server alle erhaltene Befehle mit dieser GUID ab. Das sollten i.d.R. keine oder eine sein. Zu der Guid wird der Befehl als serialisiertes JSON Objekt gespeichert. Wenn zu der ID ein oder mehrere Befehle gefunden werden, werden die Inhalte der Befehle gegen den Inhalt des gesendeten Befehls verglichen. Wenn neben der Idempotenz ID auch noch die Inhalte dieselben sind32), dann gilt der Befehl als bereits erhalten und verarbeitet.
Das muss an Sicherheit ausreichen. Der Befehl wird mindestens eine Aggregate ID enthalten, eventuell noch eine Versionsnummer und weitere Parameter33). Wenn ein Client nun einen Befehl abschickt und möchte, dass dieser idempotent verarbeitet wird34), hat er
Zusammengefasst wird eine maximal einmalige Ausführung des Befehls garantiert. Das ist es ja genau, was wir erreichen wollen, d.h. die Idempotenz wurde für genau diesen Zweck mit gesendet. Die Wahrscheinlichkeit einer falschen Doppelerkennung wird zudem soweit es geht reduziert, dass ich jede Wette abschließe, dass Clients, die sich an die Regeln halten36), in einer Millionen Jahren keine doppelte Idempotenz ID + Befehl37) erzeugen.
Befehle werden gespeichert noch bevor sie verarbeitet werden, also auch bevor sie authentifiziert wurden. Das öffnet möglichen Angreifern die Tür, einfach alle möglichen nicht authorisierten Befehle zu versenden, um die Liste der bereits erhaltenen Befehle mit Müll zu füllen. Somit sollten Befehle erst in die Liste geschrieben werden, nachdem sie authentifiziert38) wurden, und dann auch zusammen mit dem SecuritiyPrincipal39),40),41). Klar, jemand kann einen Client programmieren, der sich erst authentifiziert und dann Müll versendet. Aber diesen Client muss dann auch ein Anwender verwenden, der sich erst mit korrekte Anmeldedaten anmeldet. Das wird dann gelöst, wenn es passiert - also nie