Elasticitet i system med hjälp av Polly

Squeeds Julkalender | 2021-12-08 | Stefan Cronert
Bakom den här luckan ska jag beskriva ett bibliotek som kan hjälpa dig att skapa en mer elastisk, dynamisk och mer motståndskraftig hantering av störningar i kommunikationen med andra system, istället för att göra hemmasnickrade lösningar. Jag ska beskriva Polly, ett bibliotek som kan göra ditt system mer robust, genom devisen "bend, don't break", dvs mer elastisk,
macaw-gb2b38c9e3_1280.jpg

Inledning

Nu när vi bygger system som litar mer på tjänster som är baserade hos andra, så tappar vi kontroll över vad som händer.
Trots att miljön har blivit annorlunda så har jag fortsatt att skriva koden så som att jag har kontroll över den och inte behövt hantea problem annorlunda än om det är min kod som fallerar. De tillfällen då jag har förstått att så inte är fallet har jag skrivit en egen lösning, som ibland blir svår att förstå.

Polly ger oss möjlighet att ge användarna en bättre upplevelse utan en allt för stor arbetsinsats.

Policy

Policy är en klass som kapslar in den strategi när koden exekveras.
För att exekvera en kod finns det två metoder: Execute respektive ExecuteAndCapture.
Execute utför koden och returnerar det som policyn är satt att returnera, eventuella exception kommer att kastas och behövas hanteras, se exempel nedan.
Execute utför koden och returnerar PolicyResult av den typ som policyn är satt att returnera, den innehåller eventuella exception, se exempel nedan.

Time out

När vi väntar på någon eller något så väntar vi inte i alla evighet, vi har alla en gräns på hur länge vi känner att vi kan vänta på t.ex. bussen.
Många komponenter inom systemutveckling har timeout-inställning, t.ex. databaskopplingar och httpklienter, andra har inte det.

Polly ger oss möjlighet att lägga till time out på kod som inte har time out.


private static string
UseTimeOut(string uri,
int secondsUntilTimeout)
{
TimeoutPolicy<HttpResponseMessage> policy =
Policy.Timeout<HttpResponseMessage>(secondsUntilTimeout, TimeoutStrategy.Pessimistic, OnTimeOut);

PolicyResult<HttpResponseMessage> policyResult =
policy.ExecuteAndCapture(() => CreateResponseMessageFunc(uri).Invoke());

if (policyResult.FinalException != null)
{
//Handel exception if any
//Console.WriteLine(result.FinalException.Message);
}

var message = policyResult?.Result;
return message?.Content.ReadAsStringAsync().Result ?? string.Empty;
}

Exemplet ovan utför ett anrop till adressen i uri och om svaret inte kommer in den angivna antalet sekunder så kommer resultatet (PolicyResult) innehålla ett exception av typen TimeoutException och vi kan avbryta väntan och fortsätta att utföra programmet.

Retry

Ibland så är att avbryta anropet, utan vi vill kanske försöka flera gånger innan vi ger upp, t.ex. när en tjänst har somnat och behöver extra tid vid uppstart.


private static string
RetryTimesWithWait(string uri,
int times)
{
Policy<HttpResponseMessage> policy = Policy
.HandleResult<HttpResponseMessage>(m => m.StatusCode != HttpStatusCode.OK)
.WaitAndRetry(times, SleepDurationProvider, OnRetry);

var message = policy.Execute(CreateResponseMessageFunc(uri));
return message.Content.ReadAsStringAsync().Result;
}

I exemplet ovan så anropar vi adressen uri och om vi inte för ett svar med StatusCode OK så försöker den anropa ytterligare times gånger innan den "ger upp" och returnerar svaret från anropet.

Fallback

Om det varken går att hantera bristande kommunikation mha timeout eller att vi försöker flera gånger mot samma tjänst så kan vi ha en annan tjänst som går bra att använda, en fallback, kanske en intern tjänst.


private static string
Fallback(string uri,
string fallbackUri)
{
FallbackPolicy<HttpResponseMessage> policy = Polly.Fallback.FallbackPolicy<HttpResponseMessage>
.HandleResult(s => !s.IsSuccessStatusCode)
.Fallback(() => CreateResponseMessageFunc(fallbackUri).Invoke());

var message = policy.Execute(() => CreateResponseMessageFunc(uri).Invoke());
return message.Content.ReadAsStringAsync().Result;
}

I koden ovan så försöker vi anropa adressen uri, får vi inte tillbaka en statuskod som signalerar lyckat anrop så anropar vi den fallback-adressen i fallbackUri.

Cache

Om vi har en tjänst som går upp och ner lite stokastiskt och där datat är ganska så statiskt så kan det vara idé att låta Polly hantera en cache.


static readonly
MemoryCache MemoryCache = new MemoryCache(new MemoryCacheOptions());
static readonly MemoryCacheProvider MemoryCacheProvider = new MemoryCacheProvider(MemoryCache);

static readonly Func<Context, HttpResponseMessage, Ttl> CacheFilter =
(ctx,
msg) => new Ttl(
timeSpan: msg.StatusCode == HttpStatusCode.OK ? TimeSpan.FromMinutes(30) : TimeSpan.Zero,
slidingExpiration: true);

static readonly IAsyncPolicy<HttpResponseMessage> CacheFilterPolicy =
Policy.CacheAsync<HttpResponseMessage>(
MemoryCacheProvider.AsyncFor<HttpResponseMessage>(), //note the .AsyncFor<HttpResponseMessage>
new ResultTtl<HttpResponseMessage>(CacheFilter),
onCacheError: null
);

private static string CacheWithFilter(string uri)
{
var message =
CacheFilterPolicy.ExecuteAsync(ctx => Task.FromResult(CreateResponseMessageFunc(uri).Invoke()),
new Context(uri));
return message.Result.Content.ReadAsStringAsync().Result;
}

I koden ovan så har vi skapat en minnescache för att hantera HttpResponseMessage (det kan vara affärsobjekt eller andra klasser också). CacheFilterPolicy använder metoden CacheFilter för att bestämma hur länge som resultatet ska sparas i cachen, om StatusCode är lika med OK så sparas resultatet i 30 minuter, annars så sparas resultatet i 0 ticks, vilket innebär att den inte sparas alls.

När vi får tillbaka resultatet så sparas det i cachen och om vi gör ett nytt anrop inom 30 minuter som inte returnerar ett ok resultat så tas värdet från cachen och logiken kan fortsätta som om allt gått bra.
Cache-policy är en strategi som håller tillstånd (de cachade värden)

Wrapper

Det finns kanske tillfällen då vi vill kombinera olika strategier, t.ex. vi vill kanske ha två retries och om det inte kommer ett ok resultat använder vi en annan tjänst.
Då kan vi kombinera retry-strategin med fallback-strategin


private static string
Wrap(string uri,
string fallbackUri)
{
RetryPolicy<HttpResponseMessage> retryPolicy = Policy
.HandleResult<HttpResponseMessage>(m => !m.IsSuccessStatusCode)
.Retry(2, OnRetry);

var fallbackPolicy = Polly.Fallback.FallbackPolicy<HttpResponseMessage>
.HandleResult(s => !s.IsSuccessStatusCode)
.Fallback(() => CreateResponseMessageFunc(fallbackUri).Invoke());

var wrapPolicy = Policy.Wrap(fallbackPolicy, retryPolicy);

var message = wrapPolicy.Execute(CreateResponseMessageFunc(uri));

return message.Content.ReadAsStringAsync().Result;
}

I koden ovan så kommer vi först att applicera den mest vänstra policyn i wrap-uttrycket, dvs retry med två om försök, och när de två anropen misslyckas försöker koden att anropa tjänsten som är angiven i fallback-policyn

Circuit breaker

En mer avancerad policy är circuit breaker, som kan användas för att hindra att vi fortsätter att skicka anrop till ett system som inte svarar och därmed inte kan hantera flera anrop för tillfället, t.ex. kan vi acceptera att två anrop inte går fram, men sedan så väntar vi fem sekunder innan vi försöker igen att skicka anrop till den fallerande tjänsten.


static
Polly.CircuitBreaker.CircuitBreakerPolicy<HttpResponseMessage> BasicCircuitBreakerPolicy = Policy
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.CircuitBreaker(2, TimeSpan.FromSeconds(5));

private static string CircuitBreaker(string uri)
{
HttpResponseMessage message = null;
try
{
message = BasicCircuitBreakerPolicy.Execute(CreateResponseMessageFunc(uri));
}
catch (Polly.CircuitBreaker.BrokenCircuitException circuitException)
{
return circuitException.Message;
}

return message.Content.ReadAsStringAsync().Result;
}

Circuit breaker är en policy som håller tillstånd.

Sammanfattning

I exemplen ovan så har jag beskrivit hur vi kan använda Polly för att hantera att anropande system inte svarar på ett förväntat sätt, men det går även att använda för att hantera annan kod också.
Jag hoppas att ni har fått en insikt i hur Polly fungerar och hur det kan göra er kod bättre även om andras kod inte håller er standard. Det utan att behöva göra hemmasnickrade lösningar.

Även om det kan vara lockande att använda komplicerade lösningar så tror jag att det oftast är bättre att välja en enkel hantering av fel, både för användare och för andra utvecklare.

Policy-exemplen ovan är skrivna explicit, men det går lika bra att sätta upp dem mha dependecy injection och då blir hanteringen i stort sett osynling från koden som används för att göra anrop.

 

Elastitetsmätare