This shows you the differences between two versions of the page.
concepts:idempotency [2013/01/17 13:04] rtavassoli |
concepts:idempotency [2013/01/18 13:26] (current) rtavassoli |
||
---|---|---|---|
Line 7: | Line 7: | ||
\\ \\ | \\ \\ | ||
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 <nowiki>f(f(f(f(f(x)))))</nowiki>. Aber <nowiki>f(g(f(g(f(x)))))</nowiki> 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 <nowiki>f(g(g(x)))</nowiki>, 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. | 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 <nowiki>f(f(f(f(f(x)))))</nowiki>. Aber <nowiki>f(g(f(g(f(x)))))</nowiki> 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 <nowiki>f(g(g(x)))</nowiki>, 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. | ||
- | ===== Befehlsidempotenz ===== | + | ===== Befehlsidempotenz - oder lieber //Eindeutigkeit// ===== |
- | 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 Prozess((Saga oder UI Client)) nach einem unerwarteten Abbruch den Befehl ein zweites mal sendet. | + | In einem schwach zusammenhängenden System((loosely coupled system)), 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ßlich//((eventually)) 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 [[technology:domainmodel:command:commanduniqueness|Befehlseindeutigkeit]] ist somit ein sehr wichtiges. |
- | \\ \\ | + | |
- | Ereignisse sind auf natürliche Art idempotent, weil sie immer eine Aggregate ID und eine Version haben((und einen Ereignistyp, der den Typ des Aggregates eindeutig bestimmt)). Event Handler sollten also bei nicht idempotenten Ereignisinhalten auf die ID/Version prüfen((oder auf die Publisher Sequenz, die einfach pro Veröffentlicher eine laufende Nummer ist)). | + | |
- | \\ \\ | + | |
- | Befehle sind nicht auf natürliche Art idempotent((außer möglicherweise die Auswirkung des Befehls)). 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 Version((Ein Befehl zum Erzeugen eines Aggregats hat keine Versionsnummer, bzw. die Version 0)). Der zweite Befehl sollte also nicht einfach übersprungen werden. | + | |
- | ==== Gefahren bei fehlerhafter Befehlsidempotenz ==== | + | |
- | Wenn man Befehlskennungen((ID)) 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 1((100%)) ran bekommen, wie möglich((Wenn alle Clients sich korrekt verhalten und das System sich erfolgreich gegen Angriffe schützen kann, kann die Wahrscheinlichkeit bei Fehlerfreiheit des Systems 1 werden; das wird die Lösung sein, weil sich zumindest die Event Handler und Sagas korrekt verhalten sollten, und die Idempotenz nur diese betreffen wird.)). | + | |
- | \\ \\ | + | |
- | 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 | + | |
- | * Befehl erfolgreich durchgeführt | + | |
- | * Befehl erhalten((Ein Befehl kann auch in diesem Zustand //hängen bleiben// wenn er aus unerwarteten technischen Gründen nicht durchgeführt werden kann und der Sender nach dem //fire-and-forget// Prinzip nicht versucht, den Fehler zu beheben und den Befehl erneut zu senden)) | + | |
- | * Befehl abgelehnt((aus Business Gründen, z.B. nicht autorisiert, nicht validiert, oder andere Verletzungen der Geschäftsregeln)) | + | |
- | 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. | + | |
- | ==== Auswirkungen bei nicht abgefangenen Konflikten ==== | + | |
- | 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. | + | |
- | \\ \\ | + | |
- | Aber genau hier liegt der Hund begraben. Wenn ein Event Handler oder eine Saga einen Befehl absendet, der die schließliche Konsistenz herstellen soll, und der Befehl eine bereits verwendete ID hat, würde er kein weiteres mal ausgeführt werden - und die schließliche Konsistenz ist nicht mehr gegeben! | + | |
- | === Die Lösung === | + | |
- | Event Handler und Sagas dürfen die Befehls-IDs selbst erzeugen((müssen sie auch können)). Sie liegen auf der selben Systemebene wie die Endpunkte((WCF Dienste)), und dürfen Befehle auch direkt auf den Bus packen, ohne über einen Endpunkt zu gehen((das dürfen andere Clients auch, aber dann ohne Befehls-ID)). Nennen wir Event Hanlder, Saga, WCF Enpunkte und andere Dienste, die mit dem System kommunizieren können: | + | |
- | == Systemagenten == | + | |
- | Ein Systemagent ist ein Akteur, der ein festes Systemkonto zugewiesen bekommt und dem das System mit der Erzeugung von eindeutigen Befehls-IDs vertraut. Es läuft im Grunde ganz einfach ab. Jeder Systemagent erhält von zentraler Stelle eine eindeutige Kennung((GUID)). Dann wird jeder Systemagent in der Lage sein, zu seiner Kennung eindeutige AgentUniqueCommandIDs zu erzeugen. Die Befehls-ID setzt sich somit zusammen aus [UniqueSystemAgentID + AgentUniqueCommandID]. | + | |
- | \\ \\ | + | |
- | Wenn derselbe Event Handler oder dieselbe Saga - derselbe Agent - partitioniert wird, muss jede einzelne Instanz eine eigene UniqueSystemAgentID erhalten. | + | |
- | \\ \\ | + | |
- | Nun werden nur System Agenten erlaubt, Befehls-IDs zu erzeugen. Wenn die Konfiguration korrekt eingehalten wird, und keiner die System Agenten Konten knackt, ist das System absolut sicher. Wenn auch nur einer der beiden Punkte nicht eingehalten wird, ist das System immer noch so ziemlich absolut sicher - so sicher, wie es alle anderen machen, die sich auf eindeutige GUIDs verlassen((d.h. dass das System bei fehlerhaftem Verhalten immer noch so sicher ist wie andere Systeme bei korrektem Verhalten!)). | + | |
- | \\ \\ | + | |
- | == End-User Clients == | + | |
- | End-User Clients werden keine eigenen Befehls-IDs erzeugen. Wenn ein Event Handler einen Befehl abschickt, muss er bei unerwarteten Fehlern die Möglichkeit haben, den Befehl idempotent nochmal abzuschicken. Er kann nicht einfach //gucken was passiert ist// und dann entsprechend handeln((es sei denn, der Befehl selbst ist idempotent - was aber auch nicht immer ausreicht, weil andere Befehle dazwischen liegen können, und dann ist die Kombination nicht mehr idempotent)). Wenn ein End-User einen Befehl abschickt und ein unerwarteter Fehler passiert, startet der End-User die Anwendung neu, guckt sich die Daten an, und handelt dann entsprechend. | + | |
- | \\ \\ | + | |
- | Der End-User erhält vom Endpunkt((WCF Service)) aber die vom Endpunkt((System Agent)) erzeugte und im Befehl eingesetzte CommandID((UniqueSystemAgentID+AgentUniqueCommandID)). Diese ID kann er dann verwenden um z.B. danach zu [[technology:polling|Pollen]]. | + | |
- | \\ \\ | + | |
- | Die einzige Gefahr hierbei ist, dass der Befehl vom Endpunkt ausgeführt wird, die ID den Client vom End-User nicht erreicht, weil die Verbindung weg ist. Auch das ist lösbar. | + | |
- | == Es geht noch besser == | + | |
- | - Der Client sendet den Befehl an den Enpunkt um eine ID für ihn zu reservieren, | + | |
- | - Der Endpunkt gibt dem Befehl eine eindeutige ID, merkt sich den Befehl in der Reservierungstabelle, und schickt die ID zurück an den Client, zusammen mit einem Reservierungs-Schlüssel, der mit der dem SessionKey verschlüsselt ist, und einer Zeitspanne, für die die Reservierung gilt, | + | |
- | - Der Client erhält die ID, entschlüsselt den Reservierungs-Schlüssel, und kann nun den Befehl samt Schlüssel lokal speichern bevor er den Befehl abschickt, | + | |
- | - Der Client schickt nun den Befehl mit der ID und zusammen mit dem Schlüssel an den Endpunkt, | + | |
- | - Der Endpunkt prüft den Schlüssel zu der Befehls-ID, prüft ob der Befehl auch derselbe ist, wie der reservierte, prüft ob die Reservierung noch aktuell ist, und schickt den Befehl ab - danach ein ACK an den Client. Wurde der Befehl bereits abgeschickt gibt es auch ein ACK. Alles andere wird mit einem entsprechenden Fault gemeldet, | + | |
- | - Wenn in dieser Kommunikation ein Fehler passiert, schickt der Client die Daten einfach nochmal ab - innerhalb des Reservierungszeitraums so oft er möchte, | + | |
- | - Irgendwann muss der Client entweder ein ACK erhalten((wenn er sich korrekt verhält)), oder einen Fault, der auf ein Time-Out der Reservierung hiweist. Dann startet er den Prozess erneut, weil eine neue ID benötigt wird. | + | |
- | Dieser Workflow | + | |
- | * garantiert die globale Eindeutigkeit von Befehls-IDs | + | |
- | * eröffnet für die Dauer der Reservierung die Möglichkeit, dass ein Angreifer in den 5 Minuten alle Daten errät, den Reservierungs-Schlüssel entschlüsselt nachdem der SessionKey geknackt ist, und den reservierten Befehl ausführt. Hmmm, das war ja auch Sinn der Reservierung, oder? Also auch wenn dieses absolut unmögliche Ereignis eintritt, ist das Ergebnis ja ok. "Danke, Angreifer, dass Du den Befehl für mich ausgeführt hast ;-)" | + | |
- | == Transaktionale Endpunkte == | + | |
- | Der oben genannte Workflow ist umständlich, und sollte nicht von jedem Client für jeden Befehl so umgesetzt werden. Er ist nur wichtig, wenn der Client bei Fehlern überprüfen muss, ob ein Befehl abgesendet wurde bevor er ihn nochmal verschickt. In der Regel reicht der einfache Workflow, in dem der Client die Befehls-ID zurück erhält, oder in dem sich der Client gar nicht um die Befehls-ID kümmert völlig aus. | + | |
- | ===== Zusammenfassung ===== | + | |
- | Das System kann so gebaut werden, dass Agenten von Außen Befehls-IDs nicht selbst bestimmen können, und die Agenten von innen über UniqueAgentIDs und der sicheren Erzeugung von eindeutigen IDs zu der eigenen AgentID immer nur eindeutige IDs erzeugen. Das mit "von Außen" und "von Innen" ist nur eine Abstraktion. Das wird über das Rechtesystem geregelt. Der Command Handler, bei dem Befehle letztendlich ankommen, prüft ob der Sender das Recht hat, Befehls-IDs zu vergeben. Event Handler und Sagas können Befehle mit IDs versenden, wenn sie im Kontext eines entsprechenden Systemkontos laufen. WCF Endpunkte auch. Fremdsysteme auch, wenn man ihnen vertraut und ein eigenes Konto gibt((die UniqueAgentID ist in dem Fall einfach die KontoID, und auch das wird geprüft, d.h. die können nicht einfach eine beliebige UniqueAgentID verschicken, es muss die ID sein, die dem Fremdanbieter gegeben wurde)). | + | |