From c8a6cd34aaa686230bebe5cadb5bb6fcdbc78c79 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Tue, 2 Apr 2024 18:05:40 +0200 Subject: [PATCH 1/8] feat: add fusion cache --- .../NameRegistryClient.cs | 18 ++-- .../OrganizationRegistryClient.cs | 18 ++-- .../ResourceRegistryClient.cs | 15 ++- .../Extensions/DistributedCacheExtensions.cs | 92 ------------------- ....Domain.Dialogporten.Infrastructure.csproj | 9 +- .../InfrastructureExtensions.cs | 68 ++++++++++++-- 6 files changed, 84 insertions(+), 136 deletions(-) delete mode 100644 src/Digdir.Domain.Dialogporten.Infrastructure/Common/Extensions/DistributedCacheExtensions.cs diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs index f1669d418..e8e385470 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs @@ -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 _logger; @@ -25,25 +25,21 @@ internal class NameRegistryClient : INameRegistry DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault }; - public NameRegistryClient(HttpClient client, IDistributedCache cache, ILogger logger) + public NameRegistryClient(HttpClient client, IFusionCacheProvider cacheProvider, ILogger 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 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 GetNameFromRegister(string personalIdentificationNumber, CancellationToken cancellationToken) { const string apiUrl = "register/api/v1/parties/nameslookup"; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/OrganizationRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/OrganizationRegistryClient.cs index 8859672aa..8c5c4e349 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/OrganizationRegistryClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/OrganizationRegistryClient.cs @@ -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; @@ -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 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? orgShortNameByOrgNumber) => - orgShortNameByOrgNumber is not null ? _oneDayCacheDuration : _zeroCacheDuration; - private async Task> GetOrgShortNameByOrgNumber(CancellationToken cancellationToken) { const string searchEndpoint = "orgs/altinn-orgs.json"; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs index c40a0cc43..8c00c7afb 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs @@ -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; @@ -13,22 +13,21 @@ 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> 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(); } diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Extensions/DistributedCacheExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Extensions/DistributedCacheExtensions.cs deleted file mode 100644 index de38454a7..000000000 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Common/Extensions/DistributedCacheExtensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; - -namespace Digdir.Domain.Dialogporten.Infrastructure.Common.Extensions; - -internal static class DistributedCacheExtensions -{ - private static readonly ConcurrentDictionary _locks = new(); - - /// - /// Gets the value associated with the specified key from the distributed cache, or adds it if not found. - /// - /// The type of value to retrieve or add. - /// The distributed cache instance. - /// The cache key to look up or add. - /// - /// A function that returns the value to be added to the cache if not found. - /// This function takes a CancellationToken as a parameter for asynchronous operations. - /// - /// - /// An optional function that provides cache entry options based on the generated value. - /// If not provided or the function returns null, default options will be used. - /// - /// - /// Optional. The JSON serialization options to use when serializing and deserializing the cache value. - /// - /// A CancellationToken that can be used to cancel the operation. - /// - /// The value associated with the specified key, either retrieved from the cache or added using the provided factory function. - /// If the value cannot be found in the cache or if the specified options indicate that the cached value has expired, - /// the provided factory function's result will be returned. - /// - /// - /// This method first attempts to retrieve the value associated with the given key from the distributed cache. - /// If the value is found, it is deserialized and returned. If not found or if the cache entry options indicate that the - /// cached value has expired, the provided valueFactory function is called to generate the value. The optionsFactory - /// function can be used to specify custom cache entry options based on the generated value, such as setting an absolute - /// expiration time or using sliding expiration. - /// - internal static async Task GetOrAddAsync( - this IDistributedCache cache, - string key, - Func> valueFactory, - Func? optionsFactory = null, - JsonSerializerOptions? jsonOptions = null, - CancellationToken cancellationToken = default) - { - var value = await TryGetFromCache(cache, key, jsonOptions, cancellationToken); - if (value is not null) - { - return value; - } - - var keyLock = _locks.GetOrAdd(key, new SemaphoreSlim(1, 1)); - await keyLock.WaitAsync(cancellationToken); - - try - { - // Try getting the value from cache again (it might have been added to cache by another task while waiting for lock). - value = await TryGetFromCache(cache, key, jsonOptions, cancellationToken); - if (value is not null) - { - return value; - } - - value = await valueFactory(cancellationToken); - var options = optionsFactory?.Invoke(value) ?? new(); - if (options.AbsoluteExpiration < DateTimeOffset.UtcNow) - { - return value; - } - - await cache.SetAsync(key, JsonSerializer.SerializeToUtf8Bytes(value), options, cancellationToken); - } - finally - { - keyLock.Release(); - } - return value; - } - - private static async Task TryGetFromCache(IDistributedCache cache, string key, JsonSerializerOptions? jsonOptions, CancellationToken cancellationToken) - { - var cachedValue = await cache.GetAsync(key, cancellationToken); - if (cachedValue is not null) - { - return JsonSerializer.Deserialize(cachedValue, jsonOptions); - } - return default; - } -} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj index 9215ce58b..cf8e3c3fb 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj @@ -22,13 +22,14 @@ + + + - - + + diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 26782fcff..b32e65c22 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -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; @@ -56,24 +65,26 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi .AddValidatorsFromAssembly(thisAssembly, ServiceLifetime.Transient, includeInternalTypes: true); var infrastructureSettings = infrastructureConfigurationSection.Get() - ?? 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(); if (infrastructureSettings.Redis.Enabled == true) { - services.AddStackExchangeRedisCache(options => - { - var infrastructureSettings = infrastructureConfigurationSection.Get() - ?? 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); + ConfigureFusionCache(services, nameof(NameRegistryClient)); + ConfigureFusionCache(services, nameof(ResourceRegistryClient)); + ConfigureFusionCache(services, nameof(OrganizationRegistryClient)); + services.AddDbContext((services, options) => { var connectionString = services.GetRequiredService>() @@ -163,4 +174,43 @@ private static IHttpClientBuilder AddMaskinportenHttpClient() .AddMaskinportenHttpMessageHandler(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. + 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 + { + // 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(); + } } From fbffd06d61ebc9fe1649201a0f90064fdea8d531 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Tue, 2 Apr 2024 18:11:45 +0200 Subject: [PATCH 2/8] cleanup --- .../Altinn/ResourceRegistry/ResourceRegistryClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs index 8c00c7afb..0e9c81d5c 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/ResourceRegistry/ResourceRegistryClient.cs @@ -32,9 +32,6 @@ public async Task> GetResourceIds(string org, Cancel return resourceIds ?? Array.Empty(); } - private static DistributedCacheEntryOptions? CacheOptionsFactory(Dictionary? resourceIdsByOrg) => - resourceIdsByOrg is not null ? _oneDayCacheDuration : _zeroCacheDuration; - private async Task> GetResourceIdsByOrg(CancellationToken cancellationToken) { const string searchEndpoint = "resourceregistry/api/v1/resource/search"; From 65c61d7a13c8dd43e7dfd4ac98b0f5cd5002f0e8 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Tue, 2 Apr 2024 18:14:31 +0200 Subject: [PATCH 3/8] cleanup --- .../InfrastructureExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index b32e65c22..10bab26df 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -193,7 +193,7 @@ private static void ConfigureFusionCache(IServiceCollection services, string? ca .WithDefaultEntryOptions(new FusionCacheEntryOptions { // todo: Let's discuss these settings.. - Duration = TimeSpan.FromMinutes(2), + Duration = TimeSpan.FromDays(1), IsFailSafeEnabled = true, FailSafeMaxDuration = TimeSpan.FromHours(2), From 9a25d9cb02d63d85c8062e960ddd6819c4e9fec2 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Wed, 3 Apr 2024 09:37:39 +0200 Subject: [PATCH 4/8] cleanup --- .../InfrastructureExtensions.cs | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 10bab26df..1ae700539 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -81,9 +81,18 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi // todo: do we need default cache? 🤔 ConfigureFusionCache(services); - ConfigureFusionCache(services, nameof(NameRegistryClient)); - ConfigureFusionCache(services, nameof(ResourceRegistryClient)); - ConfigureFusionCache(services, nameof(OrganizationRegistryClient)); + ConfigureFusionCache(services, nameof(NameRegistryClient), new() + { + Duration = TimeSpan.FromDays(1), + }); + ConfigureFusionCache(services, nameof(ResourceRegistryClient), new() + { + Duration = TimeSpan.FromDays(1), + }); + ConfigureFusionCache(services, nameof(OrganizationRegistryClient), new() + { + Duration = TimeSpan.FromDays(1), + }); services.AddDbContext((services, options) => { @@ -160,6 +169,20 @@ 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); + } + private static IHttpClientBuilder AddMaskinportenHttpClient( this IServiceCollection services, IConfiguration configuration, @@ -175,8 +198,10 @@ private static IHttpClientBuilder AddMaskinportenHttpClient(configureClientDefinition); } - private static void ConfigureFusionCache(IServiceCollection services, string? cacheName = null) + private static void ConfigureFusionCache(IServiceCollection services, string? cacheName = null, FusionCacheSettings? settings = null) { + settings ??= new FusionCacheSettings(); + // todo: consider open telemetry? var fusionCacheConfig = services.AddFusionCache(cacheName ?? string.Empty) .WithOptions(options => @@ -193,21 +218,21 @@ private static void ConfigureFusionCache(IServiceCollection services, string? ca .WithDefaultEntryOptions(new FusionCacheEntryOptions { // todo: Let's discuss these settings.. - Duration = TimeSpan.FromDays(1), + Duration = settings.Duration, - IsFailSafeEnabled = true, - FailSafeMaxDuration = TimeSpan.FromHours(2), - FailSafeThrottleDuration = TimeSpan.FromSeconds(30), + IsFailSafeEnabled = settings.IsFailSafeEnabled, + FailSafeMaxDuration = settings.FailSafeMaxDuration, + FailSafeThrottleDuration = settings.FailSafeThrottleDuration, - FactorySoftTimeout = TimeSpan.FromMilliseconds(100), - FactoryHardTimeout = TimeSpan.FromMilliseconds(1500), + FactorySoftTimeout = settings.FactorySoftTimeout, + FactoryHardTimeout = settings.FactoryHardTimeout, - DistributedCacheSoftTimeout = TimeSpan.FromSeconds(1), - DistributedCacheHardTimeout = TimeSpan.FromSeconds(2), + DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout, + DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout, - AllowBackgroundDistributedCacheOperations = true, + AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations, - JitterMaxDuration = TimeSpan.FromSeconds(2) + JitterMaxDuration = settings.JitterMaxDuration }) .WithRegisteredSerializer() .WithRegisteredDistributedCache() From b67cb3e699a460e41fffd2a7e278d8cae6469b12 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Wed, 3 Apr 2024 11:37:36 +0200 Subject: [PATCH 5/8] fix: le settings --- .../InfrastructureExtensions.cs | 6 +++--- .../InfrastructureSettings.cs | 6 +++++- .../appsettings.Development.json | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 1ae700539..6b1a980f7 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -81,15 +81,15 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi // todo: do we need default cache? 🤔 ConfigureFusionCache(services); - ConfigureFusionCache(services, nameof(NameRegistryClient), new() + ConfigureFusionCache(services, nameof(Altinn.NameRegistry), new() { Duration = TimeSpan.FromDays(1), }); - ConfigureFusionCache(services, nameof(ResourceRegistryClient), new() + ConfigureFusionCache(services, nameof(Altinn.ResourceRegistry), new() { Duration = TimeSpan.FromDays(1), }); - ConfigureFusionCache(services, nameof(OrganizationRegistryClient), new() + ConfigureFusionCache(services, nameof(Altinn.OrganizationRegistry), new() { Duration = TimeSpan.FromDays(1), }); diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs index 65be366ae..9d43f2967 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureSettings.cs @@ -94,6 +94,10 @@ internal sealed class RedisSettingsValidator : AbstractValidator 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(); + }); } } \ No newline at end of file diff --git a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json index 35b72e314..00042fc37 100644 --- a/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json +++ b/src/Digdir.Domain.Dialogporten.Service/appsettings.Development.json @@ -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 From b19505ae4ced9383c930f4924b1f5487023573f3 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Wed, 3 Apr 2024 13:40:08 +0200 Subject: [PATCH 6/8] cleanup --- ....Domain.Dialogporten.Infrastructure.csproj | 2 +- .../InfrastructureExtensions.cs | 26 +++++++------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj index cf8e3c3fb..1036b5a8e 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 6b1a980f7..8281e9b51 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -25,14 +25,13 @@ 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 ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; using Digdir.Domain.Dialogporten.Infrastructure.Altinn.NameRegistry; namespace Digdir.Domain.Dialogporten.Infrastructure; @@ -67,7 +66,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi var infrastructureSettings = infrastructureConfigurationSection.Get() ?? throw new InvalidOperationException("Failed to get Redis settings. Infrastructure settings must not be null."); - services.AddFusionCacheNewtonsoftJsonSerializer(); + services.AddFusionCacheNeueccMessagePackSerializer(); if (infrastructureSettings.Redis.Enabled == true) { @@ -79,15 +78,13 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddDistributedMemoryCache(); } - // todo: do we need default cache? 🤔 - ConfigureFusionCache(services); ConfigureFusionCache(services, nameof(Altinn.NameRegistry), new() { Duration = TimeSpan.FromDays(1), }); ConfigureFusionCache(services, nameof(Altinn.ResourceRegistry), new() { - Duration = TimeSpan.FromDays(1), + Duration = TimeSpan.FromMinutes(20), }); ConfigureFusionCache(services, nameof(Altinn.OrganizationRegistry), new() { @@ -181,6 +178,7 @@ public class FusionCacheSettings 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( @@ -198,22 +196,15 @@ private static IHttpClientBuilder AddMaskinportenHttpClient(configureClientDefinition); } - private static void ConfigureFusionCache(IServiceCollection services, string? cacheName = null, FusionCacheSettings? settings = null) + private static void ConfigureFusionCache(IServiceCollection services, string cacheName, FusionCacheSettings? settings = null) { settings ??= new FusionCacheSettings(); // todo: consider open telemetry? - var fusionCacheConfig = services.AddFusionCache(cacheName ?? string.Empty) + var fusionCacheConfig = services.AddFusionCache(cacheName) .WithOptions(options => { options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2); - // todo: Consider log levels based on environment. More debug for dev, less for prod. - 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 { @@ -232,7 +223,8 @@ private static void ConfigureFusionCache(IServiceCollection services, string? ca AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations, - JitterMaxDuration = settings.JitterMaxDuration + JitterMaxDuration = settings.JitterMaxDuration, + EagerRefreshThreshold = settings.EagerRefreshThreshold }) .WithRegisteredSerializer() .WithRegisteredDistributedCache() From fa610d529ec19f7b772e49cb0c222e42e63a3b27 Mon Sep 17 00:00:00 2001 From: Are Almaas Date: Wed, 3 Apr 2024 13:48:12 +0200 Subject: [PATCH 7/8] cleanup --- .../InfrastructureExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 8281e9b51..0755c074d 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -200,7 +200,6 @@ private static void ConfigureFusionCache(IServiceCollection services, string cac { settings ??= new FusionCacheSettings(); - // todo: consider open telemetry? var fusionCacheConfig = services.AddFusionCache(cacheName) .WithOptions(options => { @@ -208,7 +207,6 @@ private static void ConfigureFusionCache(IServiceCollection services, string cac }) .WithDefaultEntryOptions(new FusionCacheEntryOptions { - // todo: Let's discuss these settings.. Duration = settings.Duration, IsFailSafeEnabled = settings.IsFailSafeEnabled, From 3080902247d259f7239309fcf90e89b308993971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ole=20J=C3=B8rgen=20Skogstad?= Date: Wed, 3 Apr 2024 14:21:56 +0200 Subject: [PATCH 8/8] chore: minor refactor (#583) --- .../InfrastructureExtensions.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index 0755c074d..32f1a7791 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -78,15 +78,15 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddDistributedMemoryCache(); } - ConfigureFusionCache(services, nameof(Altinn.NameRegistry), new() + services.ConfigureFusionCache(nameof(Altinn.NameRegistry), new() { Duration = TimeSpan.FromDays(1), - }); - ConfigureFusionCache(services, nameof(Altinn.ResourceRegistry), new() + }) + .ConfigureFusionCache(nameof(Altinn.ResourceRegistry), new() { Duration = TimeSpan.FromMinutes(20), - }); - ConfigureFusionCache(services, nameof(Altinn.OrganizationRegistry), new() + }) + .ConfigureFusionCache(nameof(Altinn.OrganizationRegistry), new() { Duration = TimeSpan.FromDays(1), }); @@ -196,11 +196,11 @@ private static IHttpClientBuilder AddMaskinportenHttpClient(configureClientDefinition); } - private static void ConfigureFusionCache(IServiceCollection services, string cacheName, FusionCacheSettings? settings = null) + private static IServiceCollection ConfigureFusionCache(this IServiceCollection services, string cacheName, FusionCacheSettings? settings = null) { settings ??= new FusionCacheSettings(); - var fusionCacheConfig = services.AddFusionCache(cacheName) + services.AddFusionCache(cacheName) .WithOptions(options => { options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(2); @@ -227,5 +227,7 @@ private static void ConfigureFusionCache(IServiceCollection services, string cac .WithRegisteredSerializer() .WithRegisteredDistributedCache() .WithRegisteredBackplane(); + + return services; } }