From 805840eafef2e0ba0f98b873c08fd1410d191ce9 Mon Sep 17 00:00:00 2001 From: John Simons Date: Tue, 29 Sep 2026 10:46:10 +1000 Subject: [PATCH 1/2] Track and report configured and live audit instance counts Adds ConfiguredInstances and LiveInstances to AuditServiceMetadata so that throughput reports can include how many audit instances are configured versus how many are actively responding. A remote is considered a live audit instance only if its configuration includes an audit queue and it has a parseable semantic version. Compatibility tests confirm that existing persisted metadata without these fields reads back with null counts. --- .../AuditServiceMetadata.cs | 6 ++- .../RemoteInstanceInformation.cs | 1 + .../AuditQuery_Tests.cs | 25 +++++++++ ...tThroughputCollectorHostedService_Tests.cs | 53 +++++++++++++++++++ ...tor_Report_EnvironmentInformation_Tests.cs | 32 +++++++++++ .../AuditThroughput/AuditQuery.cs | 5 +- .../AuditThroughputCollectorHostedService.cs | 8 ++- .../ThroughputCollector.cs | 11 ++++ .../AuditServiceMetadataCompatibilityTests.cs | 38 +++++++++++++ .../EFCore/LicensingDataStoreEFTests.cs | 25 +++++++++ .../Throughput/AuditServiceMetadataTests.cs | 14 +++++ 11 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 src/ServiceControl.Persistence.Tests.RavenDB/Throughput/AuditServiceMetadataCompatibilityTests.cs diff --git a/src/Particular.LicensingComponent.Contracts/AuditServiceMetadata.cs b/src/Particular.LicensingComponent.Contracts/AuditServiceMetadata.cs index df0ce40187..342a287e89 100644 --- a/src/Particular.LicensingComponent.Contracts/AuditServiceMetadata.cs +++ b/src/Particular.LicensingComponent.Contracts/AuditServiceMetadata.cs @@ -1,3 +1,7 @@ namespace Particular.LicensingComponent.Contracts; -public record AuditServiceMetadata(Dictionary Versions, Dictionary Transports); \ No newline at end of file +public record AuditServiceMetadata(Dictionary Versions, Dictionary Transports) +{ + public int? ConfiguredInstances { get; init; } + public int? LiveInstances { get; init; } +} diff --git a/src/Particular.LicensingComponent.Contracts/RemoteInstanceInformation.cs b/src/Particular.LicensingComponent.Contracts/RemoteInstanceInformation.cs index 6c3e4202d9..fe0840e0e9 100644 --- a/src/Particular.LicensingComponent.Contracts/RemoteInstanceInformation.cs +++ b/src/Particular.LicensingComponent.Contracts/RemoteInstanceInformation.cs @@ -11,4 +11,5 @@ public class RemoteInstanceInformation public SemanticVersion? SemanticVersion { get; set; } public TimeSpan Retention { get; set; } public string? Transport { get; set; } + public bool RespondedAsAuditInstance { get; set; } } diff --git a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs index 0d9c13d5b8..ebddb2031f 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs @@ -73,6 +73,16 @@ public async Task Should_return_audit_remotes() } } + [Test] + public async Task Should_flag_only_remotes_whose_configuration_has_an_audit_queue_as_audit_instances() + { + var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), new ConfigurationApi_ReturningAuditPrimaryAndUnreachableRemotes()); + + var remotes = await auditQuery.GetAuditRemotes(); + + Assert.That(remotes.Select(remote => remote.RespondedAsAuditInstance), Is.EqualTo(new[] { true, false, false })); + } + [Test] public async Task Should_return_successful_audit_connection_if_instances_exist_and_are_online() { @@ -167,6 +177,21 @@ public Task GetRemoteConfigs(CancellationToken cancellati public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } + class ConfigurationApi_ReturningAuditPrimaryAndUnreachableRemotes : IConfigurationApi + { + public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Task GetRemoteConfigs(CancellationToken cancellationToken = default) => + Task.FromResult( + [ + new RemoteConfiguration { ApiUri = "http://audit:44444/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","audit_queue":"audit"}}""") }, + new RemoteConfiguration { ApiUri = "http://primary:33333/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","error_queue":"error"}}""") }, + new RemoteConfiguration { ApiUri = "http://decommissioned:44444/api/", Status = "unavailable", Version = "Unknown" } + ]); + + public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + class ConfigurationApi_Configurable : IConfigurationApi { public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); diff --git a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs index e548393052..a77b84783a 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditThroughputCollectorHostedService_Tests.cs @@ -245,6 +245,33 @@ await Task.Run(async () => } } + [Test] + public async Task Should_store_how_many_audit_instances_are_configured_and_how_many_are_live() + { + var liveAuditInstance = new RemoteInstanceInformation { Status = "online", VersionString = "6.2.0", SemanticVersion = new SemanticVersion(6, 2, 0), RespondedAsAuditInstance = true }; + var primaryInstanceListedAsRemote = new RemoteInstanceInformation { Status = "online", VersionString = "6.2.0", SemanticVersion = new SemanticVersion(6, 2, 0) }; + var auditInstanceWithoutVersionHeader = new RemoteInstanceInformation { Status = "online", VersionString = "Missing", RespondedAsAuditInstance = true }; + var unreachableRemote = new RemoteInstanceInformation { Status = "unavailable", VersionString = "Unknown" }; + var auditQuery = new AuditQuery_WithRemotes([liveAuditInstance, primaryInstanceListedAsRemote, auditInstanceWithoutVersionHeader, unreachableRemote]); + + using var auditThroughputCollectorHostedService = new AuditThroughputCollectorHostedService( + NullLogger.Instance, configuration.ThroughputSettings, DataStore, + auditQuery, new FakeTimeProvider()) + { DelayStart = TimeSpan.Zero }; + + await auditThroughputCollectorHostedService.StartAsync(CancellationToken.None); + await auditQuery.KnownEndpointsRequested.WaitAsync(TimeSpan.FromSeconds(30)); + await auditThroughputCollectorHostedService.StopAsync(CancellationToken.None); + + var auditServiceMetadata = await DataStore.GetAuditServiceMetadata(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(auditServiceMetadata.ConfiguredInstances, Is.EqualTo(4)); + Assert.That(auditServiceMetadata.LiveInstances, Is.EqualTo(1)); + } + } + class AuditQuery_NoAuditRemotes : IAuditQuery { public SemanticVersion MinAuditCountsVersion => new(4, 29, 0); @@ -423,4 +450,30 @@ public Task TestAuditConnection(CancellationToken readonly DateOnly throughputDate; readonly long throughputCount; } + + class AuditQuery_WithRemotes(List remotes) : IAuditQuery + { + public Task KnownEndpointsRequested => knownEndpointsRequested.Task; + + public SemanticVersion MinAuditCountsVersion => new(4, 29, 0); + public Func ValidRemoteInstances => _ => true; + + public Task> GetAuditRemotes(CancellationToken cancellationToken = default) => + Task.FromResult(remotes); + + public Task> GetKnownEndpoints(CancellationToken cancellationToken = default) + { + knownEndpointsRequested.TrySetResult(); + + return Task.FromResult>([]); + } + + public Task> GetAuditCountForEndpoint(string endpointUrlName, CancellationToken cancellationToken = default) => + Task.FromResult>([]); + + public Task TestAuditConnection(CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + readonly TaskCompletionSource knownEndpointsRequested = new(TaskCreationOptions.RunContinuationsAsynchronously); + } } \ No newline at end of file diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs index 200ac65fa8..b91bac763e 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_EnvironmentInformation_Tests.cs @@ -196,4 +196,36 @@ await DataStore.CreateBuilder() Assert.That(report.ReportData.ScopeType, Is.Not.Null, $"Missing ScopeType from report"); Assert.That(report.ReportData.ScopeType, Is.EqualTo(expectedScopeType), $"Invalid ScopeType on report"); } + + [Test] + public async Task Should_include_audit_instance_counts_in_environment_data() + { + await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata([], []) { ConfiguredInstances = 50, LiveInstances = 2 }); + + var report = await ThroughputCollector.GenerateThroughputReport("", null); + + var environmentData = report.ReportData.EnvironmentInformation.EnvironmentData; + using (Assert.EnterMultipleScope()) + { + Assert.That(environmentData, Does.ContainKey("Audit.ConfiguredInstances").WithValue("50")); + Assert.That(environmentData, Does.ContainKey("Audit.LiveInstances").WithValue("2")); + } + } + + [Test] + public async Task Should_leave_audit_instance_counts_out_of_environment_data_when_none_are_stored() + { + await DataStore.SaveAuditServiceMetadata(new AuditServiceMetadata( + new Dictionary { ["6.2.0"] = 1 }, + new Dictionary { ["RabbitMQ.QuorumConventionalRouting"] = 1 })); + + var report = await ThroughputCollector.GenerateThroughputReport("", null); + + var environmentData = report.ReportData.EnvironmentInformation.EnvironmentData; + using (Assert.EnterMultipleScope()) + { + Assert.That(environmentData, Does.Not.ContainKey("Audit.ConfiguredInstances")); + Assert.That(environmentData, Does.Not.ContainKey("Audit.LiveInstances")); + } + } } \ No newline at end of file diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs index 619a6ebae6..12ab725394 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs @@ -53,6 +53,7 @@ public async Task> GetAuditRemotes(CancellationT { string? retention = null; string? transportTypeUsed = null; + var respondedAsAuditInstance = false; if (remote.Configuration != null) { retention = remote.Configuration.AsObject().TryGetPropertyValue("data_retention", out var dataRetention) && @@ -71,6 +72,7 @@ public async Task> GetAuditRemotes(CancellationT if (transport?.AsObject().TryGetPropertyValue("audit_queue", out var auditQueue) == true) { queues.Add(auditQueue!.GetValue()); + respondedAsAuditInstance = true; } if (transport?.AsObject().TryGetPropertyValue("audit_log_queue", out var auditLogQueue) == true) { @@ -90,7 +92,8 @@ public async Task> GetAuditRemotes(CancellationT Status = remote.Status, Retention = TimeSpan.TryParse(retention, out var ts) ? ts : TimeSpan.Zero, Queues = queues, - Transport = transportTypeUsed ?? "" + Transport = transportTypeUsed ?? "", + RespondedAsAuditInstance = respondedAsAuditInstance }; remoteInstance.SemanticVersion = SemanticVersion.TryParse(remoteInstance.VersionString ?? string.Empty, out var v) ? v : null; diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs index 5b345182dd..9e46eb2f4c 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditThroughputCollectorHostedService.cs @@ -155,7 +155,13 @@ async Task SaveAuditInstanceData(List? auditRemotes, .GroupBy(s => s.Transport!) .ToDictionary(g => g.Key, g => g.Count()); - await dataStore.SaveAuditServiceMetadata(new AuditServiceMetadata(versions, transports), cancellationToken); + var auditServiceMetadata = new AuditServiceMetadata(versions, transports) + { + ConfiguredInstances = auditRemotes.Count, + LiveInstances = auditRemotes.Count(remote => remote.RespondedAsAuditInstance && remote.SemanticVersion is not null) + }; + + await dataStore.SaveAuditServiceMetadata(auditServiceMetadata, cancellationToken); } } } \ No newline at end of file diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index e0b718c7f2..e2186f6dda 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -1,6 +1,7 @@ namespace Particular.LicensingComponent; using System; +using System.Globalization; using System.Runtime.CompilerServices; using System.Threading; using AuditThroughput; @@ -186,6 +187,16 @@ public async Task GenerateThroughputReport(string spVersion, DateT report.EnvironmentInformation.EnvironmentData[EnvironmentDataType.AuditEnabled.ToString()] = systemHasAuditEnabled.ToString(); report.EnvironmentInformation.EnvironmentData[EnvironmentDataType.MonitoringEnabled.ToString()] = systemHasMonitoringEnabled.ToString(); + if (auditServiceMetadata.ConfiguredInstances is { } configuredAuditInstances) + { + report.EnvironmentInformation.EnvironmentData["Audit.ConfiguredInstances"] = configuredAuditInstances.ToString(CultureInfo.InvariantCulture); + } + + if (auditServiceMetadata.LiveInstances is { } liveAuditInstances) + { + report.EnvironmentInformation.EnvironmentData["Audit.LiveInstances"] = liveAuditInstances.ToString(CultureInfo.InvariantCulture); + } + foreach (var environmentDataProvider in environmentDataProviders) { EnvironmentDatum[] environmentData; diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/Throughput/AuditServiceMetadataCompatibilityTests.cs b/src/ServiceControl.Persistence.Tests.RavenDB/Throughput/AuditServiceMetadataCompatibilityTests.cs new file mode 100644 index 0000000000..7d76aaf78a --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.RavenDB/Throughput/AuditServiceMetadataCompatibilityTests.cs @@ -0,0 +1,38 @@ +namespace ServiceControl.Persistence.Tests.RavenDB.Throughput; + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using ServiceControl.Persistence.RavenDB.Throughput; + +[TestFixture] +class AuditServiceMetadataCompatibilityTests : RavenPersistenceTestBase +{ + [Test] + public async Task Audit_service_metadata_saved_before_the_instance_counts_existed_reads_back_without_them() + { + var throughputDatabase = ServiceProvider.GetRequiredService().Name; + + using (var session = DocumentStore.OpenAsyncSession(throughputDatabase)) + { + var metadataWithoutCounts = new AuditServiceMetadataBeforeInstanceCounts( + new Dictionary { ["6.2.0"] = 2 }, + new Dictionary { ["RabbitMQ.QuorumConventionalRouting"] = 2 }); + + await session.StoreAsync(metadataWithoutCounts, "AuditServiceMetadata"); + await session.SaveChangesAsync(); + } + + var auditServiceMetadata = await LicensingDataStore.GetAuditServiceMetadata(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(auditServiceMetadata.Versions, Is.EquivalentTo(new Dictionary { ["6.2.0"] = 2 })); + Assert.That(auditServiceMetadata.ConfiguredInstances, Is.Null); + Assert.That(auditServiceMetadata.LiveInstances, Is.Null); + } + } + + record AuditServiceMetadataBeforeInstanceCounts(Dictionary Versions, Dictionary Transports); +} diff --git a/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs b/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs index df7d63bc5c..efb31a54e2 100644 --- a/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs +++ b/src/ServiceControl.Persistence.Tests/EFCore/LicensingDataStoreEFTests.cs @@ -1,9 +1,14 @@ namespace ServiceControl.Persistence.Tests; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using EFCore.DbContexts; +using EFCore.Entities; +using EFCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Particular.LicensingComponent.Contracts; @@ -203,6 +208,26 @@ await LicensingDataStore.SaveEndpoint( } } + [Test] + public async Task Audit_service_metadata_saved_before_the_instance_counts_existed_reads_back_without_them() + { + await using (var scope = ServiceProvider.CreateAsyncScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Settings.Add(new SettingEntity { Key = SettingKeys.AuditServiceMetadata, Value = """{"Versions":{"6.2.0":2},"Transports":{"RabbitMQ.QuorumConventionalRouting":2}}""" }); + await dbContext.SaveChangesAsync(); + } + + var auditServiceMetadata = await LicensingDataStore.GetAuditServiceMetadata(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(auditServiceMetadata.Versions, Is.EquivalentTo(new Dictionary { ["6.2.0"] = 2 })); + Assert.That(auditServiceMetadata.ConfiguredInstances, Is.Null); + Assert.That(auditServiceMetadata.LiveInstances, Is.Null); + } + } + Task SaveEndpoint(string name, ThroughputSource source) => LicensingDataStore.SaveEndpoint(new Endpoint(name, source) { SanitizedName = name }); diff --git a/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs b/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs index 5707e68d53..97f576374f 100644 --- a/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs +++ b/src/ServiceControl.Persistence.Tests/Throughput/AuditServiceMetadataTests.cs @@ -30,6 +30,20 @@ public async Task Should_retrieve_saved_audit_service_metadata() } } + [Test] + public async Task Should_retrieve_saved_audit_instance_counts() + { + await LicensingDataStore.SaveAuditServiceMetadata(new AuditServiceMetadata([], []) { ConfiguredInstances = 50, LiveInstances = 2 }); + + var retrievedAuditServiceMetadata = await LicensingDataStore.GetAuditServiceMetadata(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(retrievedAuditServiceMetadata.ConfiguredInstances, Is.EqualTo(50)); + Assert.That(retrievedAuditServiceMetadata.LiveInstances, Is.EqualTo(2)); + } + } + [Test] public async Task Should_update_existing_audit_service_metadata_if_already_exists() { From 59ceba4e3a80b2881bc78f0a50d2787cbbfd765b Mon Sep 17 00:00:00 2001 From: John Simons Date: Tue, 29 Sep 2026 10:54:22 +1000 Subject: [PATCH 2/2] Assign queues per remote instead of accumulating them all into a shared list Moving the queue list initialization inside the loop ensures each remote tracks only its own audit and audit log queues, rather than inheriting queues discovered from previous remotes. A new test confirms that two remotes each report only their own queues. The fake configuration API is also generalized to accept arbitrary remote configurations via a params constructor, replacing the single-purpose named class. --- .../AuditQuery_Tests.cs | 29 +++++++++++++------ .../AuditThroughput/AuditQuery.cs | 3 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs index ebddb2031f..082fe266ee 100644 --- a/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/AuditQuery_Tests.cs @@ -76,13 +76,30 @@ public async Task Should_return_audit_remotes() [Test] public async Task Should_flag_only_remotes_whose_configuration_has_an_audit_queue_as_audit_instances() { - var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), new ConfigurationApi_ReturningAuditPrimaryAndUnreachableRemotes()); + var configurationApi = new ConfigurationApi_ReturningRemotes( + new RemoteConfiguration { ApiUri = "http://audit:44444/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","audit_queue":"audit"}}""") }, + new RemoteConfiguration { ApiUri = "http://primary:33333/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","error_queue":"error"}}""") }, + new RemoteConfiguration { ApiUri = "http://decommissioned:44444/api/", Status = "unavailable", Version = "Unknown" }); + var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), configurationApi); var remotes = await auditQuery.GetAuditRemotes(); Assert.That(remotes.Select(remote => remote.RespondedAsAuditInstance), Is.EqualTo(new[] { true, false, false })); } + [Test] + public async Task Should_return_each_remote_with_only_its_own_queues() + { + var configurationApi = new ConfigurationApi_ReturningRemotes( + new RemoteConfiguration { ApiUri = "http://audit-a:44444/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"audit_queue":"audit.a","audit_log_queue":"audit.a.log"}}""") }, + new RemoteConfiguration { ApiUri = "http://audit-b:44444/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"audit_queue":"audit.b","audit_log_queue":"audit.b.log"}}""") }); + var auditQuery = new AuditQuery(NullLogger.Instance, new FakeEndpointApi(), new FakeAuditCountApi(), configurationApi); + + var remotes = await auditQuery.GetAuditRemotes(); + + Assert.That(remotes.Select(remote => remote.Queues), Is.EqualTo(new[] { new[] { "audit.a", "audit.a.log" }, new[] { "audit.b", "audit.b.log" } })); + } + [Test] public async Task Should_return_successful_audit_connection_if_instances_exist_and_are_online() { @@ -177,17 +194,11 @@ public Task GetRemoteConfigs(CancellationToken cancellati public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } - class ConfigurationApi_ReturningAuditPrimaryAndUnreachableRemotes : IConfigurationApi + class ConfigurationApi_ReturningRemotes(params RemoteConfiguration[] remotes) : IConfigurationApi { public Task GetConfig(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task GetRemoteConfigs(CancellationToken cancellationToken = default) => - Task.FromResult( - [ - new RemoteConfiguration { ApiUri = "http://audit:44444/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","audit_queue":"audit"}}""") }, - new RemoteConfiguration { ApiUri = "http://primary:33333/api/", Status = "online", Version = "6.2.0", Configuration = JsonNode.Parse("""{"transport":{"transport_type":"RabbitMQ.QuorumConventionalRouting","error_queue":"error"}}""") }, - new RemoteConfiguration { ApiUri = "http://decommissioned:44444/api/", Status = "unavailable", Version = "Unknown" } - ]); + public Task GetRemoteConfigs(CancellationToken cancellationToken = default) => Task.FromResult(remotes); public Task GetUrls(string baseUrl, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } diff --git a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs index 12ab725394..e048619e34 100644 --- a/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs +++ b/src/Particular.LicensingComponent/AuditThroughput/AuditQuery.cs @@ -47,10 +47,9 @@ public async Task> GetAuditRemotes(CancellationT if (remotes.Any()) { - List queues = []; - foreach (var remote in remotes) { + List queues = []; string? retention = null; string? transportTypeUsed = null; var respondedAsAuditInstance = false;