User Tools

Site Tools


concepts:idempotency

This is an old revision of the document!


Idempotenz

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ührt, 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, würde 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.

Befehlsidempotenz

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.

Gefahren bei fehlerhafter Befehlsidempotenz

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

  • Befehl erfolgreich durchgeführt
  • Befehl erhalten12)
  • Befehl abgelehnt13)

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 erzeugen14). Sie liegen auf der selben Systemebene wie die Endpunkte15), und dürfen Befehle auch direkt auf den Bus packen, ohne über einen Endpunkt zu gehen16). 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 Kennung17). 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 verlassen18).

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 handeln19). 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 Endpunkt20) aber die vom Endpunkt21) erzeugte und im Befehl eingesetzte CommandID22). Diese ID kann er dann verwenden um z.B. danach zu 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
  1. Der Client sendet den Befehl an den Enpunkt um eine ID für ihn zu reservieren,
  2. 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,
  3. 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,
  4. Der Client schickt nun den Befehl mit der ID und zusammen mit dem Schlüssel an den Endpunkt,
  5. 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,
  6. Wenn in dieser Kommunikation ein Fehler passiert, schickt der Client die Daten einfach nochmal ab - innerhalb des Reservierungszeitraums so oft er möchte,
  7. Irgendwann muss der Client entweder ein ACK erhalten23), 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 gibt24).

1) f(f(f(f(f(x))))) != f(x)
2) den Befehl
3) das Aggregate
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) Ein Befehl zum Erzeugen eines Aggregats hat keine Versionsnummer, bzw. die Version 0
9) ID
10) 100%
11) 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.
12) 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
13) aus Business Gründen, z.B. nicht autorisiert, nicht validiert, oder andere Verletzungen der Geschäftsregeln
14) müssen sie auch können
15) WCF Dienste
16) das dürfen andere Clients auch, aber dann ohne Befehls-ID
17) GUID
18) d.h. dass das System bei fehlerhaftem Verhalten immer noch so sicher ist wie andere Systeme bei korrektem Verhalten!
19) 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
20) WCF Service
21) System Agent
22) UniqueSystemAgentID+AgentUniqueCommandID
23) wenn er sich korrekt verhält
24) 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
concepts/idempotency.1358424256.txt.gz · Last modified: 2013/01/17 13:04 by rtavassoli