Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Wiederholend wie ein Papagei: Polly

Viele Probleme der Systemtechnik lassen sich durch wiederholtes Versuchen oder durch Rate Limiting lösen. Die .NET-Bibliothek Polly stellt Entwicklern hierfür verschiedenste Maßnahmen zur Resilienz-Steigerung in Form von schlüsselfertigen Bausteinen zur Verfügung.
© EMGenie

Die in Polly implementierten Resilienzmethoden sind, wie in Bild 1 gezeigt, in zwei Gruppen unterteilt. Reaktive Verfahren analysieren das Verhalten der Payload und reagieren auf dieses. Ein klassisches Beispiel wäre das mehrfache Wiederholen des Tasks, um seine erfolgreiche Erledigung zu erzwingen.

Proaktive Verfahren versuchen stattdessen, die Ausführung der Payload nur unter bestimmten Bedingungen freizugeben. Intention ist hier die Schaffung einer Ausführungsumgebung, in der das System die „maximale Erfolgschance“ annimmt.

Polly implementiert reaktive und proaktive Methoden zur Steigerung der Resilienz (Bild 1)

Polly implementiert reaktive und proaktive Methoden zur Steigerung der Resilienz (Bild 1)

© Autor

Inbetriebnahme von Polly

Nach diesen einführenden Überlegungen starten wir zunächst Visual Studio. Als Experimentalbasis soll eine Applikation auf Basis der Vorlage Konsolen-App dienen, die den Namen NMGPollyT1 erhält. Da das Testen von Resilienz-Strategien voraussetzt, dass wir auch fehlschlagende Payloads erzeugen, sollten Sie die Checkbox „Keine Anweisungen der obersten Ebene verwenden“ aktiviert lassen. Nur in diesem Fall generiert Visual Studio bei Verwendung aktueller .NET-Framework Versionen (hier 9.0) ein klassisches Projektskelett mit vollständiger Klassenstruktur.

Im nächsten Schritt folgt der Wechsel in die NuGet-Konsole, wo wir uns im ersten Schritt für das Paket Polly.Core entscheiden. Aus der breiten Vielzahl der in Polly implementierten Funktionen folgt, dass es zum Zeitpunkt der Abfassung dieses Artikels ein knappes halbes Dutzend an Paketen gibt – Bild 2 zeigt, was zur Verfügung steht.

Im Polly-Ökosystemen, da wimmelt es (Bild 2)

Im Polly-Ökosystemen, da wimmelt es (Bild 2)

© Autor

In den folgenden Schritten wollen wir die Version 8.6.6 des Programms verwenden. Die genaue Detailversion spielt dabei keine Rolle – achten Sie nur darauf, nicht versehentlich die ältere Version 7.x zu erwischen.

Zunächst ist die Erzeugung einer Resilienz-Pipeline erforderlich. Dabei handelt es sich um eine Zusammenfassung von Logiken, die Polly die zuverlässige Ausführung der diversen Tasks ermöglicht.

Die Einrichtung der Resilienz-Pipeline erfolgt im Prinzip in einem zweistufigen Prozess. Als Erstes wird ein Pipeline-Builder verwendet, die für das Einschreiben der diversen Optionen verantwortlich ist. Im zweiten Schritt wird eine Payload-Ausführung angewiesen:

 

static async Task Main(string[] args)
{
  ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions())
    .AddTimeout(TimeSpan.FromSeconds(10))
    .Build();
    await pipeline.ExecuteAsync(async token => await worker());
}

 

Die eigentliche Payload ist dann gemäß dem nachfolgenden Schema aufgebaut. Der Gedanke dabei ist, dass die erste Ausführung fehlschlägt, während die darauffolgenden Versuche das Zurückgeben der Erfolgs-Konstante ermöglichen:

 

internal class Program {
    static int counter = 0;
    static ValueTask worker() {
        counter++;
        if (counter > 1) {
            Console.WriteLine("Erfolg");
            return ValueTask.CompletedTask;
        }
        Console.WriteLine("Fehlschlag");
        return ValueTask.FromException(new EndOfStreamException()); 
}

 

Die tatsächliche Programmausführung präsentiert sich dann wie in Bild 3 gezeigt.

Die Payload wird trotz Polly-Integration nur einmal ausgeführt (Bild 3)

Die Payload wird trotz Polly-Integration nur einmal ausgeführt (Bild 3)

© Autor

Vom ValueTask

Bei der Arbeit mit Polly trifft man immer wieder auf die ValueTask-Klasse. Im Microsoft DevBlog findet sich eine durchaus interessante Einführung, in der Microsoft die Ursachen und Gedanken hinter der Entwicklung dieser Task-Unterart im Detail beschreibt.


Bestimmung der Catch-Kriterien von Polly

Die von Haus aus aktivierte beziehungsweise eingeschriebene Standard-Konfiguration der Retry-Strategie ist insofern „besonders“, als sie keinerlei Exceptions abfängt. Wer mit Polly Fehler abfangen möchte, muss das System – Analogien zu Try-Catch-Blöcken sind hier nicht zufällig – im ersten Schritt darüber informieren, welche Art von Exception als Fehlschlag anzusehen ist.

Im Fall unseres vorliegenden Programmbeispiels würde die folgende Änderung zu einer ausführbaren Version führen:

 

ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle<EndOfStreamException>(),
        MaxRetryAttempts = 3,
        Delay = TimeSpan.Zero
    })

 

Die folgende Zeile informiert Polly dabei darüber, dass EndOfStreamException-Exceptions den Fehlschlag der Programmausführung anzeigen;

 

ShouldHandle = new PredicateBuilder().Handle<EndOfStreamException>(),

 

Die Retry-Strategie probiert in diesem Fall die abermalige Ausführung der Payload. Als Resultat ergibt sich das in Bild 4 gezeigte Programmverhalten.

Polly wiederholt die Ausführung, bis „alles am Platz“ ist (Bild 4)

Polly wiederholt dieAuo Ausführung, bis „alles am Platz“ ist (Bild 4)

© Autor

Ein interessantes Verhalten zeigt sich, wenn man der Payload – beispielsweise durch Auskommentieren des Inkrementierungs-Befehls – die Möglichkeit nimmt, irgendwann erfolgreich abgearbeitet zu werden:

 

static ValueTask worker() {
     //counter++;
     if (counter > 1)

 

Das Ergebnis ist, dass das Programm nun mit einer Exception scheitert – wir sehen das in Bild 5 gezeigte Verhalten.

Nach dem vierten Durchlauf schlägt die Exception ins Hauptprogramm durch (Bild 5)

Nach dem vierten Durchlauf schlägt die Exception ins Hauptprogramm durch (Bild 5)

© Autor

Ursache dafür ist, dass ein neu ins Leben gerufenes Retry-Objekt nur drei Wiederholungen erlaubt – die Zeile MaxRetryAttempts = 3 wäre also in diesem Fall theoretisch optional. Nach dem vierten Fehlschlag zieht sich Polly aus der Verarbeitung zurück und wirft die von der Payload erzeugte Exception wieder in das Hauptprogramm zurück, das danach an ihr stoppt.

Polly stellt dem Entwickler eine  zur Verfügung, die hier schon aus Platzgründen nicht komplett besprechen können. Der Autor möchte allerdings noch die folgende Parametrisierung zeigen, die eine exponentielle Verzögerung implementiert. In der Praxis bedeutet dies, dass die individuellen Fehlschläge immer seltener auftreten, was einer überlasteten Cloud- oder Server-Ressource Gelegenheit zur Regeneration geben soll:

 

.AddRetry(new RetryStrategyOptions
{
    ShouldHandle = new PredicateBuilder().Handle<EndOfStreamException>(),
    BackoffType = DelayBackoffType.Exponential,
    Delay = TimeSpan.FromSeconds(2),
    UseJitter = true
})

On Error Goto Hell

In Praxis-Systemen gibt es immer wieder Situationen, in denen nach einigen Fehlschlägen ein permanenter Ausfall anzunehmen ist. Zufriedene Benutzer erreicht man als Entwickler in diesem Fall naturgemäß nur dann, wenn das Programm zumindest ein grundlegendes Verhalten (im Embedded-Bereich spricht man gerne von Limp Mode) aufrechterhalten kann.

In Polly ist dies über ein als Fallback bezeichnetes Verfahren implementiert. Im Prinzip macht es das, was man vom Namen her annehmen würde: Nach einer bestimmten Anzahl von Fehlschlägen wird eine Standard-Handlung ausgeführt beziehungsweise ein Standard-Wert zurückgegeben.

Die einfachste Spielart orientiert sich dabei an der Überschrift dieses Abschnitts: Tritt ein Fehlschlag auf, so wird ein alternativer Wert zurückgeliefert. Hierzu ist die folgende Anpassung an der Programmstruktur erforderlich:

 

static async Task Main(string[] args)
{
    var optionsField = new FallbackStrategyOptions<String>
    {
        ShouldHandle = new PredicateBuilder<String>()
            .Handle<EndOfStreamException>(),
        FallbackAction = static args => Outcome.FromResultAsValueTask("NOTFALL")
    };
    ResiliencePipeline<String> pipeline = new ResiliencePipelineBuilder<String>()
        .AddFallback(optionsField)
        .Build();
String result = await pipeline.ExecuteAsync(async token => await worker());
Console.WriteLine(result);

 

Über das FallbackAction-Feld legen wir fest, welcher Code im Fall eines Fehlschlags des eigentlichen Tasks zur Ausführung zu bringen ist. Da unser hier verwendeter Task als Fallback-Wert einen String verwendet, sind Anpassungen in der Resilienz-Pipeline erforderlich. Besonders wichtig ist es, dass verschiedenste Objekte fortan mit einer generischen Typ-Spezifikation auszustatten sind:

 

static ValueTask<String> worker() {
    if (counter > 1) {
        Console.WriteLine("Erfolg");
        return ValueTask.FromResult("ERFOLG!");
    }

 

Die eigentliche Ausführung muss sich fortan nach folgendem Schema auch um das Abernten der von der Polly-Pipeline generierten Werte kümmern:

 

    Console.WriteLine("Fehlschlag");
    return ValueTask.FromException<String>(new EndOfStreamException()); 
}

 

Zu guter Letzt sind weitere Anpassungen im Bereich der fehlschlagenden Payload erforderlich. Der ValueTask muss nun ja nicht mehr nur Erfolg oder Fehlschlag, sondern auch den jeweiligen Rückgabewert verarbeiten.

Im Ergebnis erhalten wir nun das in Bild 6 gezeigte Programmverhalten.

Der Task scheitert einmal und wird danach eliminiert (Bild 6)

Der Task scheitert einmal und wird danach eliminiert (Bild 6)

© Autor

Einer der stärksten Aspekte des Polly-Frameworks ist die Möglichkeit, die verschiedenen in Bild 1 gezeigten Resilienzstrategien zu kombinieren.

Im Fall des vorliegenden Programms wäre es beispielsweise wünschenswert, eine Retry- und eine Fallback-Strategie zu kombinieren. Dies lässt sich durch eine nach folgendem Schema aufgebaute Resilienz-Pipeline bewerkstelligen:

 

ResiliencePipeline<String> pipeline = new ResiliencePipelineBuilder<String>()
    .AddFallback(optionsField)
    .AddRetry(new(){
        ShouldHandle = new PredicateBuilder<String>().Handle<EndOfStreamException>(),
        MaxRetryAttempts = 3,
    })
    .Build();

 

Daraus resultiert das in Bild 7 gezeigte Programmverhalten.

Resilienz und Fallback im Doppelteam (Bild 7)

Resilienz und Fallback im Doppelteam (Bild 7)

© Autor

Im ersten Schritt versucht die Retry-Logik, das vorliegende Programm mehrmals zur Ausführung zu bringen – dies führt zur Ausgabe der ersten drei Fehlermeldungen. Nach dem Fehlschlag übernimmt der Fallback, was zur letzten Fehlermeldung und der dazugehörenden Notfall-Meldung führt.

Fazit

Wer eine der in der Einleitung genannten Resilienzstrategien in seiner Applikation einsetzen möchte, findet in Polly eine niederschwellige Implementierung. Dank der breiten Kompatibilität und der Flexibilität spricht nur wenig gegen den Einsatz, der zudem die eine oder andere Mannstunde sparen hilft.

Neueste Beiträge

PandaOS: Die Workstation, die ich mir immer gewünscht habe
Monatelang bin ich zwischen KI-Werkzeugen hin- und hergesprungen. Cursor, Claude Code, OpenCode, Aider. Jedes hatte etwas Geniales, doch keines fühlte sich vollständig an. Also habe ich ein eigenes Tool gebaut? PandaOS ist das Ergebnis.
6 Minuten
AI Slop vermeiden: Harness-First Engineering für Teams, die mit Coding Agenten arbeiten - Der neue Engpass heißt Vertrauen
Coding Agents wie GitHub Copilot, Cursor oder Claude Code haben die Produktivität von Entwicklungsteams dramatisch erhöht. Was früher Tage dauerte, entsteht heute in Stunden. Doch mit der Geschwindigkeit wächst ein neues Problem: Der Engpass liegt inzwischen im Vertrauen in das, was geschrieben wurde.
4 Minuten
4. Aug 2026
Health Checks und Kubernetes - Health Checks in ASP.NET richtig nutzen, Teil 4
In Kubernetes entscheidet ein Health Check nicht nur über Sichtbarkeit im Monitoring, sondern auch darüber, ob Traffic fließt oder ein Container neu startet. Hier zeigen wir, warum saubere Grenzen zwischen Liveness und Readiness in produktiven ASP.NET-Anwendungen so wichtig sind.
4 Minuten

Das könnte Dich auch interessieren

.NET-Anbindung für die Llama-KI - LLamaSharp
Open-Source-KI auf dem lokalen Gerät nutzen – mit handhabbarem Speicherbedarf und optionalem Zugriff auf externe Datenquellen.
8 Minuten
13. Jan 2025
Fünf freie .NET Decompiler - Mark Pelf, CodeProject
Der serbische Softwareentwickler Mark Pelf stellt bei CodeProject fünf freie .NET Decompiler vor.
2 Minuten
6. Mär 2023
Übersehen seit .NET 8: TimeProvider für testbare Zeit
Seit .NET 8 gibt es TimeProvider als offizielle Zeit-Abstraktion. Doch noch zweieinhalb Jahre später ist sie in vielen Projekten unbekannt.
6 Minuten
1. Jul 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige