Skip to content

Commit 8a2f2ef

Browse files
authored
LEGLINK-960: Add a one-week expiration policy to all Redis resource cache entries (#1846)
Add config option for RedisResourceCache TTL
1 parent 3ceee7e commit 8a2f2ef

9 files changed

Lines changed: 124 additions & 3 deletions

File tree

Config/app-config.dev.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,15 @@
16321632
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
16331633
}
16341634
},
1635+
{
1636+
"key": "ResourceCache:Redis:CacheEntryTtlDays",
1637+
"value": "7",
1638+
"label": null,
1639+
"content_type": "",
1640+
"tags": {
1641+
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
1642+
}
1643+
},
16351644
{
16361645
"key": "ResourceCache:Redis:Password",
16371646
"value": "{\"uri\":\"https://link-vault.vault.azure.net/secrets/redis-password\"}",

Config/app-config.qa.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,6 +1615,15 @@
16151615
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
16161616
}
16171617
},
1618+
{
1619+
"key": "ResourceCache:Redis:CacheEntryTtlDays",
1620+
"value": "7",
1621+
"label": null,
1622+
"content_type": "",
1623+
"tags": {
1624+
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
1625+
}
1626+
},
16181627
{
16191628
"key": "ResourceCache:Redis:Password",
16201629
"value": "{\"uri\":\"https://nhsnlink-kv-qa.vault.azure.net/secrets/link-resource-cache-redis-password\"}",

Config/app-config.qa2.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,6 +1615,15 @@
16151615
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
16161616
}
16171617
},
1618+
{
1619+
"key": "ResourceCache:Redis:CacheEntryTtlDays",
1620+
"value": "7",
1621+
"label": null,
1622+
"content_type": "",
1623+
"tags": {
1624+
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
1625+
}
1626+
},
16181627
{
16191628
"key": "ResourceCache:Redis:Password",
16201629
"value": "{\"uri\":\"https://nhsnlink-kv-qa2.vault.azure.net/secrets/link-resource-cache-redis-password\"}",

Config/app-config.test.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,6 +1664,15 @@
16641664
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
16651665
}
16661666
},
1667+
{
1668+
"key": "ResourceCache:Redis:CacheEntryTtlDays",
1669+
"value": "7",
1670+
"label": null,
1671+
"content_type": "",
1672+
"tags": {
1673+
"link:consumers": "DataAcquisition,DataAcquisitionWorker,Normalization"
1674+
}
1675+
},
16671676
{
16681677
"key": "ResourceCache:Redis:Password",
16691678
"value": "{\"uri\":\"https://nhsnlink-kv-test.vault.azure.net/secrets/link-resource-cache-redis-password\"}",

DotNet/ServiceTests/UnitTests/Shared/ResourceCache/RedisResourceCacheTests.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using LantanaGroup.Link.Shared.Application.Services.ResourceCache;
2+
using LantanaGroup.Link.Shared.Application.Models.Configs;
23
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
35
using Moq;
46
using StackExchange.Redis;
57
using StackExchange.Redis.Extensions.Core.Abstractions;
@@ -20,11 +22,58 @@ public async Task DeleteAsync_UsesAPooledDatabaseForEachOperation()
2022
.ReturnsAsync(true);
2123
redisDatabase.SetupGet(item => item.Database).Returns(database.Object);
2224

23-
var cache = new RedisResourceCache(redisDatabase.Object, Mock.Of<ILogger<RedisResourceCache>>());
25+
var cache = new RedisResourceCache(
26+
redisDatabase.Object,
27+
Options.Create(new ResourceCacheSettings()),
28+
Mock.Of<ILogger<RedisResourceCache>>());
2429

2530
await cache.DeleteAsync(new List<string> { "first" });
2631
await cache.DeleteAsync(new List<string> { "second" });
2732

2833
redisDatabase.VerifyGet(item => item.Database, Times.Exactly(2));
2934
}
35+
36+
[Fact]
37+
public async Task UpdateCorrelationCacheAsync_SetsConfiguredExpiryAfterWritingEntries()
38+
{
39+
const int cacheEntryTtlDays = 14;
40+
var redisDatabase = new Mock<IRedisDatabase>();
41+
var database = new Mock<IDatabase>();
42+
database
43+
.Setup(item => item.HashSetAsync(
44+
It.IsAny<RedisKey>(),
45+
It.IsAny<HashEntry[]>(),
46+
It.IsAny<CommandFlags>()))
47+
.Returns(Task.CompletedTask);
48+
database
49+
.Setup(item => item.KeyExpireAsync(
50+
It.IsAny<RedisKey>(),
51+
It.IsAny<TimeSpan?>(),
52+
It.IsAny<ExpireWhen>(),
53+
It.IsAny<CommandFlags>()))
54+
.ReturnsAsync(true);
55+
redisDatabase.SetupGet(item => item.Database).Returns(database.Object);
56+
57+
var cache = new RedisResourceCache(
58+
redisDatabase.Object,
59+
Options.Create(new ResourceCacheSettings
60+
{
61+
Redis = new ResourceCacheRedisSettings { CacheEntryTtlDays = cacheEntryTtlDays }
62+
}),
63+
Mock.Of<ILogger<RedisResourceCache>>());
64+
65+
await cache.UpdateCorrelationCacheAsync("correlation-id", [], ResourceType.Patient);
66+
67+
database.Verify(item => item.HashSetAsync(
68+
"correlation-id",
69+
It.IsAny<HashEntry[]>(),
70+
CommandFlags.None),
71+
Times.Once);
72+
database.Verify(item => item.KeyExpireAsync(
73+
"correlation-id",
74+
TimeSpan.FromDays(cacheEntryTtlDays),
75+
ExpireWhen.Always,
76+
CommandFlags.None),
77+
Times.Once);
78+
}
3079
}

DotNet/Shared/Application/Extensions/ResourceCacheExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static class ResourceCacheExtensions
3838
/// <code>
3939
/// "ResourceCache": {
4040
/// "CacheImplementation": "Hybrid",
41-
/// "Redis": { "ConnectionString": "", "Password": "", "PoolSize": 5, "MemoryThresholdPercent": 80.0 },
41+
/// "Redis": { "ConnectionString": "", "Password": "", "PoolSize": 5, "CacheEntryTtlDays": 7, "MemoryThresholdPercent": 80.0 },
4242
/// "BlobStorage": { "ConnectionString": "", "BlobContainerName": "", "BlobRoot": "" }
4343
/// }
4444
/// </code>

DotNet/Shared/Application/Models/Configs/ResourceCacheSettings.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ public class ResourceCacheRedisSettings
2525
public string? ConnectionString { get; set; }
2626
public string? Password { get; set; }
2727
public int PoolSize { get; set; } = 5;
28+
29+
/// <summary>
30+
/// The number of days Redis resource-cache entries remain after their most recent write.
31+
/// Defaults to 7.
32+
/// </summary>
33+
public int CacheEntryTtlDays { get; set; } = 7;
34+
2835
/// <summary>
2936
/// The percentage of the configured Redis max-memory (<see cref="MaxMemoryBytes"/>) at
3037
/// which Hybrid caching falls back to ABS. Defaults to 80. When

DotNet/Shared/Application/Services/ResourceCache/RedisResourceCache.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
using Hl7.Fhir.Serialization;
33
using LantanaGroup.Link.Shared.Application.Enums;
44
using LantanaGroup.Link.Shared.Application.Interfaces;
5+
using LantanaGroup.Link.Shared.Application.Models.Configs;
56
using LantanaGroup.Link.Shared.Application.SerDes;
67
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Options;
79
using StackExchange.Redis;
810
using StackExchange.Redis.Extensions.Core.Abstractions;
911
using System.Text.Json;
@@ -15,11 +17,25 @@ public class RedisResourceCache : IResourceCache
1517
{
1618
private readonly IRedisDatabase _redisDatabase;
1719
private readonly ILogger<RedisResourceCache> _logger;
20+
private readonly TimeSpan _cacheEntryTtl;
1821

19-
public RedisResourceCache(IRedisDatabase redisDatabase, ILogger<RedisResourceCache> logger)
22+
public RedisResourceCache(
23+
IRedisDatabase redisDatabase,
24+
IOptions<ResourceCacheSettings> settings,
25+
ILogger<RedisResourceCache> logger)
2026
{
2127
_redisDatabase = redisDatabase;
2228
_logger = logger;
29+
30+
var cacheEntryTtlDays = settings.Value.Redis.CacheEntryTtlDays;
31+
if (cacheEntryTtlDays <= 0)
32+
{
33+
throw new ArgumentOutOfRangeException(
34+
nameof(settings),
35+
"ResourceCache:Redis:CacheEntryTtlDays must be greater than zero.");
36+
}
37+
38+
_cacheEntryTtl = TimeSpan.FromDays(cacheEntryTtlDays);
2339
}
2440

2541
public async Task DeleteAsync(List<string> cacheKeys, CancellationToken cancellationToken = default)
@@ -83,6 +99,7 @@ public async Task UpdateCorrelationCacheAsync(string correlationId, List<DomainR
8399
}
84100

85101
await _redisDatabase.Database.HashSetAsync(correlationId, correlationHash.ToArray()).WaitAsync(cancellationToken);
102+
await _redisDatabase.Database.KeyExpireAsync(correlationId, _cacheEntryTtl).WaitAsync(cancellationToken);
86103
}
87104

88105
public ResourceCacheType GetCacheTypeForCorrelationId(string correlationId)

app-config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ services:
522522
- key: "ResourceCache:CacheImplementation"
523523
description: "The resource cache implementation to register. Defaults to Hybrid; supported values are Hybrid, Redis, and ABS. Redis and Hybrid use ConnectionStrings:Redis and Redis:Password."
524524
required: false
525+
- key: "ResourceCache:Redis:CacheEntryTtlDays"
526+
description: "The number of days Redis resource-cache entries remain after their most recent write. Defaults to 7."
527+
required: false
528+
defaultValue: "7"
525529
- key: "ResourceCache:Redis:MemoryThresholdPercent"
526530
description: "Optional percentage of ResourceCache:Redis:MaxMemoryBytes at which Hybrid caching falls back to blob storage. Defaults to 80.0."
527531
required: false
@@ -559,6 +563,10 @@ services:
559563
- key: "ResourceCache:CacheImplementation"
560564
description: "The resource cache implementation to register. Defaults to Hybrid; supported values are Hybrid, Redis, and ABS. Redis and Hybrid use ConnectionStrings:Redis and Redis:Password."
561565
required: false
566+
- key: "ResourceCache:Redis:CacheEntryTtlDays"
567+
description: "The number of days Redis resource-cache entries remain after their most recent write. Defaults to 7."
568+
required: false
569+
defaultValue: "7"
562570
- key: "ResourceCache:Redis:MemoryThresholdPercent"
563571
description: "Optional percentage of ResourceCache:Redis:MaxMemoryBytes at which Hybrid caching falls back to blob storage. Defaults to 80.0."
564572
required: false
@@ -640,6 +648,10 @@ services:
640648
description: "Optional Redis password for the resource cache. Defaults to empty."
641649
required: false
642650
sensitive: true
651+
- key: "ResourceCache:Redis:CacheEntryTtlDays"
652+
description: "The number of days Redis resource-cache entries remain after their most recent write. Defaults to 7."
653+
required: false
654+
defaultValue: "7"
643655
- key: "ResourceCache:Redis:MemoryThresholdPercent"
644656
description: "Optional percentage of ResourceCache:Redis:MaxMemoryBytes at which Hybrid caching falls back to blob storage. Defaults to 80.0."
645657
required: false

0 commit comments

Comments
 (0)