Skip to content

Commit c6db615

Browse files
authored
Implement user-scoped template response caching using HybridCache (#188)
1 parent cd17cb0 commit c6db615

25 files changed

Lines changed: 1419 additions & 668 deletions

File tree

source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@
126126
"baseUrl": "http://localhost:8081",
127127
"healthEndpoint": "/actuator/health",
128128
"ConcurrentOperationsLimit": 10,
129+
"LocalCacheExpirationInMinutes": 5,
129130
"headerMappings": [
130131
{
131132
"source": "Authorization",
@@ -144,20 +145,23 @@
144145
"baseUrl": "http://localhost:8081",
145146
"healthEndpoint": "/actuator/health",
146147
"headerMappings": [],
147-
"ConcurrentOperationsLimit": 10
148+
"ConcurrentOperationsLimit": 10,
149+
"LocalCacheExpirationInMinutes": 5
148150
},
149151
"ConceptDescriptionTemplateRepository": {
150152
"Name": "ConceptDescriptionTemplateRepository",
151153
"baseUrl": "http://localhost:8081",
152154
"healthEndpoint": "/actuator/health",
153155
"headerMappings": [],
154-
"ConcurrentOperationsLimit": 10
156+
"ConcurrentOperationsLimit": 10,
157+
"LocalCacheExpirationInMinutes": 5
155158
},
156159
"AasTemplateRegistry": {
157160
"Name": "AasTemplateRegistry",
158161
"baseUrl": "http://localhost:8082",
159162
"healthEndpoint": "/actuator/health",
160163
"ConcurrentOperationsLimit": 10,
164+
"LocalCacheExpirationInMinutes": 5,
161165
"headerMappings": [
162166
{
163167
"source": "Authorization",
@@ -171,6 +175,7 @@
171175
"baseUrl": "http://localhost:8083",
172176
"healthEndpoint": "/actuator/health",
173177
"ConcurrentOperationsLimit": 10,
178+
"LocalCacheExpirationInMinutes": 5,
174179
"headerMappings": [
175180
{
176181
"source": "Authorization",

source/AAS.TwinEngine.DataEngine.ModuleTests/packages.lock.json

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,37 @@
157157
"resolved": "10.0.0",
158158
"contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg=="
159159
},
160+
"Microsoft.Extensions.Caching.Abstractions": {
161+
"type": "Transitive",
162+
"resolved": "10.0.10",
163+
"contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==",
164+
"dependencies": {
165+
"Microsoft.Extensions.Primitives": "10.0.10"
166+
}
167+
},
168+
"Microsoft.Extensions.Caching.Hybrid": {
169+
"type": "Transitive",
170+
"resolved": "10.8.0",
171+
"contentHash": "RCtCTK3eqKXx8LXqd7B8GjbTkIp4k3EgKeunaCmBJ+WA55Nva83CI2I+wVyp0S9jTG+aT6AYWQbkKOfvFOlmLA==",
172+
"dependencies": {
173+
"Microsoft.Extensions.Caching.Abstractions": "10.0.10",
174+
"Microsoft.Extensions.Caching.Memory": "10.0.10",
175+
"Microsoft.Extensions.Logging.Abstractions": "10.0.10",
176+
"Microsoft.Extensions.Options": "10.0.10"
177+
}
178+
},
179+
"Microsoft.Extensions.Caching.Memory": {
180+
"type": "Transitive",
181+
"resolved": "10.0.10",
182+
"contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==",
183+
"dependencies": {
184+
"Microsoft.Extensions.Caching.Abstractions": "10.0.10",
185+
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
186+
"Microsoft.Extensions.Logging.Abstractions": "10.0.10",
187+
"Microsoft.Extensions.Options": "10.0.10",
188+
"Microsoft.Extensions.Primitives": "10.0.10"
189+
}
190+
},
160191
"Microsoft.Extensions.Compliance.Abstractions": {
161192
"type": "Transitive",
162193
"resolved": "10.6.0",
@@ -254,8 +285,8 @@
254285
},
255286
"Microsoft.Extensions.DependencyInjection.Abstractions": {
256287
"type": "Transitive",
257-
"resolved": "10.0.8",
258-
"contentHash": "21nbDV60SRPWGIivsyl6lqBeEJNG1sginhhfWgRrr3Ais7aQ12To25OAHQxgoiJkjqy1aQ6RxpZBGYuTi7Ge6A=="
288+
"resolved": "10.0.10",
289+
"contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA=="
259290
},
260291
"Microsoft.Extensions.DependencyInjection.AutoActivation": {
261292
"type": "Transitive",
@@ -405,10 +436,10 @@
405436
},
406437
"Microsoft.Extensions.Logging.Abstractions": {
407438
"type": "Transitive",
408-
"resolved": "10.0.8",
409-
"contentHash": "fdVadZmsC8jRP0KvKy8mO8f6GV/HyBvElfcSxEhd+5FM5boAw/01iSaCto5G3G37ApJira4A3pNaVvBv8cUiLQ==",
439+
"resolved": "10.0.10",
440+
"contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==",
410441
"dependencies": {
411-
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8"
442+
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10"
412443
}
413444
},
414445
"Microsoft.Extensions.Logging.Configuration": {
@@ -479,11 +510,11 @@
479510
},
480511
"Microsoft.Extensions.Options": {
481512
"type": "Transitive",
482-
"resolved": "10.0.8",
483-
"contentHash": "VBD+131DpTNCNDfA4kIyKTiCySvJGNhwibdWBSdFRu7GMfXLXcXODkgA+KStKbbhzraLglZWUN4nXyHgW4JIRA==",
513+
"resolved": "10.0.10",
514+
"contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==",
484515
"dependencies": {
485-
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.8",
486-
"Microsoft.Extensions.Primitives": "10.0.8"
516+
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10",
517+
"Microsoft.Extensions.Primitives": "10.0.10"
487518
}
488519
},
489520
"Microsoft.Extensions.Options.ConfigurationExtensions": {
@@ -500,8 +531,8 @@
500531
},
501532
"Microsoft.Extensions.Primitives": {
502533
"type": "Transitive",
503-
"resolved": "10.0.8",
504-
"contentHash": "OBPo4nYhMyIbtueoC10CBm6AGAbo/A9IV8QQ/6ryZS7VvmqpGT7hunazeHLxFawRzn3oLOq4jhqhpBX4tfswWQ=="
534+
"resolved": "10.0.10",
535+
"contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ=="
505536
},
506537
"Microsoft.Extensions.Resilience": {
507538
"type": "Transitive",
@@ -937,6 +968,7 @@
937968
"Asp.Versioning.Mvc": "[10.0.0, 10.0.0]",
938969
"Cronos": "[0.13.0, 0.13.0]",
939970
"JsonSchema.Net": "[7.3.4, 7.3.4]",
971+
"Microsoft.Extensions.Caching.Hybrid": "[10.8.0, )",
940972
"Microsoft.Extensions.DependencyInjection": "[10.0.8, 10.0.8]",
941973
"Microsoft.Extensions.Http": "[10.0.8, 10.0.8]",
942974
"Microsoft.Extensions.Http.Resilience": "[10.6.0, 10.6.0]",

source/AAS.TwinEngine.DataEngine.UnitTests/AAS.TwinEngine.DataEngine.UnitTests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
<ItemGroup>
1919
<PackageReference Include="coverlet.collector" Version="[10.0.1]" />
20+
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.8.0" />
21+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
2022
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="[18.6.0]" />
2123
<PackageReference Include="NSubstitute" Version="[5.3.0]" />
2224
<PackageReference Include="TngTech.ArchUnitNET.xUnit" Version="[0.13.3]" />
@@ -35,5 +37,9 @@
3537
<ItemGroup>
3638
<None Include="TestData\Test.png" CopyToOutputDirectory="Always" />
3739
</ItemGroup>
40+
41+
<ItemGroup>
42+
<Folder Include="Infrastructure\Http\Clients\Caching\" />
43+
</ItemGroup>
3844

3945
</Project>

source/AAS.TwinEngine.DataEngine.UnitTests/ApplicationLogic/Services/AasRegistry/ShellDescriptorServiceTests.cs

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry;
77
using AAS.TwinEngine.DataEngine.DomainModel.Plugin;
88
using AAS.TwinEngine.DataEngine.DomainModel.Shared;
9+
using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config;
910

1011
using AasCore.Aas3_1;
1112

1213
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Options;
1315

1416
using NSubstitute;
1517
using NSubstitute.ExceptionExtensions;
@@ -24,9 +26,18 @@ public class ShellDescriptorServiceTests
2426
private readonly IShellDescriptorDataHandler _dataHandler = Substitute.For<IShellDescriptorDataHandler>();
2527
private readonly IPluginManifestConflictHandler _pluginManifestConflictHandler = Substitute.For<IPluginManifestConflictHandler>();
2628
private readonly ILogger<ShellDescriptorService> _logger = Substitute.For<ILogger<ShellDescriptorService>>();
29+
private readonly IOptions<TemplateManagementConfig> _templateManagementConfig;
2730
private readonly ShellDescriptorService _sut;
2831

29-
public ShellDescriptorServiceTests() => _sut = new ShellDescriptorService(_templateProvider, _shellTemplateMappingProvider, _dataHandler, _pluginDataHandler, _pluginManifestConflictHandler, _logger);
32+
public ShellDescriptorServiceTests()
33+
{
34+
var config = new TemplateManagementConfig
35+
{
36+
AasTemplateRegistry = new ServiceInstance { ConcurrentOperationsLimit = 10 }
37+
};
38+
_templateManagementConfig = Options.Create(config);
39+
_sut = new ShellDescriptorService(_templateProvider, _shellTemplateMappingProvider, _dataHandler, _pluginDataHandler, _pluginManifestConflictHandler, _logger, _templateManagementConfig);
40+
}
3041

3142
[Fact]
3243
public async Task GetAllShellDescriptorsAsync_ReturnsFilledShellDescriptors()
@@ -307,8 +318,106 @@ public async Task GetShellDescriptorByIdAsync_ShouldThrowShellDescriptorNotFound
307318

308319
await Assert.ThrowsAsync<ShellDescriptorNotFoundException>(() => _sut.GetShellDescriptorByIdAsync(id, cancellationToken));
309320
}
321+
[Fact]
322+
public async Task GetAllShellDescriptorsAsync_BuildsDescriptorsInParallel()
323+
{
324+
var cancellationToken = CancellationToken.None;
325+
var metadataList = Enumerable.Range(1, 5)
326+
.Select(i => new ShellDescriptorMetaData { Id = $"id{i}" })
327+
.ToList();
328+
var metaData = new ShellDescriptorsMetaData
329+
{
330+
PagingMetaData = null,
331+
ShellDescriptors = metadataList
332+
};
333+
334+
_pluginManifestConflictHandler.Manifests.Returns(new List<PluginManifest>());
335+
_pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, Arg.Any<IReadOnlyList<PluginManifest>>(), cancellationToken)
336+
.Returns(metaData);
337+
_shellTemplateMappingProvider.GetTemplateId(Arg.Any<string>()).Returns("template-1");
338+
_templateProvider.GetShellDescriptorTemplateAsync("template-1", cancellationToken).Returns(GetShellDescriptorTemplate());
339+
_dataHandler.FillOut(Arg.Any<ShellDescriptor>(), Arg.Any<ShellDescriptorMetaData>())
340+
.Returns(callInfo =>
341+
{
342+
var value = callInfo.ArgAt<ShellDescriptorMetaData>(1);
343+
return new ShellDescriptor { Id = value.Id };
344+
});
345+
346+
var result = await _sut.GetAllShellDescriptorsAsync(null, null, cancellationToken);
347+
348+
Assert.NotNull(result);
349+
Assert.NotNull(result.Result);
350+
Assert.Equal(5, result.Result.Count);
351+
Assert.All(result.Result, descriptor => Assert.False(string.IsNullOrWhiteSpace(descriptor.Id)));
352+
}
353+
354+
[Fact]
355+
public async Task GetAllShellDescriptorsAsync_RespectsMaxConcurrency()
356+
{
357+
var cancellationToken = CancellationToken.None;
358+
const int concurrencyLimit = 2;
359+
var config = new TemplateManagementConfig
360+
{
361+
AasTemplateRegistry = new ServiceInstance { ConcurrentOperationsLimit = concurrencyLimit }
362+
};
363+
var sut = new ShellDescriptorService(
364+
_templateProvider, _shellTemplateMappingProvider, _dataHandler,
365+
_pluginDataHandler, _pluginManifestConflictHandler, _logger,
366+
Options.Create(config));
367+
368+
var currentConcurrency = 0;
369+
var maxObservedConcurrency = 0;
370+
var lockObj = new object();
371+
372+
var metadataList = Enumerable.Range(1, 6)
373+
.Select(i => new ShellDescriptorMetaData { Id = $"id{i}" })
374+
.ToList();
375+
var metaData = new ShellDescriptorsMetaData
376+
{
377+
PagingMetaData = null,
378+
ShellDescriptors = metadataList
379+
};
380+
381+
_pluginManifestConflictHandler.Manifests.Returns(new List<PluginManifest>());
382+
_pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, Arg.Any<IReadOnlyList<PluginManifest>>(), cancellationToken)
383+
.Returns(metaData);
384+
_shellTemplateMappingProvider.GetTemplateId(Arg.Any<string>()).Returns("template-1");
385+
386+
_templateProvider.GetShellDescriptorTemplateAsync("template-1", cancellationToken)
387+
.Returns(async callInfo =>
388+
{
389+
lock (lockObj)
390+
{
391+
currentConcurrency++;
392+
if (currentConcurrency > maxObservedConcurrency)
393+
maxObservedConcurrency = currentConcurrency;
394+
}
395+
396+
await Task.Delay(50, cancellationToken);
397+
398+
lock (lockObj)
399+
{
400+
currentConcurrency--;
401+
}
402+
403+
return GetShellDescriptorTemplate();
404+
});
405+
406+
_dataHandler.FillOut(Arg.Any<ShellDescriptor>(), Arg.Any<ShellDescriptorMetaData>())
407+
.Returns(callInfo =>
408+
{
409+
var value = callInfo.ArgAt<ShellDescriptorMetaData>(1);
410+
return new ShellDescriptor { Id = value.Id };
411+
});
412+
413+
var result = await sut.GetAllShellDescriptorsAsync(null, null, cancellationToken);
414+
415+
Assert.NotNull(result);
416+
Assert.Equal(6, result.Result!.Count);
417+
Assert.True(maxObservedConcurrency <= concurrencyLimit,
418+
$"Expected max concurrency <= {concurrencyLimit}, but observed {maxObservedConcurrency}");
419+
}
310420

311-
#region Test Data Helpers
312421

313422
private static List<ShellDescriptorMetaData> GetShellDescriptorDataList()
314423
=> [
@@ -391,6 +500,4 @@ private static List<ShellDescriptor> GetExpectedShellDescriptors() => [
391500
]
392501
}
393502
];
394-
395-
#endregion
396503
}

0 commit comments

Comments
 (0)