Skip to content

Commit

Permalink
feat(api): Add resilience policies using Polly
Browse files Browse the repository at this point in the history
This commit adds resilience policies to the API.

In `PollyContextExtensions.cs`, a static class `PollyContextExtensions` is added, which defines item names for context items. It also includes a method `TryGetLogger` that retrieves the logger from the resilience context.

In `ResiliencePipelineDependencyInjectionExtensions.cs`, two static classes are added: `ResiliencePipelines` and `ResiliencePipelineDependencyInjectionExtensions`. The former defines keys/names of different resilience pipelines, while the latter provides extension methods for adding resilience policies to the dependency injection container using Polly. This file includes an example of adding a specific resilience pipeline called "player-clans-update" with fallback, circuit breaker, and retry policies.

Additionally, in the existing file `PlayerService.cs`, changes are made to use the newly added resilience pipeline. The constructor now takes an instance of `ILogger<PlayerService>` and a `ResiliencePipelineProvider<string>`. The logger is used to set the logger property in the resilience context, and then this context is passed to `_resiliencePipeline.ExecuteAsync()` method when updating player clan status.

Lastly, in the existing file `Startup.cs`, a call to add resilience policies using the extension method `AddResiliencePolicies()` is added within the ConfigureServices method.

These changes enhance the API's ability to handle failures and improve its overall resiliency.
  • Loading branch information
SakuraIsayeki committed Jan 25, 2024
1 parent ed8873d commit 32c4aac
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 9 deletions.
25 changes: 25 additions & 0 deletions WowsKarma.Api/Infrastructure/Resilience/PollyContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Diagnostics.CodeAnalysis;
using Polly;

namespace WowsKarma.Api.Infrastructure.Resilience;

public static class PollyContextExtensions
{
/// <summary>
/// Defines the item names for context items.
/// </summary>
/// <seealso cref="ResilienceContext"/>
public static class ContextItems
{
public static readonly ResiliencePropertyKey<ILogger> Logger = new("logger");
}

/// <summary>
/// Gets the logger from the resilience context.
/// </summary>
/// <param name="context">The resilience context.</param>
/// <param name="logger">The logger, if initially provided to the context and found.</param>
/// <returns></returns>
public static bool TryGetLogger(this ResilienceContext context, [NotNullWhen(true)] out ILogger? logger)
=> context.Properties.TryGetValue(ContextItems.Logger, out logger);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Text.Json;
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using static WowsKarma.Api.Infrastructure.Resilience.PollyContextExtensions;

namespace WowsKarma.Api.Infrastructure.Resilience;

/// <summary>
/// Keys/Names of the different resilience pipelines.
/// </summary>
public static class ResiliencePipelines
{
public const string PlayerClansUpdatePolicyName = "player-clans-update";
}

/// <summary>
/// Extension methods for adding resilience policies to the dependency injection container, using Polly.
/// </summary>
public static class ResiliencePipelineDependencyInjectionExtensions
{
/// <summary>
/// Adds resilience policies to the dependency injection container, using Polly.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the policies to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddResiliencePolicies(this IServiceCollection services)
{
services.AddResiliencePipeline<string, bool>(ResiliencePipelines.PlayerClansUpdatePolicyName, builder => builder
.AddFallback(new()
{
ShouldHandle = static args => ValueTask.FromResult(args.Outcome.Exception is not null),
FallbackAction = static args =>
{
if (args.Outcome.Exception is not BrokenCircuitException or IsolatedCircuitException)
{
(args.Context.TryGetLogger(out ILogger? logger) ? logger : null)?
.LogWarning(args.Outcome.Exception, "Fallback triggered for {Policy} policy", ResiliencePipelines.PlayerClansUpdatePolicyName);
}
return Outcome.FromResultAsValueTask(false);
}
})
.AddCircuitBreaker(new()
{
ShouldHandle = static args => ValueTask.FromResult(args.Outcome switch
{
{ Exception: JsonException } => true,
_ => false
}),
OnClosed = static args =>
{
(args.Context.TryGetLogger(out ILogger? logger) ? logger : null)?
.LogInformation("Circuit closed for {Policy} policy", ResiliencePipelines.PlayerClansUpdatePolicyName);
return ValueTask.CompletedTask;
},
OnHalfOpened = static args =>
{
(args.Context.TryGetLogger(out ILogger? logger) ? logger : null)?
.LogInformation("Circuit half-opened for {Policy} policy", ResiliencePipelines.PlayerClansUpdatePolicyName);
return ValueTask.CompletedTask;
},
OnOpened = static args =>
{
(args.Context.TryGetLogger(out ILogger? logger) ? logger : null)?
.LogWarning("Circuit opened for {Policy} policy", ResiliencePipelines.PlayerClansUpdatePolicyName);
return ValueTask.CompletedTask;
}
})
.AddRetry(new()
{
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 4,
ShouldHandle = static args => ValueTask.FromResult(args.Outcome switch
{
{ Exception: HttpRequestException } => true,
_ => false
})
})
);

return services;
}
}
41 changes: 34 additions & 7 deletions WowsKarma.Api/Services/PlayerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
using Hangfire.Tags.Attributes;
using Nodsoft.Wargaming.Api.Client.Clients.Wows;
using Nodsoft.Wargaming.Api.Common.Data.Responses.Wows.Vortex;
using Polly;
using Polly.Registry;
using WowsKarma.Api.Data;
using WowsKarma.Api.Infrastructure.Exceptions;
using WowsKarma.Api.Infrastructure.Resilience;
using WowsKarma.Api.Utilities;

namespace WowsKarma.Api.Services;
Expand All @@ -18,14 +21,24 @@ public class PlayerService
private readonly WowsPublicApiClient _wgApi;
private readonly WowsVortexApiClient _vortex;
private readonly ClanService _clanService;


public PlayerService(ApiDbContext context, WowsPublicApiClient wgApi, WowsVortexApiClient vortex, ClanService clanService)
{
private readonly ILogger<PlayerService> _logger;
private readonly ResiliencePipeline<bool> _resiliencePipeline;


public PlayerService(
ApiDbContext context,
WowsPublicApiClient wgApi,
WowsVortexApiClient vortex,
ClanService clanService,
ILogger<PlayerService> logger,
ResiliencePipelineProvider<string> resiliencePipelineRegistry
) {
_context = context;
_wgApi = wgApi;
_vortex = vortex;
_clanService = clanService;
_clanService = clanService;
_logger = logger;
_resiliencePipeline = resiliencePipelineRegistry.GetPipeline<bool>(ResiliencePipelines.PlayerClansUpdatePolicyName);
}

/// <summary>
Expand Down Expand Up @@ -95,8 +108,22 @@ public async Task<Player> GetPlayerAsync(uint accountId, bool includeRelated = f

if (includeClanInfo)
{
player = await UpdatePlayerClanStatusAsync(player, ct);
updated = true;
ResilienceContext resilienceCtx = ResilienceContextPool.Shared.Get(ct);
resilienceCtx.Properties.Set(PollyContextExtensions.ContextItems.Logger, _logger);

try
{
// ReSharper disable once HeapView.CanAvoidClosure
updated |= await _resiliencePipeline.ExecuteAsync(async ctx =>
{
player = await UpdatePlayerClanStatusAsync(player, ctx.CancellationToken);
return true;
}, resilienceCtx);
}
finally
{
ResilienceContextPool.Shared.Return(resilienceCtx);
}
}

if (updated)
Expand Down
6 changes: 4 additions & 2 deletions WowsKarma.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using WowsKarma.Api.Data;
using WowsKarma.Api.Hubs;
using WowsKarma.Api.Infrastructure.Authorization;
using WowsKarma.Api.Infrastructure.Resilience;
using WowsKarma.Api.Infrastructure.Telemetry;
using WowsKarma.Api.Middlewares;
using WowsKarma.Api.Minimap.Client;
Expand Down Expand Up @@ -269,9 +270,10 @@ public void ConfigureServices(IServiceCollection services)

services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]);
});

services.AddResiliencePolicies();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down
1 change: 1 addition & 0 deletions WowsKarma.Api/WowsKarma.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<PackageReference Include="Nodsoft.Wargaming.Api.Client" Version="0.3.5" />
<PackageReference Include="Nodsoft.WowsReplaysUnpack.ExtendedData" Version="2.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Polly.Extensions" Version="8.2.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
Expand Down

0 comments on commit 32c4aac

Please sign in to comment.