From 0bb11ea8bd6db4204cdfacf1c73de0bf55e30f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 22 Apr 2026 13:35:34 +0200 Subject: [PATCH 01/11] Make endpoint parsing code recillient to missing host information --- src/Directory.Packages.props | 14 +++++++------- .../Operations/EndpointDetailsParser.cs | 12 +++++++----- .../ExceptionTypeAndStackTraceFailureClassifier.cs | 6 ++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index ec2eba2ceb..88a9eb2e39 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -44,7 +44,7 @@ - + @@ -57,12 +57,12 @@ - - - + + + - + @@ -80,8 +80,8 @@ - - + + diff --git a/src/ServiceControl/Operations/EndpointDetailsParser.cs b/src/ServiceControl/Operations/EndpointDetailsParser.cs index c2edcef186..ab34edda08 100644 --- a/src/ServiceControl/Operations/EndpointDetailsParser.cs +++ b/src/ServiceControl/Operations/EndpointDetailsParser.cs @@ -75,14 +75,16 @@ public static EndpointDetails ReceivingEndpoint(IReadOnlyDictionary new StackFrame @@ -54,10 +55,7 @@ public string ClassifyFailure(ClassifiableMessageDetails failure) return GetNonStandardClassification(exception.ExceptionType); } - static string GetNonStandardClassification(string exceptionType) - { - return exceptionType + ": 0"; - } + static string GetNonStandardClassification(string exceptionType) => exceptionType + ": No stacktrace available"; public const string Id = "Exception Type and Stack Trace"; From 33df94f972cb8a0800ba116c2ddb2297bc6916dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 23 Apr 2026 14:20:16 +0200 Subject: [PATCH 02/11] Add unit tests --- .../Operations/EndpointDetailsParserTests.cs | 49 +++++++++++++++++++ ...nTypeAndStackTraceFailureClassifierTest.cs | 16 +++--- .../Operations/EndpointDetailsParser.cs | 5 +- 3 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs diff --git a/src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs b/src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs new file mode 100644 index 0000000000..ff56217174 --- /dev/null +++ b/src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs @@ -0,0 +1,49 @@ +namespace ServiceControl.UnitTests.Operations; + +using System.Collections.Generic; +using NServiceBus.Faults; +using NUnit.Framework; +using ServiceControl.Contracts.Operations; +using ServiceControl.Infrastructure; + +[TestFixture] +public class EndpointDetailsParserTests +{ + [Test] + public void Receiving_endpoint_should_use_failed_queue_machine_when_host_is_missing() + { + var headers = new Dictionary + { + { FaultsHeaderKeys.FailedQ, "Sales@backend-01" } + }; + + var endpoint = EndpointDetailsParser.ReceivingEndpoint(headers); + + Assert.Multiple(() => + { + Assert.That(endpoint, Is.Not.Null); + Assert.That(endpoint.Name, Is.EqualTo("Sales")); + Assert.That(endpoint.Host, Is.EqualTo("backend-01")); + Assert.That(endpoint.HostId, Is.EqualTo(DeterministicGuid.MakeId("Sales", "backend-01"))); + }); + } + + [Test] + public void Receiving_endpoint_should_use_unknown_host_when_failed_queue_is_used_to_infer_endpoint_name() + { + var headers = new Dictionary + { + { FaultsHeaderKeys.FailedQ, "Billing" } + }; + + var endpoint = EndpointDetailsParser.ReceivingEndpoint(headers); + + Assert.Multiple(() => + { + Assert.That(endpoint, Is.Not.Null); + Assert.That(endpoint.Name, Is.EqualTo("Billing")); + Assert.That(endpoint.Host, Is.EqualTo("unknown")); + Assert.That(endpoint.HostId, Is.EqualTo(DeterministicGuid.MakeId("Billing", "unknown"))); + }); + } +} \ No newline at end of file diff --git a/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs b/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs index d669039557..93b182ff41 100644 --- a/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs +++ b/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs @@ -7,6 +7,8 @@ [TestFixture] public class ExceptionTypeAndStackTraceFailureClassifierTest { + const string noStackTraceClassification = "exceptionType: No stacktrace available"; + [Test] public void Failure_Without_ExceptionDetails_should_not_group() { @@ -23,7 +25,7 @@ public void Empty_stack_trace_should_group_by_exception_type() var failureWithEmptyStackTrace = CreateFailureDetailsWithStackTrace(string.Empty); var classification = classifier.ClassifyFailure(failureWithEmptyStackTrace); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -33,7 +35,7 @@ public void Null_stack_trace_should_group_by_exception_type() var failureWithNullStackTrace = CreateFailureDetailsWithStackTrace(null); var classification = classifier.ClassifyFailure(failureWithNullStackTrace); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -43,7 +45,7 @@ public void Non_standard_stack_trace_format_should_group_by_exception_type() var failureWithNonStandardStackTrace = CreateFailureDetailsWithStackTrace("something other than a normal stack trace"); var classification = classifier.ClassifyFailure(failureWithNonStandardStackTrace); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -53,7 +55,7 @@ public void Null_message_should_group_by_exception_type() var failureWithNullMessage = CreateFailureDetailsWithMessage(null); var classification = classifier.ClassifyFailure(failureWithNullMessage); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -63,7 +65,7 @@ public void Empty_message_should_group_by_exception_type() var failureWithEmptyMessage = CreateFailureDetailsWithMessage(string.Empty); var classification = classifier.ClassifyFailure(failureWithEmptyMessage); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -73,7 +75,7 @@ public void Whitespace_message_should_group_by_exception_type() var failureWithWhitespaceMessage = CreateFailureDetailsWithMessage(" "); var classification = classifier.ClassifyFailure(failureWithWhitespaceMessage); - Assert.That(classification, Is.EqualTo("exceptionType: 0")); + Assert.That(classification, Is.EqualTo(noStackTraceClassification)); } [Test] @@ -154,4 +156,4 @@ static ClassifiableMessageDetails CreateFailureDetailsWithMessage(string message return new ClassifiableMessageDetails(null, failure, null); } } -} \ No newline at end of file +} diff --git a/src/ServiceControl/Operations/EndpointDetailsParser.cs b/src/ServiceControl/Operations/EndpointDetailsParser.cs index ab34edda08..7801a0fe1e 100644 --- a/src/ServiceControl/Operations/EndpointDetailsParser.cs +++ b/src/ServiceControl/Operations/EndpointDetailsParser.cs @@ -75,7 +75,10 @@ public static EndpointDetails ReceivingEndpoint(IReadOnlyDictionary Date: Sun, 26 Apr 2026 17:42:18 +0200 Subject: [PATCH 03/11] latest alpha --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 88a9eb2e39..33741020f0 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -94,4 +94,4 @@ - + \ No newline at end of file From 0ad573ab6c33e2c6357962d42d3064475680092b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 4 May 2026 19:45:00 +0200 Subject: [PATCH 04/11] Fixup --- src/Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 33741020f0..6c01911b45 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -44,7 +44,7 @@ - + @@ -57,12 +57,12 @@ - - - + + + - + @@ -80,8 +80,8 @@ - - + + From 50791221b5129eb221661e2e7f4dc7498e5ba121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 08:22:43 +0200 Subject: [PATCH 05/11] Adjust test for new behavior --- ...en_ingesting_failed_message_with_missing_headers.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs index f0d933885c..cb0b280106 100644 --- a/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs +++ b/src/ServiceControl.AcceptanceTests/Recoverability/MessageFailures/When_ingesting_failed_message_with_missing_headers.cs @@ -34,8 +34,8 @@ public async Task Should_be_ingested_when_minimal_required_headers_is_present() //No failure time will result in utc now being used Assert.That(failure.TimeOfFailure, Is.GreaterThan(testStartTime)); - // Both host and endpoint name is currently needed so this will be null since no host can be detected from the failed q header - Assert.That(failure.ReceivingEndpoint, Is.Null); + Assert.That(failure.ReceivingEndpoint, Is.Not.Null); + Assert.That(failure.ReceivingEndpoint.Name, Is.EqualTo(context.EndpointNameOfReceivingEndpoint)); } [Test] @@ -45,10 +45,6 @@ public async Task Should_include_headers_required_by_ServicePulse() { c.AddMinimalRequiredHeaders(); - // This is needed for ServiceControl to be able to detect both endpoint (via failed q header) and host via the processing machine header - // Missing endpoint or host will cause a null ref in ServicePulse - c.Headers[Headers.ProcessingMachine] = "MyMachine"; - c.Headers[FaultsHeaderKeys.ExceptionType] = "SomeExceptionType"; c.Headers[FaultsHeaderKeys.Message] = "Some message"; }) @@ -62,8 +58,6 @@ public async Task Should_include_headers_required_by_ServicePulse() // ServicePulse assumes that the receiving endpoint name is present Assert.That(failure.ReceivingEndpoint, Is.Not.Null); - Assert.That(failure.ReceivingEndpoint.Name, Is.EqualTo(context.EndpointNameOfReceivingEndpoint)); - Assert.That(failure.ReceivingEndpoint.Host, Is.EqualTo("MyMachine")); // ServicePulse needs both an exception type and description to render the UI in a resonable way Assert.That(failure.Exception.ExceptionType, Is.EqualTo("SomeExceptionType")); From 4df6e9e93a40cda9caa45853b5edd30834c47a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 08:31:22 +0200 Subject: [PATCH 06/11] Make sure additional queues to create are not duplicated --- .../TransportCustomization.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Transports/TransportCustomization.cs b/src/ServiceControl.Transports/TransportCustomization.cs index 500a9c5846..ba1245aad6 100644 --- a/src/ServiceControl.Transports/TransportCustomization.cs +++ b/src/ServiceControl.Transports/TransportCustomization.cs @@ -138,15 +138,22 @@ public virtual async Task ProvisionQueues(TransportSettings transportSettings, I true, null); //null means "not hosted by core", transport SHOULD adjust accordingly to not assume things - var receivers = new[]{ + var receivers = new[] + { new ReceiveSettings( transportSettings.EndpointName, new QueueAddress(transportSettings.EndpointName), false, false, - transportSettings.ErrorQueue)}; + transportSettings.ErrorQueue) + }; + + var additionalQueuesToProvision = additionalQueues + .Distinct() + .Union([transportSettings.ErrorQueue]) + .Select(ToTransportQualifiedQueueNameCore).ToArray(); - var transportInfrastructure = await transport.Initialize(hostSettings, receivers, additionalQueues.Union([transportSettings.ErrorQueue]).Select(ToTransportQualifiedQueueNameCore).ToArray()); + var transportInfrastructure = await transport.Initialize(hostSettings, receivers, additionalQueuesToProvision); await transportInfrastructure.Shutdown(); } From ff88de98d6dae1681538d23b46bc817c41313664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 09:49:50 +0200 Subject: [PATCH 07/11] Difference casing can still produce duplicates --- src/ServiceControl.Transports/TransportCustomization.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.Transports/TransportCustomization.cs b/src/ServiceControl.Transports/TransportCustomization.cs index ba1245aad6..563030555f 100644 --- a/src/ServiceControl.Transports/TransportCustomization.cs +++ b/src/ServiceControl.Transports/TransportCustomization.cs @@ -149,9 +149,10 @@ public virtual async Task ProvisionQueues(TransportSettings transportSettings, I }; var additionalQueuesToProvision = additionalQueues - .Distinct() .Union([transportSettings.ErrorQueue]) - .Select(ToTransportQualifiedQueueNameCore).ToArray(); + .Select(ToTransportQualifiedQueueNameCore) + .Distinct() + .ToArray(); var transportInfrastructure = await transport.Initialize(hostSettings, receivers, additionalQueuesToProvision); await transportInfrastructure.Shutdown(); From b2e7f69317420d874fce6d17673eb490a2063c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 11:03:04 +0200 Subject: [PATCH 08/11] Make sure receive queue is not added to the additional queues --- src/ServiceControl.Transports/TransportCustomization.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.Transports/TransportCustomization.cs b/src/ServiceControl.Transports/TransportCustomization.cs index 563030555f..720d00ff5f 100644 --- a/src/ServiceControl.Transports/TransportCustomization.cs +++ b/src/ServiceControl.Transports/TransportCustomization.cs @@ -138,17 +138,18 @@ public virtual async Task ProvisionQueues(TransportSettings transportSettings, I true, null); //null means "not hosted by core", transport SHOULD adjust accordingly to not assume things + var receiveQueueName = transportSettings.EndpointName; var receivers = new[] { new ReceiveSettings( transportSettings.EndpointName, - new QueueAddress(transportSettings.EndpointName), + new QueueAddress(receiveQueueName), false, false, transportSettings.ErrorQueue) }; - var additionalQueuesToProvision = additionalQueues + var additionalQueuesToProvision = additionalQueues.Where(queueName => queueName != receiveQueueName) .Union([transportSettings.ErrorQueue]) .Select(ToTransportQualifiedQueueNameCore) .Distinct() From a7008a0342474b77ed20a278eb90c4f4dd0e6b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 13:43:02 +0200 Subject: [PATCH 09/11] Update NServiceBus.Transport.AzureServiceBus version --- src/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6c01911b45..571507d059 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -47,7 +47,7 @@ - + @@ -94,4 +94,4 @@ - \ No newline at end of file + From 2365ac4773347c998692bd20257b72e0715fb11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 15:51:44 +0200 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Daniel Marbach --- .../ExceptionTypeAndStackTraceFailureClassifierTest.cs | 2 +- .../Groupers/ExceptionTypeAndStackTraceFailureClassifier.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs b/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs index 93b182ff41..ebea27ce41 100644 --- a/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs +++ b/src/ServiceControl.UnitTests/Recoverability/ExceptionTypeAndStackTraceFailureClassifierTest.cs @@ -7,7 +7,7 @@ [TestFixture] public class ExceptionTypeAndStackTraceFailureClassifierTest { - const string noStackTraceClassification = "exceptionType: No stacktrace available"; + const string noStackTraceClassification = "exceptionType: No stacktrace"; [Test] public void Failure_Without_ExceptionDetails_should_not_group() diff --git a/src/ServiceControl/Recoverability/Grouping/Groupers/ExceptionTypeAndStackTraceFailureClassifier.cs b/src/ServiceControl/Recoverability/Grouping/Groupers/ExceptionTypeAndStackTraceFailureClassifier.cs index 0f188cd44d..3f18175eb2 100644 --- a/src/ServiceControl/Recoverability/Grouping/Groupers/ExceptionTypeAndStackTraceFailureClassifier.cs +++ b/src/ServiceControl/Recoverability/Grouping/Groupers/ExceptionTypeAndStackTraceFailureClassifier.cs @@ -55,7 +55,7 @@ public string ClassifyFailure(ClassifiableMessageDetails failure) return GetNonStandardClassification(exception.ExceptionType); } - static string GetNonStandardClassification(string exceptionType) => exceptionType + ": No stacktrace available"; + static string GetNonStandardClassification(string exceptionType) => exceptionType + ": No stacktrace"; public const string Id = "Exception Type and Stack Trace"; From 75c9d3a044b911fcecf146183596665f20b71eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 6 May 2026 16:58:16 +0200 Subject: [PATCH 11/11] Better test name --- ...tailsParserTests.cs => When_parsing_receive_endpoint.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/ServiceControl.UnitTests/Operations/{EndpointDetailsParserTests.cs => When_parsing_receive_endpoint.cs} (84%) diff --git a/src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs b/src/ServiceControl.UnitTests/Operations/When_parsing_receive_endpoint.cs similarity index 84% rename from src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs rename to src/ServiceControl.UnitTests/Operations/When_parsing_receive_endpoint.cs index ff56217174..e8e78a3218 100644 --- a/src/ServiceControl.UnitTests/Operations/EndpointDetailsParserTests.cs +++ b/src/ServiceControl.UnitTests/Operations/When_parsing_receive_endpoint.cs @@ -7,10 +7,10 @@ namespace ServiceControl.UnitTests.Operations; using ServiceControl.Infrastructure; [TestFixture] -public class EndpointDetailsParserTests +public class When_parsing_receive_endpoint { [Test] - public void Receiving_endpoint_should_use_failed_queue_machine_when_host_is_missing() + public void Should_infer_host_from_machine_name_in_failed_queue_when_host_header_is_missing() { var headers = new Dictionary { @@ -29,7 +29,7 @@ public void Receiving_endpoint_should_use_failed_queue_machine_when_host_is_miss } [Test] - public void Receiving_endpoint_should_use_unknown_host_when_failed_queue_is_used_to_infer_endpoint_name() + public void Should_fallback_to_unknown_if_host_can_not_be_determined() { var headers = new Dictionary {