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 all 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.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
elsand marked this conversation as resolved.
Show resolved Hide resolved
{
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
elsand marked this conversation as resolved.
Show resolved Hide resolved
{
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