Blog

Immer das Gleiche mit der Idempotenz!

Idempotenz ist beim Aufbau einer Serviceorientierten Architektur ein häufig gefordertes Pattern bei der Gestaltung der Services. Ein Service ist idempotent, wenn er die Fähigkeit besitzt, damit umzugehen, dass Nachrichten mehrfach zugestellt werden. Er soll sich bei wiederholten Aufrufen mit denselben Parametern identisch verhalten, ohne dabei Schaden anzurichten. Im Klartext heißt das: Wird ein Service mit denselben Parametern zweimal aufgerufen, soll er denselben Zustand hinterlassen.

Diese Forderung klingt erstmal vernünftig. Um eine solches Verhalten zu erzielen, werden gelegentlich Idempotenz-Frameworks geschaffen, die Service-Aufrufe und deren Ergebnisse cachen. Wird dann der gleiche Aufruf nochmal durchgeführt, wird einfach das gecachte Ergebnis zurückgeliefert, anstatt die Verarbeitung ein weiteres Mal anzustoßen. Warum ein solcher Weg aber besonders im Kontext vom BPM und Prozessautomatisierung gefährlich ist, möchte ich an einem Praxisbeispiel erläutern.

Déjà-vu für Services

In einem aktuellen Projekt automatisieren wir komplizierte fachliche Prozesse mit einer auf EJB3 basierenden Serviceorientierten Architektur. Darüber liegt eine Process Engine, die die Prozesssteuerung und Orchestrierung der Services übernimmt. Die Process Engine kommuniziert mit der SOA über Webservices.

Genau an dieser Stelle existiert eine Idempotenzschicht: Ein SOAP-Handler bildet eine Klammer um die Services und fängt die In- und Outbound-Nachrichten ab. Anhand einer Call-ID, die in der Process-Engine aus dem fachlichen Kontext gebildet und im SOAP-Request mitgegeben wird, prüft der Idempotenz-Handler, ob ein identischer Aufruf bereits durchgeführt wurde.

Ist das nicht der Fall, wird der tatsächliche Service aufgerufen. Danach greift erneut der Idempotenz-Handler ein und sichert die SOAP-Response.

Erkennt der Inbound-Handler, dass zu der Call-ID bereits ein Aufruf im Cache existiert, so wird eben nicht der Service aufgerufen, sondern die zuvor gecachte Response gelesen und direkt zurückgegeben.

Und genau im Kontext der Prozessautomatisierung führt dieses Verhalten zu Problemen. Zwei Szenarien:

Szenario 1: Gewollt wiederholte Service-Aufrufe während des Prozessablaufs

In unseren Prozessen beschränken wir uns im Wesentlichen darauf, IDs in der Payload durch den Prozess zu schleusen. Den Aufwand, komplette Geschäftsobjekte zu verwalten, haben wir dadurch vollständig in die Service-Schicht verlagert. Bedingt dadurch muss, wenn im Prozess doch mal ein komplettes Objekt benötigt wird, dieses immer über einen Service geladen werden, um den neuesten Stand zu erhalten.

Es gibt also Services, die zu einer ID ein Objekt laden und zurückgeben. Solche Services dürfen sich in diesem Szenario nicht idempotent verhalten. Wenn seit dem letzten Laden aus dem Prozess heraus ein Objekt durch andere Services manipuliert wurde, will der Prozess bei erneutem Laden keinesfalls dasselbe Ergebnis erhalten wie beim letzten Mal, sondern den neuen Zustand.

Hier könnte man noch argumentieren, dass durch geschicktes Bilden der Call-ID ein erneuter Aufruf erzwungen werden kann, indem dieser der Kontext, in dem der Service im Prozess aufgerufen wird, hinzugefügt wird. In folgendem Szenario ist das aber nicht mehr oder nur noch mit unvertretbar hohem Aufwand möglich:

Szenario 2: Kompensation und Wiederaufsetzen von Prozessinstanzen

Wir verwenden aus Prozesssicht ein Optimistic Lock-Verfahren. Während einer fachlichen Transaktion, die sich über mehrere Services erstreckt, gehen wir erst einmal davon aus, dass es keine konkurrierenden Änderungen geben kann. Am Ende der Transaktion prüfen wir dann, ob es doch konkurrierende Änderungen gab und kompensieren dann ggf. die Transaktion. Der Prozess setzt dann an einer früheren Stelle wieder auf und führt die Verarbeitung erneut durch.

Dabei kommt es zwangsläufig zu wiederholten Serviceaufrufen mit exakt demselben fachlichen Kontext! Auch dann dürfen sich die Services nicht idempotent verhalten, denn es kann nicht davon ausgegangen werden, dass erneute Verarbeitung tatsächlich zum selben Ergebnis führt.

Idempotenz ist eine Fallentscheidung

Dieser Blogartikel ist aber mitnichten ein Plädoyer gegen Idempotenz an sich. Es ist unbedingt erforderlich, dass sich Services idempotent verhalten, wenn dies angebracht ist. Und genau das muss individuell für jeden Service entschieden werden. Soll ein Service idempotent sein, so muss er diese Idempotenz selbst implementieren und gegebenenfalls vor der Verarbeitung selbst prüfen, ob die diese bereits durchgeführt wurde.

Generelle Idempotenz-Frameworks scheiden somit von vornherein aus, denn sie verallgemeinern die Entscheidung über Idempotenz und verlagern darüber hinaus einen Teil der Verantwortung zum Service-Consumer, der, wie in unserem Beispiel, durch das Bilden der Call-ID beeinflussen kann, wie sich der Service verhält, ohne das dies transparent wäre.

Über den Autor

Avatar

Ich bin Senior Consultant bei Holisticon und beschäftige mich dort vor allem mit BPM/SOA und Themen rund um Prozessorientierung und -automatisierung. Dabei interessieren mich besonders alle Dinge an der Schnittstelle zwischen Business und IT.

Ein Kommentar

  1. Hi Stefan,

    sehr wertvoller Beitrag. Wenn Idempotenz gewünscht ist, ist es gar nicht so einfach zu Implementieren. An dieser Stelle merkt man ganz deutlich, was an einem SOAP über HTTP so schief ist. HTTP Protokoll bietet eine elegante und einfache Möglichkeit an Caching zu implementieren, welche von den REST-FUll Services sehr einfach genutzt werden kann. Dadurch dass jedoch die SOAP Anfrage HTTP lediglich als Transport nutzt, kann man mit HTTP-Caching wenig anfangen und muss den gesamten Stack aufrufen bis hin zu deserialistem Datenoobjekt, um von einem Interzeptor feststellen zu lassen, dass der Service bereits aufgerufen wurde, der Parameter in einem Cache liegt und dazu bereits eine Antwort generiert wurde, die zurückzugeben ist.

    Gruß

    Simon

Antwort hinterlassen