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ü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.

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. Das System kann also im Grunde niemals in einen anhaltenden inkonsistenten Zustand versetzt werden. Aber gucken wir uns das im Einzelnen an

Event Handler

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 neue.

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 mehrerer round trips anstatt, dass die neue ID-Partition Kombination einfach so akzeptiert wird. So ein Konflikt sollte aber nur alle Jubeljahre passieren, dann ist ein round trip nicht so teuer16).

Da ja auch das SecurityPrincipal des authentifizierten Users für die DuplicateCommandException geprüft wird, könnte man pro Partition ein eigenes Konto verwenden. Wofür man sich letztendlich entscheidet ist egal, beide Mechanismen werden in den Command Handlers eingebaut.


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 ist17) 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 setzen18).

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 wird19). 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

Sagas verwenden eine CorrelationID um den Prozess zu koordinieren. Wenn die Saga die ID sicher erzeugt20), dann ist die Kombination [SagaTyp|PartitionID21)|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.

Endanwender

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 Konflikte22) mit absoluter Sicherheit gelöst werden23). 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?

Gefahrenabwehr

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öst24). 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.

Natürliche Idempotenz

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-ID25), 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-delivery26). Also ist die einmalige Behandlung von Befehlen schon durch den27) Einsatz von Bus-Technologien bedingt.

Wie mache ich einen Befehl idempotent?

Ein Befehl wird für ein Aggregate28) 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 signifikant29).

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 sind30), 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 Parameter31). Wenn ein Client nun einen Befehl abschickt und möchte, dass dieser idempotent verarbeitet wird32), hat er

  • die Sicherheit, dass wenn er den Befehl so mehrmals schickt, der Befehl garantiert maximal einmal verarbeitet wird,
  • ausreichende Sicherheit, dass die Idempotenz ID + Aggregate ID + Befehlstyp + Befehlsparameter in dieser Kombination niemals doppelt erzeugt werden. Und wenn doch, wird der Befehl als bereits verarbeitet gemeldet, am besten mit dem Zeitpunkt, wann das war33). Erstens ist die Wahrscheinlichkeit so gering, dass sogar ich damit kein Problem mehr habe. Und zweitens: Was kann da im schlimmsten Fall passieren? Dann schick eine neue Version des Befehls ab, mit einer neuen Idempotenz ID, und der Befehl wird durchgeführt. Einen Befehl nicht auszuführen ist doch keine Gefahr für die Konsistenz des Systems.

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 halten34), in einer Millionen Jahren keine doppelte Idempotenz ID + Befehl35) erzeugen.

Eine Gefahrenquelle

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 authentifiziert36) wurden, und dann auch zusammen mit dem SecuritiyPrincipal37),38),39). 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 ;-)

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, kann die Wahrscheinlichkeit bei Fehlerfreiheit des Systems 1 werden; wir wollen das System aber nicht gegen neue Clients schließen
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) Höchstens zu DuplicateCommandException, dann sendet der Event Handler den Befehl einfach mit einer neuen ID ab
15) dabei ist die Partition des Absenders gemeint
16) der Programmieraufwand schon, aber das ist der Preis der Idempotenz, finde ich
17) z.B. ändere den Namen in “Max Mustermann”
18) das übernimmt dann der Server. Die Prüfung auf Idempotenz findet dann gar nicht statt
19) vielleicht wurde die Person schon mal in Max Mustermann umbenannt, inzwischen aber in Max Musterkerl, und soll jetzt aber wieder Mustermann heißen
20) bereits verwendete CorrelationIDs speichern und bei Konflikten eine neue erzeugt
21) Wenn dieselbe Saga auf mehreren Partitionen läuft, erhält jede Partition einfach eine eindeutige ID. Daran muss man natürlich denken!
22) DuplicateCommandException
23) pro Command Handler Partition, versteht sich
24) Wenn derselbe Befehl von jemanden anderen gesendet wurde, geht das System davon aus, dass es zwei unterschiedliche Befehle sein sollten, und meldet bei ein und derselben Befehlskennung einen Konflikt
25) Guid
26) Der serialisierte Befehl wird mindestens einmal geliefert. Es kann aber passieren, dass er mehrmals geliefert wird. Wenn zwischen den Lieferungen andere Befehle liegen, dann reicht die Idempotenz der Befehle in der Domäne nicht aus, denn wenn “setze Konto auf 100” zwei mal kommt, “setze Konto auf 50” danach abgesendet wurde, aber zwischen den beiden Befehlen ankommt, weil der Absender des ersten das Ergebnis nicht gesehen hat und ihn nochmal absendet, ist das Ergebnis falsch.
27) möglichen
28) oder für einen Domain Service, dann das Hauptaggregate verwenden
29) eher nicht, hängt aber auch u.a. von dem GUID Erstellungsverfahren ab
30) wobei Befehlsversionen berücksichtigt werden müssen - d.h. vielleicht gab es eine Systemumstellung und die Saga sendet denselben Befehl in einer neuen Version. In dem Fall muss die Gleichheit trotzdem sicher gestellt sein
31) ein Zeitstempel kann auch noch dazu kommen, d.h. der Client setzt automatisch einen Zeitstempel
32) er muss ja gar keine Idempotenz ID setzen
33) man kann auch definieren, dass gefundene Befehle, die älter als ein Tag sind, ignoriert werden
34) u.a. beim Erzeugen von GUIDs
35) Typ + Zustand
36) nicht authorisiert
37) Der Identität des Absenders
38) Idempotenz kann also nur von Clients verlangt werden, die sich auch korrekt angemeldet haben
39) Das reduziert die Wahrscheinlichkeit, dass 2 getrennte Befehle als gleich angesehen werden, nochmal um den Faktor Anzahl(Benuzterkonten + Systemkonten)
concepts/idempotency.1358340415.txt.gz · Last modified: 2013/01/16 13:46 by rtavassoli