-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathHybridResourceCacheTests.cs
More file actions
230 lines (189 loc) · 9 KB
/
Copy pathHybridResourceCacheTests.cs
File metadata and controls
230 lines (189 loc) · 9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
using FluentAssertions;
using Hl7.Fhir.Model;
using LantanaGroup.Link.Shared.Application.Enums;
using LantanaGroup.Link.Shared.Application.Interfaces;
using LantanaGroup.Link.Shared.Application.Models.Configs;
using LantanaGroup.Link.Shared.Application.Services.ResourceCache;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using StackExchange.Redis;
using StackExchange.Redis.Extensions.Core.Abstractions;
using Task = System.Threading.Tasks.Task;
namespace UnitTests.Shared.ResourceCache;
/// <summary>
/// Covers <see cref="HybridResourceCache"/>'s Redis-vs-ABS selection, which since LEGLINK-770
/// derives the max-memory denominator from configuration (<see cref="ResourceCacheRedisSettings.MaxMemoryBytes"/>)
/// rather than Redis <c>INFO maxmemory</c> (Azure Managed Redis does not return it).
/// </summary>
[Trait("Category", "UnitTests")]
public class HybridResourceCacheTests
{
private readonly Mock<IResourceCache> _redisCache = new();
private readonly Mock<IResourceCache> _absCache = new();
private readonly Mock<IRedisDatabase> _redisDatabase = new();
private readonly Mock<IDatabase> _database = new();
private readonly Mock<IConnectionMultiplexer> _multiplexer = new();
private readonly Mock<ILogger<HybridResourceCache>> _logger = new();
public HybridResourceCacheTests()
{
_redisDatabase.SetupGet(database => database.Database).Returns(_database.Object);
_database.SetupGet(database => database.Multiplexer).Returns(_multiplexer.Object);
}
private HybridResourceCache CreateSut(ResourceCacheRedisSettings redisSettings)
{
var settings = new ResourceCacheSettings { Redis = redisSettings };
return new HybridResourceCache(
_redisCache.Object,
_absCache.Object,
_redisDatabase.Object,
Options.Create(settings),
_logger.Object);
}
/// <summary>Configures a connected Redis server whose INFO memory section returns the given used_memory.</summary>
private void SetupRedisUsedMemory(long usedMemory)
{
SetupRedisInfo(new[] { new KeyValuePair<string, string>("used_memory", usedMemory.ToString()) });
}
private void SetupRedisInfo(IEnumerable<KeyValuePair<string, string>> memoryPairs)
{
var grouping = memoryPairs.GroupBy(_ => "memory").First();
var server = new Mock<IServer>();
server.SetupGet(s => s.IsConnected).Returns(true);
server.Setup(s => s.InfoAsync(It.IsAny<RedisValue>(), It.IsAny<CommandFlags>()))
.ReturnsAsync(new[] { grouping });
_multiplexer.Setup(m => m.GetServers()).Returns(new[] { server.Object });
}
private void SetupNoConnectedServer()
{
_multiplexer.Setup(m => m.GetServers()).Returns(Array.Empty<IServer>());
}
private Task Write(HybridResourceCache sut, string correlationId)
{
return sut.UpdateCorrelationCacheAsync(correlationId, new List<DomainResource>(), ResourceType.Patient);
}
private void VerifyWroteTo(Mock<IResourceCache> expected, Mock<IResourceCache> notExpected)
{
expected.Verify(c => c.UpdateCorrelationCacheAsync(It.IsAny<string>(), It.IsAny<List<DomainResource>>(), It.IsAny<ResourceType>(), It.IsAny<CancellationToken>()), Times.Once);
notExpected.Verify(c => c.UpdateCorrelationCacheAsync(It.IsAny<string>(), It.IsAny<List<DomainResource>>(), It.IsAny<ResourceType>(), It.IsAny<CancellationToken>()), Times.Never);
}
private void VerifyWarningLogged(Times times)
{
VerifyLogged(LogLevel.Warning, times);
}
private void VerifyLogged(LogLevel level, Times times)
{
_logger.Verify(
log => log.Log(
level,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
times);
}
[Fact]
public async Task UsedMemory_below_threshold_of_configured_max_uses_Redis()
{
// 100 MB used of 1000 MB max = 10%, threshold 80% => Redis
SetupRedisUsedMemory(100L * 1024 * 1024);
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-below");
VerifyWroteTo(_redisCache, _absCache);
}
[Fact]
public async Task UsedMemory_at_or_above_threshold_of_configured_max_uses_ABS()
{
// 900 MB used of 1000 MB max = 90%, threshold 80% => ABS
SetupRedisUsedMemory(900L * 1024 * 1024);
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-above");
VerifyWroteTo(_absCache, _redisCache);
}
[Theory]
[InlineData(null)]
[InlineData(0L)]
[InlineData(-1L)]
public async Task MaxMemoryBytes_missing_or_invalid_uses_Redis_and_logs_warning(long? maxMemoryBytes)
{
SetupRedisUsedMemory(900L * 1024 * 1024); // would be ABS if a valid max were configured
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = maxMemoryBytes, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-nomax");
VerifyWroteTo(_redisCache, _absCache);
VerifyWarningLogged(Times.Once());
}
[Fact]
public async Task UsedMemory_missing_from_info_uses_Redis()
{
SetupRedisInfo(new[] { new KeyValuePair<string, string>("some_other_stat", "123") });
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-nousedmem");
VerifyWroteTo(_redisCache, _absCache);
}
[Fact]
public async Task No_connected_server_uses_ABS()
{
SetupNoConnectedServer();
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-noserver");
VerifyWroteTo(_absCache, _redisCache);
}
[Fact]
public async Task Exception_reading_memory_uses_ABS()
{
_multiplexer.Setup(m => m.GetServers()).Throws(new RedisException("boom"));
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-throws");
VerifyWroteTo(_absCache, _redisCache);
}
/// <summary>
/// The selection decision must be visible in deployed environments, which run at Information.
/// Logging it at Debug left LEGLINK-948 undiagnosable: an ABS result could not be distinguished
/// from a failed probe without redeploying at a different log level.
/// </summary>
[Fact]
public async Task Selection_decision_is_logged_at_Information()
{
SetupRedisUsedMemory(100L * 1024 * 1024);
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-logged");
VerifyLogged(LogLevel.Information, Times.AtLeastOnce());
}
[Fact]
public async Task No_connected_server_logs_a_warning()
{
SetupNoConnectedServer();
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-noserver-warn");
VerifyWarningLogged(Times.Once());
}
/// <summary>
/// Azure Managed Redis proxies Redis Enterprise and is not guaranteed to return the unique-key
/// INFO shape that open-source Redis does. A duplicate key previously threw out of
/// <c>ToDictionary</c> and was swallowed by the catch-all into a silent ABS fallback.
/// </summary>
[Fact]
public async Task Duplicate_keys_in_info_memory_do_not_force_the_ABS_fallback()
{
var usedMemory = (100L * 1024 * 1024).ToString();
SetupRedisInfo(new[]
{
new KeyValuePair<string, string>("used_memory", usedMemory),
new KeyValuePair<string, string>("used_memory", usedMemory)
});
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-duplicate-keys");
VerifyWroteTo(_redisCache, _absCache);
}
[Fact]
public async Task Decision_is_memoized_per_correlationId()
{
SetupRedisUsedMemory(100L * 1024 * 1024);
var sut = CreateSut(new ResourceCacheRedisSettings { MaxMemoryBytes = 1000L * 1024 * 1024, MemoryThresholdPercent = 80.0 });
await Write(sut, "corr-memo");
await Write(sut, "corr-memo");
// The memory pressure check (GetServers) should only happen once for the same correlationId.
_multiplexer.Verify(m => m.GetServers(), Times.Once);
_redisCache.Verify(c => c.UpdateCorrelationCacheAsync(It.IsAny<string>(), It.IsAny<List<DomainResource>>(), It.IsAny<ResourceType>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
}
}