From eab024c4cfbd3770cd80889432e64cb2b2855745 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Thu, 30 Jul 2026 09:30:30 +0800 Subject: [PATCH 1/3] remove any EndpointDocuments that haven't seen any throughput data for the reporting period --- .../InMemoryLicensingDataStore.cs | 9 +++++++++ .../ILicensingDataStore.cs | 2 ++ ...ughputCollector_ThroughputSummary_Tests.cs | 19 +++++++++++++++++-- .../ThroughputCollector.cs | 9 ++++++++- .../ThroughputDataExtensions.cs | 2 +- .../Implementation/LicensingDataStore.cs | 1 + .../Throughput/LicensingDataStore.cs | 17 ++++++++++++++++- 7 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs index 46639e6305..cd5e67c868 100644 --- a/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence.InMemory/InMemoryLicensingDataStore.cs @@ -179,6 +179,15 @@ public Task SaveLicensedEndpointDetails(LicensedEndpointDetails result, Cancella return Task.CompletedTask; } + public Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken) + { + foreach (var id in endpointIds) + { + endpoints.Remove(id); + } + return Task.CompletedTask; + } + class EndpointCollection : KeyedCollection { protected override EndpointIdentifier GetKeyForItem(Endpoint item) => item.Id; diff --git a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs index 3a73a50891..d2121fcde5 100644 --- a/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs +++ b/src/Particular.LicensingComponent.Persistence/ILicensingDataStore.cs @@ -15,6 +15,8 @@ public interface ILicensingDataStore Task SaveEndpoint(Endpoint endpoint, CancellationToken cancellationToken); + Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken); + Task>> GetEndpointThroughputByQueueName(IList queueNames, CancellationToken cancellationToken); Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSource, DateOnly date, long messageCount, CancellationToken cancellationToken) => diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index b3007fe41f..53aa86ae08 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -177,10 +177,11 @@ await DataStore.CreateBuilder() } [Test] - public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_no_throughput() + public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_zero_throughput() { // Arrange - await DataStore.CreateBuilder().AddEndpoint().Build(); + await DataStore.CreateBuilder().AddEndpoint().WithThroughput(new ThroughputData([ + new EndpointDailyThroughput(new DateOnly(2025, 1, 10), 0)])).Build(); // Act var summary = await ThroughputCollector.GetThroughputSummary(default); @@ -191,6 +192,20 @@ public async Task Should_return_correct_max_daily_throughput_in_summary_when_end Assert.That(summary[0].MaxDailyThroughput, Is.EqualTo(0), $"Incorrect MaxDailyThroughput recorded for {summary[0].Name}"); } + [Test] + public async Task Should_not_return_endpoint_in_summary_when_endpoint_has_no_throughput() + { + // Arrange + await DataStore.CreateBuilder().AddEndpoint().Build(); + + // Act + var summary = await ThroughputCollector.GetThroughputSummary(default); + + // Assert + Assert.That(summary, Is.Not.Null); + Assert.That(summary, Is.Empty, "Invalid number of endpoints in throughput summary"); + } + [Test] public async Task Should_return_correct_max_daily_throughput_in_summary_when_data_from_multiple_sources_and_name_is_different() { diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 32f70aaf40..04edae5b59 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -224,7 +224,14 @@ async IAsyncEnumerable GetDistinctEndpointData([EnumeratorCancella var userIndicator = UserIndicator(endpointGroupPerQueue) ?? null; - yield return new EndpointData(endpointName, throughputData, userIndicator, EndpointScope(endpointGroupPerQueue), EndpointIndicators(endpointGroupPerQueue), IsKnownEndpoint(endpointGroupPerQueue)); + if (throughputData.Any(x => x.Any())) + { + yield return new EndpointData(endpointName, throughputData, userIndicator, EndpointScope(endpointGroupPerQueue), EndpointIndicators(endpointGroupPerQueue), IsKnownEndpoint(endpointGroupPerQueue)); + } + else + { + await dataStore.RemoveEndpoints([.. endpointGroupPerQueue.Select(endpoint => endpoint.Id)], cancellationToken); + } } } diff --git a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs index ce19589692..18a11ed2f6 100644 --- a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs +++ b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs @@ -51,5 +51,5 @@ public static long AverageMonthlyThroughput(this List throughput } public static bool HasDataFromSource(this IDictionary> throughputPerQueue, ThroughputSource source) => - throughputPerQueue.Any(queueName => queueName.Value.Any(data => data.ThroughputSource == source && data.Count > 0)); + throughputPerQueue.Any(queueThroughput => queueThroughput.Value.Any(data => data.ThroughputSource == source && data.Count > 0)); } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs index acf8fe9102..f57591a034 100644 --- a/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.EFCore/Implementation/LicensingDataStore.cs @@ -14,6 +14,7 @@ class LicensingDataStore : ILicensingDataStore public Task IsThereThroughputForLastXDays(int days, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task IsThereThroughputForLastXDaysForSource(int days, ThroughputSource throughputSource, bool includeToday, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task RecordEndpointThroughput(string endpointName, ThroughputSource throughputSource, IList throughput, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveAuditServiceMetadata(AuditServiceMetadata auditServiceMetadata, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveBrokerMetadata(BrokerMetadata brokerMetadata, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task SaveEndpoint(Particular.LicensingComponent.Contracts.Endpoint endpoint, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 26b666cdcb..40faf054e6 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -25,6 +25,8 @@ class LicensingDataStore( const string ReportMasksDocumentId = "ReportMasks"; const string LicencedEndpointDetailsDocumentId = "LicensedEndpointDetails"; + const int ThroughputPeriodMonths = 14; + static readonly AuditServiceMetadata DefaultAuditServiceMetadata = new([], []); static readonly BrokerMetadata DefaultBrokerMetadata = new(null, []); static readonly ReportConfigurationDocument DefaultReportConfiguration = new(); @@ -121,6 +123,19 @@ public async Task SaveEndpoint(Endpoint endpoint, CancellationToken cancellation await session.SaveChangesAsync(cancellationToken); } + public async Task RemoveEndpoints(EndpointIdentifier[] endpointIds, CancellationToken cancellationToken) + { + var documentIds = endpointIds.Select(id => id.GenerateDocumentId()); + + var store = await storeProvider.GetDocumentStore(cancellationToken); + using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); + + foreach (var documentId in documentIds) { + session.Delete(documentId); + } + await session.SaveChangesAsync(cancellationToken); + } + public async Task>> GetEndpointThroughputByQueueName(IList queueNames, CancellationToken cancellationToken) { var results = queueNames.ToDictionary(queueName => queueName, _ => new List() as IEnumerable); @@ -128,7 +143,7 @@ public async Task>> GetEndpointT var store = await storeProvider.GetDocumentStore(cancellationToken); using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); - var from = DateTime.UtcNow.AddMonths(-14); + var from = DateTime.UtcNow.AddMonths(-ThroughputPeriodMonths); var query = session.Query() .Where(document => document.SanitizedName.In(queueNames)) .Include(builder => builder.IncludeTimeSeries(ThroughputTimeSeriesName, from)); From a6de4b9ff94e13174c68b8718bf181fcd5e14f68 Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Fri, 31 Jul 2026 15:21:54 +0800 Subject: [PATCH 2/3] fix warnings --- .../Throughput/LicensingDataStore.cs | 3 ++- src/ServiceControl/Licensing/LicenseController.cs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs index 40faf054e6..dc0b269a85 100644 --- a/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs +++ b/src/ServiceControl.Persistence.RavenDB/Throughput/LicensingDataStore.cs @@ -130,7 +130,8 @@ public async Task RemoveEndpoints(EndpointIdentifier[] endpointIds, Cancellation var store = await storeProvider.GetDocumentStore(cancellationToken); using IAsyncDocumentSession session = store.OpenAsyncSession(databaseConfiguration.Name); - foreach (var documentId in documentIds) { + foreach (var documentId in documentIds) + { session.Delete(documentId); } await session.SaveChangesAsync(cancellationToken); diff --git a/src/ServiceControl/Licensing/LicenseController.cs b/src/ServiceControl/Licensing/LicenseController.cs index 51efd95cc3..cd9580773c 100644 --- a/src/ServiceControl/Licensing/LicenseController.cs +++ b/src/ServiceControl/Licensing/LicenseController.cs @@ -1,10 +1,8 @@ #nullable enable namespace ServiceControl.Licensing { - using System; using System.IO; using System.IO.Compression; - using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; From e7b46ceedb1543415b7ca332b0ca3e7f67a0b89e Mon Sep 17 00:00:00 2001 From: Phil Bastian Date: Fri, 31 Jul 2026 15:42:14 +0800 Subject: [PATCH 3/3] update tests --- ...hroughputCollector_Report_Throughput_Tests.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs index 6603abcad0..c991f87e6c 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs @@ -194,7 +194,7 @@ await DataStore.CreateBuilder() public async Task Should_return_correct_throughput_in_report_when_endpoint_has_no_throughput() { // Arrange - await DataStore.CreateBuilder().AddEndpoint().Build(); + await DataStore.CreateBuilder().AddEndpoint().WithThroughput(ThroughputSource.Broker, data: [0]).Build(); // Act var report = await ThroughputCollector.GenerateThroughputReport("", null, default); @@ -211,6 +211,20 @@ public async Task Should_return_correct_throughput_in_report_when_endpoint_has_n } } + [Test] + public async Task Should_not_return_endpoint_in_report_when_endpoint_has_no_throughput() + { + // Arrange + await DataStore.CreateBuilder().AddEndpoint().Build(); + + // Act + var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + + // Assert + Assert.That(report, Is.Not.Null); + Assert.That(report.ReportData.Queues.Count, Is.Zero, "Invalid number of endpoints in throughput report"); + } + [Test] public async Task Should_return_correct_throughput_in_report_when_data_from_multiple_sources_and_name_is_different() {