Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fusion cache #579

Merged
merged 9 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Infrastructure.Common.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;
namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry;

internal class NameRegistryClient : INameRegistry
{
private static readonly DistributedCacheEntryOptions _oneDayCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) };
private static readonly DistributedCacheEntryOptions _zeroCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.MinValue };

private readonly IDistributedCache _cache;
private readonly IFusionCache _cache;
private readonly HttpClient _client;
private readonly ILogger<NameRegistryClient> _logger;

Expand All @@ -25,25 +25,21 @@ internal class NameRegistryClient : INameRegistry
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
};

public NameRegistryClient(HttpClient client, IDistributedCache cache, ILogger<NameRegistryClient> logger)
public NameRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider, ILogger<NameRegistryClient> logger)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_cache = cacheProvider.GetCache(nameof(NameRegistry)) ?? throw new ArgumentNullException(nameof(cacheProvider));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<string?> GetName(string personalIdentificationNumber, CancellationToken cancellationToken)
{
return await _cache.GetOrAddAsync(
return await _cache.GetOrSetAsync(
$"Name_{personalIdentificationNumber}",
(ct) => GetNameFromRegister(personalIdentificationNumber, ct),
CacheOptionsFactory,
cancellationToken: cancellationToken);
token: cancellationToken);
}

private static DistributedCacheEntryOptions CacheOptionsFactory(string? name) =>
name is not null ? _oneDayCacheDuration : _zeroCacheDuration;
arealmaas marked this conversation as resolved.
Show resolved Hide resolved

private async Task<string?> GetNameFromRegister(string personalIdentificationNumber, CancellationToken cancellationToken)
{
const string apiUrl = "register/api/v1/parties/nameslookup";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Diagnostics;
using System.Net.Http.Json;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Infrastructure.Common.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;

Expand All @@ -12,28 +12,22 @@ internal class OrganizationRegistryClient : IOrganizationRegistry
private static readonly DistributedCacheEntryOptions _oneDayCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) };
private static readonly DistributedCacheEntryOptions _zeroCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.MinValue };

private readonly IDistributedCache _cache;
private readonly IFusionCache _cache;
private readonly HttpClient _client;

public OrganizationRegistryClient(HttpClient client, IDistributedCache cache)
public OrganizationRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_cache = cacheProvider.GetCache(nameof(OrganizationRegistry)) ?? throw new ArgumentNullException(nameof(cacheProvider));
}
public async Task<string?> GetOrgShortName(string orgNumber, CancellationToken cancellationToken)
{
var orgShortNameByOrgNumber = await _cache.GetOrAddAsync(
OrgShortNameReferenceCacheKey,
GetOrgShortNameByOrgNumber,
CacheOptionsFactory,
cancellationToken: cancellationToken);
var orgShortNameByOrgNumber = await _cache.GetOrSetAsync(OrgShortNameReferenceCacheKey, async token => await GetOrgShortNameByOrgNumber(token), token: cancellationToken);
orgShortNameByOrgNumber.TryGetValue(orgNumber, out var orgShortName);

return orgShortName;
}

private static DistributedCacheEntryOptions? CacheOptionsFactory(Dictionary<string, string>? orgShortNameByOrgNumber) =>
orgShortNameByOrgNumber is not null ? _oneDayCacheDuration : _zeroCacheDuration;

private async Task<Dictionary<string, string>> GetOrgShortNameByOrgNumber(CancellationToken cancellationToken)
{
const string searchEndpoint = "orgs/altinn-orgs.json";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.Net.Http.Json;
using Digdir.Domain.Dialogporten.Application.Externals;
using Digdir.Domain.Dialogporten.Domain.Common;
using Digdir.Domain.Dialogporten.Infrastructure.Common.Extensions;
using Microsoft.Extensions.Caching.Distributed;
using ZiggyCreatures.Caching.Fusion;

namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry;

Expand All @@ -13,29 +13,25 @@ internal sealed class ResourceRegistryClient : IResourceRegistry
private static readonly DistributedCacheEntryOptions _oneDayCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(1) };
private static readonly DistributedCacheEntryOptions _zeroCacheDuration = new() { AbsoluteExpiration = DateTimeOffset.MinValue };

private readonly IDistributedCache _cache;
private readonly IFusionCache _cache;
private readonly HttpClient _client;

public ResourceRegistryClient(HttpClient client, IDistributedCache cache)
public ResourceRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_cache = cacheProvider.GetCache(nameof(ResourceRegistry)) ?? throw new ArgumentNullException(nameof(cacheProvider));
}

public async Task<IReadOnlyCollection<string>> GetResourceIds(string org, CancellationToken cancellationToken)
{
var resourceIdsByOrg = await _cache.GetOrAddAsync(
var resourceIdsByOrg = await _cache.GetOrSetAsync(
OrgResourceReferenceCacheKey,
GetResourceIdsByOrg,
CacheOptionsFactory,
cancellationToken: cancellationToken);
async token => await GetResourceIdsByOrg(token),
token: cancellationToken);
resourceIdsByOrg.TryGetValue(org, out var resourceIds);
return resourceIds ?? Array.Empty<string>();
}

private static DistributedCacheEntryOptions? CacheOptionsFactory(Dictionary<string, string[]>? resourceIdsByOrg) =>
resourceIdsByOrg is not null ? _oneDayCacheDuration : _zeroCacheDuration;

private async Task<Dictionary<string, string[]>> GetResourceIdsByOrg(CancellationToken cancellationToken)
{
const string searchEndpoint = "resourceregistry/api/v1/resource/search";
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="ZiggyCreatures.FusionCache" Version="1.0.0" />
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="1.0.0" />
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference
Include="..\Digdir.Domain.Dialogporten.Application\Digdir.Domain.Dialogporten.Application.csproj" />
<ProjectReference
Include="..\Digdir.Library.Entity.EntityFrameworkCore\Digdir.Library.Entity.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\Digdir.Domain.Dialogporten.Application\Digdir.Domain.Dialogporten.Application.csproj" />
<ProjectReference Include="..\Digdir.Library.Entity.EntityFrameworkCore\Digdir.Library.Entity.EntityFrameworkCore.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.Events;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry;
using ZiggyCreatures.Caching.Fusion;
using System.Text.Json;
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Caching.Distributed;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Memory;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry;

namespace Digdir.Domain.Dialogporten.Infrastructure;

Expand Down Expand Up @@ -56,24 +65,26 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
.AddValidatorsFromAssembly(thisAssembly, ServiceLifetime.Transient, includeInternalTypes: true);

var infrastructureSettings = infrastructureConfigurationSection.Get<InfrastructureSettings>()
?? throw new InvalidOperationException("Failed to get Redis settings. Infrastructure settings must not be null.");
?? throw new InvalidOperationException("Failed to get Redis settings. Infrastructure settings must not be null.");

services.AddFusionCacheNewtonsoftJsonSerializer();
arealmaas marked this conversation as resolved.
Show resolved Hide resolved

if (infrastructureSettings.Redis.Enabled == true)
{
services.AddStackExchangeRedisCache(options =>
{
var infrastructureSettings = infrastructureConfigurationSection.Get<InfrastructureSettings>()
?? throw new InvalidOperationException("Failed to get Redis connection string. Infrastructure settings must not be null.");
var connectionString = infrastructureSettings.Redis.ConnectionString;
options.Configuration = connectionString;
options.InstanceName = "Redis";
});
services.AddStackExchangeRedisCache(opt => opt.Configuration = infrastructureSettings.Redis.ConnectionString);
services.AddFusionCacheStackExchangeRedisBackplane(opt => opt.Configuration = infrastructureSettings.Redis.ConnectionString);
}
else
{
services.AddDistributedMemoryCache();
}

// todo: do we need default cache? 🤔
ConfigureFusionCache(services);
arealmaas marked this conversation as resolved.
Show resolved Hide resolved
ConfigureFusionCache(services, nameof(NameRegistryClient));
ConfigureFusionCache(services, nameof(ResourceRegistryClient));
ConfigureFusionCache(services, nameof(OrganizationRegistryClient));

services.AddDbContext<DialogDbContext>((services, options) =>
{
var connectionString = services.GetRequiredService<IOptions<InfrastructureSettings>>()
Expand Down Expand Up @@ -163,4 +174,43 @@ private static IHttpClientBuilder AddMaskinportenHttpClient<TClient, TImplementa
.AddHttpClient<TClient, TImplementation>()
.AddMaskinportenHttpMessageHandler<TClientDefinition, TClient>(configureClientDefinition);
}

private static void ConfigureFusionCache(IServiceCollection services, string? cacheName = null)
{
// todo: consider open telemetry?
var fusionCacheConfig = services.AddFusionCache(cacheName ?? string.Empty)
.WithOptions(options =>
{
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2);
// todo: Consider log levels based on environment. More debug for dev, less for prod.
arealmaas marked this conversation as resolved.
Show resolved Hide resolved
options.FailSafeActivationLogLevel = LogLevel.Debug;
options.SerializationErrorsLogLevel = LogLevel.Warning;
options.DistributedCacheSyntheticTimeoutsLogLevel = LogLevel.Debug;
options.DistributedCacheErrorsLogLevel = LogLevel.Error;
options.FactorySyntheticTimeoutsLogLevel = LogLevel.Debug;
options.FactoryErrorsLogLevel = LogLevel.Error;
})
.WithDefaultEntryOptions(new FusionCacheEntryOptions
elsand marked this conversation as resolved.
Show resolved Hide resolved
{
// todo: Let's discuss these settings..
Duration = TimeSpan.FromMinutes(2),

IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),

FactorySoftTimeout = TimeSpan.FromMilliseconds(100),
FactoryHardTimeout = TimeSpan.FromMilliseconds(1500),

DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1),
DistributedCacheHardTimeout = TimeSpan.FromSeconds(2),

AllowBackgroundDistributedCacheOperations = true,

JitterMaxDuration = TimeSpan.FromSeconds(2)
})
.WithRegisteredSerializer()
.WithRegisteredDistributedCache()
.WithRegisteredBackplane();
}
}