Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
SakuraIsayeki committed Jan 25, 2024
2 parents ed8873d + b92e292 commit 61e859d
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 19 deletions.
2 changes: 1 addition & 1 deletion WowsKarma.Api/Controllers/PlayerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async Task<IActionResult> GetAccount(uint id, bool includeClanInfo = true
return BadRequest(new ArgumentException(null, nameof(id)));
}

Player playerProfile = await _playerService.GetPlayerAsync(id, false, includeClanInfo);
Player? playerProfile = await _playerService.GetPlayerAsync(id, false, includeClanInfo);

return playerProfile is null
? NotFound()
Expand Down
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;
}
}
69 changes: 53 additions & 16 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 All @@ -44,7 +57,10 @@ public async Task<IEnumerable<Player>> GetPlayersAsync(IEnumerable<uint> ids, bo

foreach (uint id in ids.AsParallel().WithCancellation(ct))
{
players.Add(await GetPlayerAsync(id, includeRelated, includeClanInfo, ct));
if (await GetPlayerAsync(id, includeRelated, includeClanInfo, ct) is { } player)
{
players.Add(player);
}
}

return players;
Expand All @@ -56,7 +72,7 @@ public async Task<IEnumerable<Player>> GetPlayersAsync(IEnumerable<uint> ids, bo
* Method no longer returns any tracked entity, resulting in dropped changes for EF Core
* Do not use unless readonly.
*/
public async Task<Player> GetPlayerAsync(uint accountId, bool includeRelated = false, bool includeClanInfo = false, CancellationToken ct = default)
public async Task<Player?> GetPlayerAsync(uint accountId, bool includeRelated = false, bool includeClanInfo = false, CancellationToken ct = default)
{
if (accountId is 0)
{
Expand All @@ -78,13 +94,19 @@ public async Task<Player> GetPlayerAsync(uint accountId, bool includeRelated = f
}


Player player = await dbPlayers.FirstOrDefaultAsync(p => p.Id == accountId, ct);
Player? player = await dbPlayers.FirstOrDefaultAsync(p => p.Id == accountId, ct);
bool updated = false;
bool insert = player is null;

if (insert || UpdateNeeded(player))
{
player = await UpdatePlayerRecordAsync(player, accountId);

if (player is null)
{
return null;
}

updated = true;

if (insert)
Expand All @@ -95,8 +117,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 Expand Up @@ -195,14 +231,15 @@ internal async Task<Player> UpdatePlayerClanStatusAsync(Player player, Cancellat
return player;
}

internal async Task<Player> UpdatePlayerRecordAsync(Player player, uint? accountId = null)
internal async Task<Player?> UpdatePlayerRecordAsync(Player? player, uint? accountId = null)
{
Player apiPlayer = (await _vortex.FetchAccountAsync(player?.Id ?? accountId ?? 0)).ToDbModel() ?? throw new ApplicationException("Account returned null.");
if ((await _vortex.FetchAccountAsync(player?.Id ?? accountId ?? 0))?.ToDbModel() is { } apiPlayer)
{
player = player is null
? _context.Players.Add(apiPlayer).Entity
: Player.MapFromApi(player, apiPlayer);
}

player = player is null
? _context.Players.Add(apiPlayer).Entity
: Player.MapFromApi(player, apiPlayer);

return player;
}

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 61e859d

Please sign in to comment.