Skip to content

Commit 2a99f7a

Browse files
committed
feat: implement ShellDescriptorService to retrieve and populate AAS shell descriptors with template mapping support
1 parent 38ca92c commit 2a99f7a

4 files changed

Lines changed: 189 additions & 21 deletions

File tree

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

Lines changed: 157 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,152 @@ 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+
}
420+
421+
[Fact]
422+
public async Task GetAllShellDescriptorsAsync_SkipsFailedDescriptors_InParallelExecution()
423+
{
424+
var cancellationToken = CancellationToken.None;
425+
var metadataList = new List<ShellDescriptorMetaData>
426+
{
427+
new() { Id = "good-1" },
428+
new() { Id = "bad-1" },
429+
new() { Id = "good-2" },
430+
new() { Id = null },
431+
new() { Id = "good-3" }
432+
};
433+
var metaData = new ShellDescriptorsMetaData
434+
{
435+
PagingMetaData = null,
436+
ShellDescriptors = metadataList
437+
};
438+
439+
_pluginManifestConflictHandler.Manifests.Returns(new List<PluginManifest>());
440+
_pluginDataHandler.GetDataForAllShellDescriptorsAsync(null, null, Arg.Any<IReadOnlyList<PluginManifest>>(), cancellationToken)
441+
.Returns(metaData);
442+
443+
_shellTemplateMappingProvider.GetTemplateId("good-1").Returns("template-1");
444+
_shellTemplateMappingProvider.GetTemplateId("good-2").Returns("template-1");
445+
_shellTemplateMappingProvider.GetTemplateId("good-3").Returns("template-1");
446+
_shellTemplateMappingProvider.GetTemplateId("bad-1").Throws(new ResourceNotFoundException());
447+
448+
_templateProvider.GetShellDescriptorTemplateAsync("template-1", cancellationToken).Returns(GetShellDescriptorTemplate());
449+
_dataHandler.FillOut(Arg.Any<ShellDescriptor>(), Arg.Any<ShellDescriptorMetaData>())
450+
.Returns(callInfo =>
451+
{
452+
var value = callInfo.ArgAt<ShellDescriptorMetaData>(1);
453+
return new ShellDescriptor { Id = value.Id };
454+
});
455+
456+
var result = await _sut.GetAllShellDescriptorsAsync(null, null, cancellationToken);
457+
458+
Assert.NotNull(result);
459+
Assert.NotNull(result.Result);
460+
Assert.Equal(3, result.Result.Count);
461+
Assert.Contains(result.Result, d => d.Id == "good-1");
462+
Assert.Contains(result.Result, d => d.Id == "good-2");
463+
Assert.Contains(result.Result, d => d.Id == "good-3");
464+
Assert.DoesNotContain(result.Result, d => d.Id == "bad-1");
465+
}
310466

311-
#region Test Data Helpers
312467

313468
private static List<ShellDescriptorMetaData> GetShellDescriptorDataList()
314469
=> [
@@ -391,6 +546,4 @@ private static List<ShellDescriptor> GetExpectedShellDescriptors() => [
391546
]
392547
}
393548
];
394-
395-
#endregion
396549
}

source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Providers/TemplateProvider/Services/TemplateProviderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Net;
1+
using System.Net;
22
using System.Linq;
33
using System.Text;
44
using System.Text.Json;
@@ -50,7 +50,7 @@ public TemplateProviderTests()
5050

5151
_sut = new Template(logger, options, _cachedHttp);
5252
}
53-
53+
5454
[Fact]
5555
public async Task GetShellDescriptorTemplateAsync_ReturnsShellDescriptor_WhenValidResponse()
5656
{

source/AAS.TwinEngine.DataEngine/ApplicationLogic/Services/AasRegistry/ShellDescriptorService.cs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.AasEnvironment.Providers;
44
using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin;
55
using AAS.TwinEngine.DataEngine.DomainModel.AasRegistry;
6+
using AAS.TwinEngine.DataEngine.ServiceConfiguration.Config;
7+
8+
using Microsoft.Extensions.Options;
69

710
using UnauthorizedAccessException = AAS.TwinEngine.DataEngine.ApplicationLogic.Exceptions.Infrastructure.UnauthorizedAccessException;
811

@@ -14,8 +17,10 @@ public class ShellDescriptorService(
1417
IShellDescriptorDataHandler shellDescriptorDataHandler,
1518
IPluginDataHandler pluginDataHandler,
1619
IPluginManifestConflictHandler pluginManifestConflictHandler,
17-
ILogger<ShellDescriptorService> logger) : IShellDescriptorService
20+
ILogger<ShellDescriptorService> logger,
21+
IOptions<TemplateManagementConfig> templateManagementConfig) : IShellDescriptorService
1822
{
23+
private readonly int _concurrentOperationsLimit = templateManagementConfig.Value.AasTemplateRegistry.ConcurrentOperationsLimit;
1924
public async Task<ShellDescriptors?> GetAllShellDescriptorsAsync(int? limit, string? cursor, CancellationToken cancellationToken)
2025
{
2126
try
@@ -26,16 +31,7 @@ public class ShellDescriptorService(
2631
.ConfigureAwait(false);
2732

2833
var shellDescriptorMetadataList = metadata.ShellDescriptors ?? [];
29-
var shellDescriptors = new List<ShellDescriptor>(shellDescriptorMetadataList.Count);
30-
31-
foreach (var shellDescriptorMetadata in shellDescriptorMetadataList)
32-
{
33-
var shellDescriptor = await TryBuildShellDescriptorAsync(shellDescriptorMetadata, cancellationToken).ConfigureAwait(false);
34-
if (shellDescriptor is not null)
35-
{
36-
shellDescriptors.Add(shellDescriptor);
37-
}
38-
}
34+
var shellDescriptors = await BuildShellDescriptorsInParallelAsync(shellDescriptorMetadataList, cancellationToken).ConfigureAwait(false);
3935

4036
return new ShellDescriptors
4137
{
@@ -119,6 +115,28 @@ public class ShellDescriptorService(
119115
}
120116
}
121117

118+
private async Task<List<ShellDescriptor>> BuildShellDescriptorsInParallelAsync(
119+
List<ShellDescriptorMetaData> metadataList,
120+
CancellationToken cancellationToken)
121+
{
122+
using var semaphore = new SemaphoreSlim(_concurrentOperationsLimit, _concurrentOperationsLimit);
123+
var tasks = metadataList.Select(async metadata =>
124+
{
125+
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
126+
try
127+
{
128+
return await TryBuildShellDescriptorAsync(metadata, cancellationToken).ConfigureAwait(false);
129+
}
130+
finally
131+
{
132+
_ = semaphore.Release();
133+
}
134+
});
135+
136+
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
137+
return [.. results.OfType<ShellDescriptor>()];
138+
}
139+
122140
private async Task<ShellDescriptor> BuildShellDescriptorAsync(
123141
ShellDescriptorMetaData shellDescriptorMetadata,
124142
string templateId,

source/AAS.TwinEngine.DataEngine/Program.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ public static async Task Main(string[] args)
4848
_ = builder.Services.AddAuthorization();
4949

5050
_ = builder.Services.AddControllers()
51-
.AddJsonOptions(options =>
52-
{
53-
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
54-
});
51+
.AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
5552

5653
_ = builder.Services.AddEndpointsApiExplorer();
5754
_ = builder.Services.AddOpenApiDocument(settings =>

0 commit comments

Comments
 (0)