From d711144a9b94d2fc5a812b5a49a0777e7c004b30 Mon Sep 17 00:00:00 2001 From: Christian Rebnord Date: Thu, 9 Jul 2026 12:44:40 +0200 Subject: [PATCH] Calculate TotalThroughput as the maximum organization-wide daily total The throughput report previously set TotalThroughput to the sum of each endpoint's individual best day. Endpoints that peak on different days were counted as if their peaks occurred on the same day, overstating actual throughput - increasingly so with more queues and longer reporting windows. Per the published definition of daily throughput (maximum number of messages processed by all NServiceBus endpoints across the entire organization in a day), the daily totals must be summed across endpoints first, and the maximum taken over days. When multiple sources report data for the same endpoint and day, the maximum value is used, consistent with the existing per-endpoint MaxDailyThroughput semantics. Co-Authored-By: Claude Fable 5 --- ...hould_generate_correct_report.approved.txt | 2 +- ...ughputCollector_Report_Throughput_Tests.cs | 28 ++++++++++++++++++- .../ThroughputCollector.cs | 9 +++++- .../ThroughputDataExtensions.cs | 7 +++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/ApprovalFiles/ThroughputCollector_Report_Throughput_Tests.Should_generate_correct_report.approved.txt b/src/Particular.LicensingComponent.UnitTests/ApprovalFiles/ThroughputCollector_Report_Throughput_Tests.Should_generate_correct_report.approved.txt index df4c5ac553..56f72cf300 100644 --- a/src/Particular.LicensingComponent.UnitTests/ApprovalFiles/ThroughputCollector_Report_Throughput_Tests.Should_generate_correct_report.approved.txt +++ b/src/Particular.LicensingComponent.UnitTests/ApprovalFiles/ThroughputCollector_Report_Throughput_Tests.Should_generate_correct_report.approved.txt @@ -141,7 +141,7 @@ "DailyThroughputFromMonitoring": [] } ], - "TotalThroughput": 249, + "TotalThroughput": 238, "TotalQueues": 5, "IgnoredQueues": [], "EnvironmentInformation": { 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..28ca74affa 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_Report_Throughput_Tests.cs @@ -147,11 +147,37 @@ await DataStore.CreateBuilder() using (Assert.EnterMultipleScope()) { - Assert.That(report.ReportData.TotalThroughput, Is.EqualTo(195), $"Incorrect TotalThroughput recorded"); + Assert.That(report.ReportData.TotalThroughput, Is.EqualTo(185), $"Incorrect TotalThroughput recorded"); Assert.That(report.ReportData.TotalQueues, Is.EqualTo(3), $"Incorrect TotalQueues recorded"); } } + [Test] + public async Task Should_report_total_throughput_as_maximum_daily_throughput_across_all_endpoints() + { + // Arrange - endpoints peak on different days; TotalThroughput is the best single day + // of the organization-wide sum, not the sum of each endpoint's individual peak + var startDate = new DateOnly(2024, 4, 24); + + await DataStore.CreateBuilder() + .AddEndpoint("Endpoint1", sources: [ThroughputSource.Broker]).WithThroughput(startDate: startDate, data: [100, 10]) + .AddEndpoint("Endpoint2", sources: [ThroughputSource.Broker]).WithThroughput(startDate: startDate, data: [20, 100]) + .Build(); + + // Act + var report = await ThroughputCollector.GenerateThroughputReport("", null, default); + + // Assert + Assert.That(report, Is.Not.Null); + + using (Assert.EnterMultipleScope()) + { + Assert.That(report.ReportData.Queues.First(w => w.QueueName == "Endpoint1").Throughput, Is.EqualTo(100), $"Incorrect Throughput recorded for Endpoint1"); + Assert.That(report.ReportData.Queues.First(w => w.QueueName == "Endpoint2").Throughput, Is.EqualTo(100), $"Incorrect Throughput recorded for Endpoint2"); + Assert.That(report.ReportData.TotalThroughput, Is.EqualTo(120), $"TotalThroughput should be the maximum daily throughput across all endpoints, not the sum of each endpoint's maximum"); + } + } + [Test] public async Task Should_return_correct_throughput_in_report_when_multiple_sources() { diff --git a/src/Particular.LicensingComponent/ThroughputCollector.cs b/src/Particular.LicensingComponent/ThroughputCollector.cs index 925cc203c0..af4542d210 100644 --- a/src/Particular.LicensingComponent/ThroughputCollector.cs +++ b/src/Particular.LicensingComponent/ThroughputCollector.cs @@ -119,6 +119,7 @@ public async Task GenerateThroughputReport(string spVersion, DateT var queueThroughputs = new List(); List ignoredQueueNames = []; + var dailyThroughputTotals = new Dictionary(); await foreach (var endpointData in GetDistinctEndpointData(cancellationToken)) { @@ -140,6 +141,11 @@ public async Task GenerateThroughputReport(string spVersion, DateT }; queueThroughputs.Add(queueThroughput); + + foreach (var (date, messageCount) in endpointData.ThroughputData.DailyThroughput()) + { + dailyThroughputTotals[date] = dailyThroughputTotals.GetValueOrDefault(date) + messageCount; + } } var auditServiceMetadata = await dataStore.GetAuditServiceMetadata(cancellationToken); @@ -161,7 +167,8 @@ public async Task GenerateThroughputReport(string spVersion, DateT IgnoredQueues = [.. ignoredQueueNames], Queues = [.. queueThroughputs], TotalQueues = queueThroughputs.Count, - TotalThroughput = queueThroughputs.Sum(q => q.Throughput ?? 0), + //the maximum number of messages processed across all endpoints in a single day + TotalThroughput = dailyThroughputTotals.Count > 0 ? dailyThroughputTotals.Values.Max() : 0, EnvironmentInformation = new EnvironmentInformation { AuditServicesData = new AuditServicesData(auditServiceMetadata.Versions, auditServiceMetadata.Transports), EnvironmentData = brokerMetaData.Data } }; diff --git a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs index 6d54edb6c9..cc0aced7ee 100644 --- a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs +++ b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs @@ -23,6 +23,13 @@ public static long MaxDailyThroughput(this List throughputs) return 0; } + // Daily throughput for an endpoint. When multiple sources have data for the same day, + // the maximum value is used, since all sources measure the same messages. + public static IEnumerable DailyThroughput(this List throughputs) => throughputs + .SelectMany(td => td) + .GroupBy(kvp => kvp.Key) + .Select(group => new EndpointDailyThroughput(group.Key, group.Max(kvp => kvp.Value))); + public static MonthlyThroughput[] MonthlyThroughput(this List throughputs) => [.. throughputs .SelectMany(data => data) .GroupBy(kvp => $"{kvp.Key:yyyy-MM}")