Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Technischer Idempotenzmechanismus für Services

Idempotenz wird bei technischen Problemen wie Timeouts oder weil beim Messaging nur At-Least-Once-Delivery garantiert werden kann, benötigt: aufgrund von Retries wird dabei derselbe identische Request erneut ausgeführt. Dies ist notwendig, damit beispielsweise nicht doppelt gebucht wird. Es muss also sichergestellt werden, dass das Ergebnis bei mehrfacher Ausführung des Request identisch zu einfacher Ausführung ist.

Details siehe: Idempotenz (Idempotency).

Vor allem im Rahmen von Massentests ist aufgefallen, dass an einigen Stellen die angestrebte Idempotenz in Bezug auf Seviceaufrufe nur bedingt eingehalten wird. Die primären Gründe hierfür sind die inhärente Komplexität des Themas ansich (z.B. das Verhalten bei überlappenden Service-Aufrufen, eine komplexe Domäne.) und Fehlannahmen / Kommunikationsprobleme im Bezug auf die korrekte Identifikation von idempotenten Aufrufen (-> woran erkenne ich, dass zwei Serviceaufrufe inhaltlich identisch sind?).

Hiermit soll eine Hilfe bereitgestellt werden, mit der die Idempotenzhandhabung generell genormt und vereinfacht werden kann. Die Lösung soll einfach an bereits bestehende Implementierungen angeschlossen werden können, allerdings auch mit zukünftigen Entwicklungen (z.B. Commanding) zusammenspielen

Wir sehen das Hauptproblem, wo Idempotenzverletzungen stattfinden, zur Zeit im Rahmen der Eventabarbeitung (Business Event Dispatcher), wollen aber auch eine Lösung, die im Rahmen von explitizen Serviceaufrufen mit Retries (z.B: durch Polly Policies) funktioniert, schaffen.

Design

Das Design der Idempotenz ist in Implementierungskonzept / Analyse: Idempotenz in Serviceaufrufen im Detail erläutert. Es gibt analog zu DeferredBusinessEvents zwei Anschlussmöglichkeiten: Einen expliziten Anschluss mittels IIdempotencyController und Dependency Injection sowie einen impliziten Anschluss mittels AOP.

Beide Ansätze werden im folgenden beschrieben.

Kamoto generiert immer bei jeder Schnittstelle alle notwendigen Typen zur Unterstützung von Idempotenz in die WSDL. Z.B. den IdempotentOperationPendingFaultContract und dem IdempotencyKey im Header.

Daher darf kein eigenes Attribut namens IdempotencyKey im Service / KSM definiert werden! Dies würde zu einem WSDL-Generierungsfehler in Kamoto sorgen.

Grundidee
  • Service-Aufrufe werden über einen eindeutigen Identifier (Idempotenzschlüssel) gekennzeichnet, so dass sie unterscheidbar sind.
  • Am Anfang von einem Service-Aufruf wird geprüft, ob zuvor innerhalb einer definierbaren Zeitspanne bereits ein Aufruf mit dem gleichen Idempotenzschlüssel stattgefunden hat. Im Fachschema des entsprechenden Landes wird dazu die Tabelle IdempotentRequestInfo angelegt, in der Infos über die Aufrufe idempotenter Service-Operationen und die jeweiligen Ergebnisse gespeichert werden können.
  • Falls die Tabelle IdempotentRequestInfo keinen Datensatz für den Idempotenzschlüssel enthält, wird zunächst ein Datensatz hinzugefügt, dann der Fachcode ausgeführt und letztlich der Datensatz mit den Ergebnis aktualisiert, bevor dieses an den Aufrufer zurückgegeben wird.
  • Falls die Tabelle IdempotentRequestInfo einen Datensatz mit Ergebnis für den Idempotenzschlüssel enthält, wird der Fachcode nicht erneut ausgeführt und stattdessen das Ergebnis aus der Datenbank zurückgegeben.
  • Falls die Tabelle IdempotentRequestInfo einen Datensatz ohne Ergebnis für den Idempotenzschlüssel enthält, bedeutet dies, dass der erste Service-Aufruf noch nicht abgeschlossen ist. In diesem Fall bekommt der zweiten Aufrufer eine IdempotentOperationPendingException mit der Hinweis, es zu einem späteren Zeitpunkt erneut zu versuchen.
  • Ein Datensatz ohne Ergebnis ist Default-mässig 10 Minuten gültig.
  • Ein Datensatz mit Ergebnis ist Default-mässig 1 Tag gültig.
  • Abgelaufene Datensätze werden von der API nicht berücksichtigt.
  • Ein Scheduled-Service namens Schleupen.CS.PI.SB.Idempotency.ClearExpiredIdempotentRequestsAggregatingActivityService löscht die nicht mehr gültigen Datensätze. Der Scheduled-Service lauft einmal pro Stunde.
Nutzung bei Service-Kaskaden

Nutzt ein Service die bereitgestellte Idempotenz-API und ruft gleichzeitig selbst weitere Services auf, gibt es Konstellationen, in denen die Idempotenz nicht wie gewünscht erreicht wird. Beispielsweise ist dies der Fall, wenn kaskadierend schreibende Operationen weiterer Services aufgerufen werden und nach dem Abschluss dieser Aufrufe Fehler auftreten. Bei einem Retry kann dann im Allgemeinen nicht gewährleistet werden, dass die schreibenden Operationen untergeordneter Services zu einem identischen Verhalten führen.

Aus diesem Grund werden die folgenden Faustregeln zum kaskadierenden Aufruf von weiteren Services vorgeschlagen:

  • Lesende Service-Operationen können aus einem Service mit Anschluss der Idempotenz-API ohne Einschränkungen aufgerufen werden. Lesende Aufrufe sind stets idempotent.
  • Der Zugriff auf schreibende Service-Operationen innerhalb eines schreibenden Service ist nicht erlaubt.
  • Alternativ sollte die Struktur des Service und dessen Eingliederung in die Architektur überdacht werden. Eine Hilfestellung dazu bietet der Wiki-Artikel Entscheidungsbaum zur Implementierung resilienter Anwendungen.

Implementierung

Schritte zum Anschluss - Checkliste
  • Verweis auf die folgenden Pakete im Services-Projekt hinzufügen
    • Schleupen.CS.PI.SB.Idempotency
    • Schleupen.CS.PI.SB.Idempotency.Castle3
  • Verweis auf folgendes Paket im BusinessCases-Projekt hinzufügen
    • Schleupen.CS.PI.SB.Idempotency
  • Castle Konfiguration im ServiceHostConfigurator um den folgenden Codeschnipsel ergänzen ergänzen:
    windsorContainer.Install(new IdempotencyInstaller(IsInsideWcfOperation));
  • Unterstützte Version der HV20 DevelopmentTools installieren (>= 3.25.2.17)
  • Unterstützte Version vom PackageBuilder referenzieren (>= 3.21.2.91) (per Paket Update aktualisieren)
  • Datenbankpatch einbinden
  • IdempotencyController in Service Operation anschliessen oder den impliziten Anschluss nutzen
  • IdempotentOperationPendingFaultAssembler im ErrorHandlerBehavior-Attribut von Services angeben, die idempotente Operationen implementieren.
  • IdempotentOperationPendingFault ist in der WSDL-Datei nur vorhanden, wenn die WSDL mit Kamoto neugeneriert wird.
  • Alternativ, kann auch der UnhandledFaultAssembler verwendet werden.
Explizit mit IdempotencyController
[ServiceBehavior(IncludeExceptionDetailInFaults = true, TransactionIsolationLevel = IsolationLevel.ReadCommitted)]
[ErrorHandlerBehavior(typeof(IdempotentOperationPendingFaultAssembler<IdempotentOperationPendingFaultContract>),
    ...,
    typeof(UnhandledFaultAssembler<UnhandledFaultContract>))]
public sealed class BuchEntityService : IBuchEntityService
{
    private readonly IIdempotencyController idempotencyController;
    public BuchEntityService(IIdempotencyController idempotencyController, ...)
    {
        this.idempotencyController = idempotencyController;
        // ...
    }

    // ...

    [OperationBehavior(TransactionScopeRequired = true)]
    public CreateResponse Create(CreateRequest request)
    {
        if (request == null) { throw new ArgumentNullException(nameof(request)); }
        return idempotencyController.WithScope(
           request.IdempotencyKey,
           () =>
            {
                // Fachcode
                return new CreateResponse { ... };
            },
          OperationContext.Current?.IncomingMessageHeaders.Action /* Kennzeichnet die Service-Operation eindeutig */,
          IdempotencyMode.Optional /* optionaler Parameter (Default: IdempotencyMode.Optional) */);
    }.
 }

Zusätzlich zur Signatur mit dem IdempotencyKey als String kann, falls das Command Pattern genutzt wird, eine Implementierung von IIdempotentRequest angegeben werden, die den IdempotencyKey als Property bereitstellt.

Für Oneway Services gibt es auch einen Overload der WithScope()-Methode, der eine Action statt einer Func<T> akzeptiert.

Der IdempotencyKey wird analog zum Session Token im SOAP-Header des WCF-Aufrufs übertragen und kann bei Bedarf auch durch den Aufrufer bestimmt werden, sofern die WSDL mit einer hinreichend neuen Kamoto-Version erstellt wurde (s. Schritte zum Anschluss - Checkliste). Wenn im Service beim Aufruf des IdempotencyControllers der Modus IdempotencyMode.Optional (Default) verwendet wird, darf der IdempotencyKey vom Aufrufer leer gelassen werden, woraufhin der Idempotenzmechanismus ignoriert und der Service ganz normal aufgerufen wird. Im Modus IdempotencyMode.Required führt ein leerer IdempotencyKey hingegen zu einer Exception.

Implizit per AOP

Ein OperationBehavior-Attribut kann mit dem IdempotentOperationBehavior-Attribut annotiert werden, um einen impliziten Idempotenzanschluss zu erreichen.

[ServiceBehavior]
[ErrorHandlerBehavior(typeof(IdempotentOperationPendingFaultAssembler<IdempotentOperationPendingFaultContract>),
    ...,
    typeof(UnhandledFaultAssembler<UnhandledFaultContract>))]
internal class BuchEntityService
{
    [IdempotentOperationBehavior]
    // ...
    public CreateResponse Create(CreateRequest request)  // CreateRequest hat im Header den IdempontencyKey
    {
        // ...
        return new CreateResponse(...);
    }
}

Auf dem Attribut sind zudem die folgenden optionalen Parameter vorhanden:

  • IdempotencyMode
    Gibt an, ob Idempotenz optional ist (default) oder verlangt wird.
  • OperationName
    Kennzeichnet den Namen der Operation; falls nicht angegeben wird <ServiceName>.<MethodenName> verwendet.
  • ExecutionExpirationTimeSpan
    Gibt die Ausführungszeit der Operation an. Wenn innerhalb dieser Zeit ein erneuter Aufruf mit gleichem IdempotencyKey durchgeführt wird, wird die Operation nicht erneut ausgeführt. Der Default liegt bei 10 Minuten.
  • ResultExpirationTimeSpan
    Gibt an, wie lange das Ergebnis einer Operation vorgehalten werden soll. Solange dieses gespeichert ist, wird die Operation nicht erneut ausgeführt, wenn der Service mit dem entsprechenden IdempotencyKey aufgerufen wird. Der Default liegt bei einem Tag.

Welche Services benötigen diese Attributierung?

Operationen, die per se idempotent sind, benötigen diese Attributierung nicht:

  • Query-Operationen
  • Delete-Operation

Einen Sonderfall nehmen hierbei Update-Operationen ein, die per se auch idempotent sind (es ist ja egal wie oft bspw. das Feld Titel gesetzt wird, das Resultat ist stets dasselbe). Dennoch sollte hier die Idempotenz-API angeschlossen werden, damit nicht unnötig viele Events ausgelöst werden!

Für allen anderen schreibenden Operationen sollte also die API angeschlossen werden.

Es wird davon ausgegangen, dass auf dem Request das Feld IdempotencyKey existiert, dessen Wert für die Identifikation von gleichen Service-Aufrufen genutzt werden kann. Das ist zum einen der Fall, wenn die WSDL mit einer aktuellen Version von Kamoto erzeugt wurde. Zum anderen generieren der Business Event Dispatcher sowie PowerShell-Clients den Header automatisch, falls sie ein Request-Objekt verarbeiten, bei dem der Header nicht existiert.

Spezifischer FaultAssembler

Für den korrekten Anschluss der Idempotenz-API im Land ist es sinnvoll, den IdempotentOperationPendingFaultAssembler<> wie im obigen Beispiel dargestellt im ErrorHandlerBehavior derjenigen Services, die die Idempotenzmechanismen nutzen, anzugeben. Nur so ist gewährleistet, dass ein stark typisierter Fehler an den Aufrufer gemeldet werden kann, wenn eine Operation mit einem bereits bekannten Idempotenzschlüssel angestoßen werden soll. Anhand der IdempotentOperationPendingException kann der Client erkennen, dass die Operation bereits durch einen anderen Aufrufer angestoßen wurde und das Ergebnis - sofern kein Fehler auftritt - zu einem späteren Zeitpunkt zur Verfügung stehen sollte.

Der IdempotentOperationPendingFault ist in der WSDL nur vorhanden, wenn die WSDL mit Kamoto neugeneriert wird. Falls es nicht gewünscht ist die WSDL neu zu generieren, kann alternativ der UnhandledFaultAssembler verwendet werden. In diesem Fall, muss der Client in der InnerException des UnhandledFault schauen, um zu erkennen, dass die Operation bereits durch einen anderen Aufrufer angestoßen wurde.
Um auf den stark typisierter Fehler reagieren zu können, muss auch der Client die neue WSDL referenzieren. Alternativ, kann der Client entsprechend die Fehlermeldung analysieren.

Retries im Powershell-Client und innerhalb der Infrastruktur (Business Event Dispatcher und Workflows) können entsprechend mit typisierten und untypisierten Fehlermeldungen umgehen.

Sofern die generischen Service-Tests aus dem Framework im Land angeschlossen sind, wird automatisch geprüft, ob der spezifische FaultAssembler bei allen Services mit idempotenten Operationen angegeben ist. Sollte dies nicht der Fall sein, schlagen die generischen Service-Test mit einem entsprechenden Hinweis fehl. Ein Service kann sehr leicht von dem Test ausgeschlossen werden, in dem die Methode FilterTypesForIdempotentOperationPendingFaultAssemblerSet überschrieben wird.

Castle Konfiguration

Jedes Land, das den technischen Idempotenzmechanismus in seinen Services verwenden möchte, muss analog zu DeferredBusinessEvents einen Activity-Service bereitstellen, der dafür sorgt, dass die Idempotenzdaten mit abgelaufender Gültigkeit in der landspezifischen Datenbanktabelle gelöscht werden. Dieser Services wird durch den IdempotencyInstaller bereitgestellt, so dass nur die folgenden Schritte zu erledigen sind:

  • Verweis auf Schleupen.CS.PI.SB.Idempotency hinzufügen
  • Verweis auf Schleupen.CS.PI.SB.Idempotency.Castle3 hinzufügen
  • Castle-Konfiguration um den folgenden Codeschnipsel ergänzen
    Beispiel aus BIB:
public class ServiceHostFactoryConfigurator : WcfContainerConfigurator
{
    protected override void OnContainerConfigured(IWindsorContainer windsorContainer)
    {
        // ...
        windsorContainer.Install(new IdempotencyInstaller(IsInsideWcfOperation));
        // ...

        base.OnContainerConfigured(windsorContainer);
    }
}
Datenbankpatch

Pro Datenbankschema im Land muss eine IdempotentRequestInfo-Datenbanktabelle erstellt werden.
Hierfür kann ein Datenbankpatch nach der Vorlage mit dem folgenden SQL verwendet werden, wobei [XXX] durch das Landschema zu ersetzen ist.

CREATE TABLE [XXX].[IdempotentRequestInfo] (
    [Id] [uniqueidentifier] NOT NULL,
    [ClusterId] [INT] IDENTITY(1, 1) NOT NULL,
    [IdempotencyKey] [VARCHAR](256) NOT NULL,
    [RequestResult] [VARCHAR](MAX) NULL,
    [ValidUntil] [datetimeoffset](0) NOT NULL,
    [Operation] [VARCHAR](256) NULL,
    [ERSTELLT] DATETIMEOFFSET(0) NULL CONSTRAINT [d_IdempotentRequestInfo_ERSTELLT] DEFAULT SYSDATETIMEOFFSET(),
    [GEAENDERT] DATETIMEOFFSET(0) NULL,
    CONSTRAINT [p_IdempotentRequestInfo] PRIMARY KEY NONCLUSTERED ([Id] ASC),
    CONSTRAINT [u_IdempotentRequestInfo_ClusterId] UNIQUE CLUSTERED ([ClusterId] ASC),
    CONSTRAINT [u_IdempotentRequestInfo_IdempotencyKeyAndOperation] UNIQUE NONCLUSTERED (
        [IdempotencyKey] ASC,
        [Operation] ASC
        )
    );

CREATE TRIGGER [XXX].t_IdempotentRequestInfo_update_GEAENDERT ON [XXX].IdempotentRequestInfo
FOR UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE [XXX].IdempotentRequestInfo
    SET GEAENDERT = SYSDATETIMEOFFSET()
    FROM INSERTED i
    JOIN [XXX].IdempotentRequestInfo t
        ON t.Id = i.Id;
END;

REVOKE ALL PRIVILEGES
    ON "XXX".IdempotentRequestInfo
    FROM PUBLIC AS "csdbadmin";

GRANT SELECT
    , INSERT
    , UPDATE
    , DELETE
    ON "XXX".IdempotentRequestInfo
    TO csdbworker AS "csdbadmin";

GRANT SELECT
    ON "XXX".IdempotentRequestInfo
    TO csdbreader AS "csdbadmin";

Abgrenzung

Idempotenz sorgt dafür, dass (Service-)Operationen nicht mehrmals kurz hintereinander oder sogar überlappend aufgerufen werden. Vor allem im Rahmen der Eventzustellung kann es leicht zu Konstellationen kommen, wo langlaufende Services mit den Retries des Business Event Dispatchers nach clientseitigen Timeouts zu überlappenden Aufrufen führen, wodurch Idempotenz aufgrund der Komplexität fast nur noch durch Locking Mechanismen erreicht werden kann. Wir stellen mit dem Idempotenz-Framework eine Möglichkeit bereit, mit wenig Aufwand die technische Idempotenz zu erreichen, also die Wahrscheinlichkeit kurz hintereinander oder fast gleichzeitig auftretende Service-Operationen mit gleichem Effekt zu vermeiden. Die Implementierung von fachlicher Idempotenz ist nach wie vor notwendig und muss auf der logischen Ebene durch den Service selbst sichergestellt werden.

Wenn im Folgenden von Idempotenz bzw. idempotenter Operationen gesprochen wird, ist damit stets die technische Idempotenz gemeint.

Mithilfe von technischer Idempotenz kann man immer sicher die Idempotenz implementieren - es ist ein technisches Problem. Zudem: Idempotenz wird aus technischen Gründen (Retries, At-Least-Once-Garantien bei der Messagezustellung) benötigt!

Cookie Consent mit Real Cookie Banner