Skip to content

Commit

Permalink
feat: add fusion cache (#579)
Browse files Browse the repository at this point in the history
related to #274

- Legger til
[FusionCache](https://github.com/ZiggyCreatures/FusionCache) som
erstatning for IDistributedCache med sunne verdier som default settings.
Alt av settings kan overstyres i service-kallet.
- For å lage en ny cache bruker vi named caches. Navn på cache på
eksplisitt oppgis.
- Bruker Redis som backplane

TODO i neste PR: Bind til IConfiguration for å gjøre det konfigurerbart
via app configuration

---------

Co-authored-by: Ole Jørgen Skogstad <skogstad@softis.net>
  • Loading branch information
arealmaas and oskogstad authored Apr 3, 2024
1 parent 46eeedb commit 973fa5c
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 140 deletions.
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;

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.NeueccMessagePack" 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,14 @@
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.Events;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.ResourceRegistry;
using System.Text.Json;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Caching.Memory;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry;

namespace Digdir.Domain.Dialogporten.Infrastructure;

Expand Down Expand Up @@ -56,24 +64,33 @@ 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.AddFusionCacheNeueccMessagePackSerializer();

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();
}

services.ConfigureFusionCache(nameof(Altinn.NameRegistry), new()
{
Duration = TimeSpan.FromDays(1),
})
.ConfigureFusionCache(nameof(Altinn.ResourceRegistry), new()
{
Duration = TimeSpan.FromMinutes(20),
})
.ConfigureFusionCache(nameof(Altinn.OrganizationRegistry), new()
{
Duration = TimeSpan.FromDays(1),
});

services.AddDbContext<DialogDbContext>((services, options) =>
{
var connectionString = services.GetRequiredService<IOptions<InfrastructureSettings>>()
Expand Down Expand Up @@ -149,6 +166,21 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
return services;
}

public class FusionCacheSettings
{
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(1);
public TimeSpan FailSafeMaxDuration { get; set; } = TimeSpan.FromHours(2);
public TimeSpan FailSafeThrottleDuration { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan FactorySoftTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
public TimeSpan FactoryHardTimeout { get; set; } = TimeSpan.FromMilliseconds(1500);
public TimeSpan DistributedCacheSoftTimeout { get; set; } = TimeSpan.FromSeconds(1);
public TimeSpan DistributedCacheHardTimeout { get; set; } = TimeSpan.FromSeconds(2);
public bool AllowBackgroundDistributedCacheOperations { get; set; } = true;
public bool IsFailSafeEnabled { get; set; } = true;
public TimeSpan JitterMaxDuration { get; set; } = TimeSpan.FromSeconds(2);
public float EagerRefreshThreshold { get; set; } = 0.8f;
}

private static IHttpClientBuilder AddMaskinportenHttpClient<TClient, TImplementation, TClientDefinition>(
this IServiceCollection services,
IConfiguration configuration,
Expand All @@ -163,4 +195,39 @@ private static IHttpClientBuilder AddMaskinportenHttpClient<TClient, TImplementa
.AddHttpClient<TClient, TImplementation>()
.AddMaskinportenHttpMessageHandler<TClientDefinition, TClient>(configureClientDefinition);
}

private static IServiceCollection ConfigureFusionCache(this IServiceCollection services, string cacheName, FusionCacheSettings? settings = null)
{
settings ??= new FusionCacheSettings();

services.AddFusionCache(cacheName)
.WithOptions(options =>
{
options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2);
})
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = settings.Duration,

IsFailSafeEnabled = settings.IsFailSafeEnabled,
FailSafeMaxDuration = settings.FailSafeMaxDuration,
FailSafeThrottleDuration = settings.FailSafeThrottleDuration,

FactorySoftTimeout = settings.FactorySoftTimeout,
FactoryHardTimeout = settings.FactoryHardTimeout,

DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout,
DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout,

AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations,

JitterMaxDuration = settings.JitterMaxDuration,
EagerRefreshThreshold = settings.EagerRefreshThreshold
})
.WithRegisteredSerializer()
.WithRegisteredDistributedCache()
.WithRegisteredBackplane();

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ internal sealed class RedisSettingsValidator : AbstractValidator<RedisSettings>
public RedisSettingsValidator()
{
RuleFor(x => x.Enabled).Must(x => x is false or true);
RuleFor(x => x.ConnectionString).NotEmpty();

When(x => x.Enabled == true, () =>
{
RuleFor(x => x.ConnectionString).NotEmpty();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"Password": "guest"
},
"Infrastructure": {
"Redis": {
"Enabled": false
},
"DialogDbConnectionString": "TODO: Add to local secrets",
// Settings from appsettings.json, environment variables or other configuration providers.
// The first three are always mandatory for all client definitions types
Expand Down

0 comments on commit 973fa5c

Please sign in to comment.