Idempotenz

Eine idempotente Abfrage verändert keine Daten. Jede Abfrage sollte idempotent sein. Wenn das nicht möglich ist handelt es sich wahrscheinlich um keine Abfrage sondern einen Befehl.

Ein Befehl führt in der Regel zu veränderten Daten. Die Veränderungen finden unter Umständen nicht nur innerhalb der Software sondern auch bei externen Diensten statt. Im Gegensatz zu Abfragen bedeutet es bei Befehlen etwas anderes wenn von Idempotenz gesprochen wird.

Ein Befehl führt natürlich zu Veränderung, aber nur beim ersten Mal. Wenn der selbe Befehl mehrfach entgegen genommen wird hat nur der erste Auswirkungen.

Diese Eigenschaft ist in der Praxis für robuste Systeme sehr nützlich. Bei einem Implementierungsfehler wodurch ein Befehl mehrfach ausgelöst wird entsteht kein Schaden. In einer Situation mit schlechter Netzwerkverbindung können Befehle erneut gesendet werden. Das passiert zum Beispiel auch automatisch durch einen Webbrowser. Diese Wiederholung von HTTP-Requests führt ohne idempotente Befehle zu Problemen.

Implementierung

Folgender Ansatz funktioniert nur wenn eine einzige, ACID kompatible, Datenspeicher verwendet wird und es zu keinen sonstigen Datenveränderungen in externen Diensten kommt. Wenn diese Voraussetzungen nicht gegeben sind muss mit einem individuellen Wiederherstellungs-Prozess gearbeitet werden.

In einer Situation mit den beschriebenen Voraussetzungen ist der erste Schritt das garantieren einer sequenziellen Abarbeitung von identischen Befehlen. In PostgreSQL kann das mit dem Isolations Level Serializable von Transaktionen erreicht werden. (Relevante Dokumentation von PostgreSQL.)

Der zweite Schritt ist das erkennen dass ein Befehl bereits ausgeführt wurde. In diesem Fall wird das ausführen des Befehls übersprüngen und stattdessen sofort die Erfolgsantwort zurückgegeben.

Im folgenden Beispiel ist Pseudoquellcode für einen fiktiven HTTP-Endpunkt zur Registrierung eines Accounts zu sehen.

post '/accounts' do |request|
  email = request.parameters.fetch(:email)

  DB.transaction(isolation: :serializable) do
    account = Account.find_by(email: email)

    if account.exists?
      return successful_account_registration(account)
    else
      account = Account.create(email: email)

      return successful_account_registration(account)
    end
  end
end

Literatur