From b6ff7abbedd76f6ccc484adbfd682b3e6456d598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 22 Aug 2023 11:31:03 +0200 Subject: [PATCH 01/18] Bump to core 9 --- .github/workflows/ci.yml | 13 +-- .github/workflows/release.yml | 5 +- .../AcceptanceTesting.csproj | 6 +- .../AcceptanceTests.ASB.csproj | 6 +- .../AcceptanceTests.ASQ.csproj | 6 +- .../AcceptanceTests.Learning.csproj | 6 +- .../AcceptanceTests.Msmq.csproj | 8 +- .../ConfigureMsmqTransportTestExecution.cs | 110 +++++++++--------- .../TestableMsmqTransport.cs | 40 +++---- .../AcceptanceTests.RabbitMQ.csproj | 6 +- .../AcceptanceTests.SQS.csproj | 6 +- .../AcceptanceTests.SqlServer.csproj | 6 +- src/AcceptanceTests/AcceptanceTests.csproj | 7 +- .../Shared/Request_reply_custom_address.cs | 35 ++++-- .../Shared/Support/BridgeAcceptanceTest.cs | 8 -- src/Custom.Build.props | 2 +- src/NServiceBus.MessagingBridge.sln | 2 + .../Configuration/BridgeConfiguration.cs | 7 +- .../Configuration/HostBuilderExtensions.cs | 3 +- .../EndpointProxyFactory.cs | 11 +- .../NServiceBus.MessagingBridge.csproj | 4 +- src/UnitTests/BridgeConfigurationTests.cs | 11 +- src/UnitTests/UnitTests.csproj | 5 +- 23 files changed, 164 insertions(+), 149 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3478dd6..aa533566 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ windows-2019, ubuntu-20.04 ] + os: [ windows-2022, ubuntu-22.04 ] transport: [ Learning, RabbitMQ, AzureServiceBus, AzureStorageQueue, SqlServer, AmazonSQS, MSMQ ] include: - - os: windows-2019 + - os: windows-2022 os-name: Windows - - os: ubuntu-20.04 + - os: ubuntu-22.04 os-name: Linux exclude: - - os: ubuntu-20.04 + - os: ubuntu-22.04 transport: MSMQ fail-fast: false steps: @@ -38,9 +38,8 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: | - 7.0.x - 6.0.x + dotnet-version: 8.0.x + dotnet-quality: 'preview' - name: Build run: dotnet build src --configuration Release - name: Upload packages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8189a090..e73e9cfe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ env: DOTNET_NOLOGO: true jobs: release: - runs-on: windows-2019 # Windows required for ILMerge to work in ScriptBuilderTask + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4.0.0 @@ -17,7 +17,8 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v3.2.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x + dotnet-quality: 'preview' - name: Build run: dotnet build src --configuration Release - name: Sign NuGet packages diff --git a/src/AcceptanceTesting/AcceptanceTesting.csproj b/src/AcceptanceTesting/AcceptanceTesting.csproj index a3b7be8a..250e83db 100644 --- a/src/AcceptanceTesting/AcceptanceTesting.csproj +++ b/src/AcceptanceTesting/AcceptanceTesting.csproj @@ -1,14 +1,14 @@ - net472;net6.0;net7.0 + net8.0 true ..\NServiceBusTests.snk - - + + diff --git a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj index 63760ed5..6239ebd2 100644 --- a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj +++ b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj @@ -1,17 +1,17 @@ - net6.0;net7.0 + net8.0 - + - + diff --git a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj index 9d7d39e5..f17c340e 100644 --- a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj +++ b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj @@ -1,16 +1,16 @@ - net6.0;net7.0 + net8.0 - + - + diff --git a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj index 074709c4..26d92cfc 100644 --- a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj +++ b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj @@ -1,11 +1,11 @@ - net6.0;net7.0 + net8.0 - + @@ -19,7 +19,7 @@ - + diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index a9cb8183..817bdaf1 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -1,12 +1,12 @@ - net472 + net8.0 - - + + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs index ec16704d..f089648e 100644 --- a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs +++ b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs @@ -1,81 +1,83 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Messaging; +//using System.Collections.Generic; +//using System.Linq; +//using System.Messaging; using System.Threading; using System.Threading.Tasks; using NServiceBus; using NServiceBus.AcceptanceTesting.Support; +//using NServiceBus.Transport; class ConfigureMsmqTransportTestExecution : IConfigureTransportTestExecution { public BridgeTransportDefinition GetBridgeTransport() { - var transportDefinition = new TestableMsmqTransport(); + //var transportDefinition = new TestableMsmqTransport(); return new BridgeTransportDefinition { - TransportDefinition = transportDefinition, - Cleanup = (ct) => Cleanup(transportDefinition, ct) + TransportDefinition = null,//transportDefinition, + //Cleanup = (ct) => Cleanup(transportDefinition, ct) }; } public Func ConfigureTransportForEndpoint(EndpointConfiguration endpointConfiguration, PublisherMetadata publisherMetadata) { - var transportDefinition = new TestableMsmqTransport(); - var routingConfig = endpointConfiguration.UseTransport(transportDefinition); - endpointConfiguration.UsePersistence(); + //var transportDefinition = new TestableMsmqTransport(); + //var routingConfig = endpointConfiguration.UseTransport(transportDefinition); + //endpointConfiguration.UsePersistence(); - foreach (var publisher in publisherMetadata.Publishers) - { - foreach (var eventType in publisher.Events) - { - routingConfig.RegisterPublisher(eventType, publisher.PublisherName); - } - } + //foreach (var publisher in publisherMetadata.Publishers) + //{ + // foreach (var eventType in publisher.Events) + // { + // routingConfig.RegisterPublisher(eventType, publisher.PublisherName); + // } + //} - return (ct) => Cleanup(transportDefinition, ct); + //return (ct) => Cleanup(transportDefinition, ct); + return (_) => Task.CompletedTask; } - static Task Cleanup(TestableMsmqTransport msmqTransport, CancellationToken cancellationToken) - { - var allQueues = MessageQueue.GetPrivateQueuesByMachine("localhost"); - var queuesToBeDeleted = new List(); + //static Task Cleanup(TestableMsmqTransport msmqTransport, CancellationToken cancellationToken) + //{ + // var allQueues = MessageQueue.GetPrivateQueuesByMachine("localhost"); + // var queuesToBeDeleted = new List(); - foreach (var messageQueue in allQueues) - { - using (messageQueue) - { - if (msmqTransport.ReceiveQueues.Any(ra => - { - var indexOfAt = ra.IndexOf("@", StringComparison.Ordinal); - if (indexOfAt >= 0) - { - ra = ra.Substring(0, indexOfAt); - } - return messageQueue.QueueName.StartsWith(@"private$\" + ra, StringComparison.OrdinalIgnoreCase); - })) - { - queuesToBeDeleted.Add(messageQueue.Path); - } - } - } + // foreach (var messageQueue in allQueues) + // { + // using (messageQueue) + // { + // if (msmqTransport.ReceiveQueues.Any(ra => + // { + // var indexOfAt = ra.IndexOf("@", StringComparison.Ordinal); + // if (indexOfAt >= 0) + // { + // ra = ra.Substring(0, indexOfAt); + // } + // return messageQueue.QueueName.StartsWith(@"private$\" + ra, StringComparison.OrdinalIgnoreCase); + // })) + // { + // queuesToBeDeleted.Add(messageQueue.Path); + // } + // } + // } - foreach (var queuePath in queuesToBeDeleted) - { - try - { - MessageQueue.Delete(queuePath); - Console.WriteLine("Deleted '{0}' queue", queuePath); - } - catch (Exception) - { - Console.WriteLine("Could not delete queue '{0}'", queuePath); - } - } + // foreach (var queuePath in queuesToBeDeleted) + // { + // try + // { + // MessageQueue.Delete(queuePath); + // Console.WriteLine("Deleted '{0}' queue", queuePath); + // } + // catch (Exception) + // { + // Console.WriteLine("Could not delete queue '{0}'", queuePath); + // } + // } - MessageQueue.ClearConnectionCache(); + // MessageQueue.ClearConnectionCache(); - return Task.CompletedTask; - } + // return Task.CompletedTask; + //} } \ No newline at end of file diff --git a/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs b/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs index bb09c22a..45085776 100644 --- a/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs +++ b/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs @@ -1,24 +1,24 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using NServiceBus; -using NServiceBus.Transport; +//using System; +//using System.Linq; +//using System.Threading; +//using System.Threading.Tasks; +//using NServiceBus; +//using NServiceBus.Transport; -/// -/// A dedicated subclass of the MsmqTransport that enables us to intercept the receive queues for the test. -/// -class TestableMsmqTransport : MsmqTransport -{ - public string[] ReceiveQueues = new string[0]; +///// +///// A dedicated subclass of the MsmqTransport that enables us to intercept the receive queues for the test. +///// +//class TestableMsmqTransport : MsmqTransport +//{ +// public string[] ReceiveQueues = new string[0]; - public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) - { - MessageEnumeratorTimeout = TimeSpan.FromMilliseconds(10); +// public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) +// { +// MessageEnumeratorTimeout = TimeSpan.FromMilliseconds(10); - var infrastructure = await base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken).ConfigureAwait(false); - ReceiveQueues = infrastructure.Receivers.Select(r => r.Value.ReceiveAddress).ToArray(); +// var infrastructure = await base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken).ConfigureAwait(false); +// ReceiveQueues = infrastructure.Receivers.Select(r => r.Value.ReceiveAddress).ToArray(); - return infrastructure; - } -} \ No newline at end of file +// return infrastructure; +// } +//} \ No newline at end of file diff --git a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj index aaa1ab48..9bc96727 100644 --- a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj +++ b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj @@ -1,11 +1,11 @@ - net6.0;net7.0 + net8.0 - + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj index 712247dd..e4b6e038 100644 --- a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj +++ b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj @@ -1,17 +1,17 @@ - net6.0;net7.0 + net8.0 - + - + diff --git a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj index 38b0fe95..87e3e377 100644 --- a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj +++ b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj @@ -1,11 +1,11 @@ - net6.0;net7.0 + net8.0 - + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests/AcceptanceTests.csproj b/src/AcceptanceTests/AcceptanceTests.csproj index 730099d7..c1174281 100644 --- a/src/AcceptanceTests/AcceptanceTests.csproj +++ b/src/AcceptanceTests/AcceptanceTests.csproj @@ -1,12 +1,11 @@ - net472;net6.0;net7.0 + net8.0 - - + @@ -16,7 +15,7 @@ - + diff --git a/src/AcceptanceTests/Shared/Request_reply_custom_address.cs b/src/AcceptanceTests/Shared/Request_reply_custom_address.cs index 83356fe8..b8b2fef5 100644 --- a/src/AcceptanceTests/Shared/Request_reply_custom_address.cs +++ b/src/AcceptanceTests/Shared/Request_reply_custom_address.cs @@ -2,6 +2,7 @@ using NServiceBus; using NServiceBus.AcceptanceTesting; using NServiceBus.AcceptanceTesting.Customization; +using NServiceBus.Transport; using NUnit.Framework; public class Request_reply_custom_address : BridgeAcceptanceTest @@ -11,15 +12,7 @@ public async Task Should_get_the_reply() { var ctx = await Scenario.Define() .WithEndpoint(c => c - .When(cc => cc.EndpointsStarted, (b, _) => - { - var sendOptions = new SendOptions(); - var endpointAddress = GetTestEndpointAddress(); - - sendOptions.RouteReplyTo(endpointAddress); - - return b.Send(new MyMessage(), sendOptions); - })) + .When(cc => cc.EndpointsStarted, (b, _) => b.SendLocal(new StartMessage()))) .WithEndpoint() .WithEndpoint() .WithBridge(bridgeConfiguration => @@ -53,6 +46,26 @@ public SendingEndpoint() c.ConfigureRouting().RouteToEndpoint(typeof(MyMessage), typeof(ReplyingEndpoint)); }); } + + public class ResponseHandler : IHandleMessages + { + readonly ITransportAddressResolver transportAddressResolver; + + public ResponseHandler(ITransportAddressResolver transportAddressResolver) + { + this.transportAddressResolver = transportAddressResolver; + } + + public Task Handle(StartMessage message, IMessageHandlerContext context) + { + var sendOptions = new SendOptions(); + var endpointName = NServiceBus.AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(ReplyReceivingEndpoint)); + var endpointAddress = transportAddressResolver.ToTransportAddress(new QueueAddress(endpointName)); + sendOptions.RouteReplyTo(endpointAddress); + + return context.Send(new MyMessage(), sendOptions); + } + } } public class ReplyReceivingEndpoint : EndpointConfigurationBuilder @@ -95,6 +108,10 @@ public Task Handle(MyMessage message, IMessageHandlerContext context) } } + public class StartMessage : IMessage + { + } + public class MyMessage : IMessage { } diff --git a/src/AcceptanceTests/Shared/Support/BridgeAcceptanceTest.cs b/src/AcceptanceTests/Shared/Support/BridgeAcceptanceTest.cs index e856f90b..dc56117d 100644 --- a/src/AcceptanceTests/Shared/Support/BridgeAcceptanceTest.cs +++ b/src/AcceptanceTests/Shared/Support/BridgeAcceptanceTest.cs @@ -41,14 +41,6 @@ public Task TearDown() return bridgeTransportDefinition.Cleanup(CancellationToken.None); } - protected string GetTestEndpointAddress() where T : EndpointConfigurationBuilder - { - var endpointName = NServiceBus.AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(T)); -#pragma warning disable CS0618 // Type or member is obsolete - return bridgeTransportDefinition.TransportDefinition.ToTransportAddress(new QueueAddress(endpointName)); -#pragma warning restore CS0618 // Type or member is obsolete - } - protected TransportDefinition TransportBeingTested => bridgeTransportDefinition.TransportDefinition; protected TransportDefinition TestTransport; diff --git a/src/Custom.Build.props b/src/Custom.Build.props index 36840bc2..58889225 100644 --- a/src/Custom.Build.props +++ b/src/Custom.Build.props @@ -1,7 +1,7 @@ - 0.1 + 1.0 minor diff --git a/src/NServiceBus.MessagingBridge.sln b/src/NServiceBus.MessagingBridge.sln index 77e7c8bf..3fdd87ac 100644 --- a/src/NServiceBus.MessagingBridge.sln +++ b/src/NServiceBus.MessagingBridge.sln @@ -27,8 +27,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AcceptanceTests.SQS", "Acce EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7C9AAE78-1698-46A0-89C8-DE78587C1B29}" ProjectSection(SolutionItems) = preProject + ..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml Custom.Build.props = Custom.Build.props Directory.Build.props = Directory.Build.props + ..\.github\workflows\release.yml = ..\.github\workflows\release.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AcceptanceTests.ASQ", "AcceptanceTests.ASQ\AcceptanceTests.ASQ.csproj", "{6E534115-6E4C-4F4D-AFFA-14FCC81CA7AE}" diff --git a/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs b/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs index daf98e8e..7b34ee7a 100644 --- a/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs +++ b/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs @@ -38,7 +38,7 @@ public void AddTransport(BridgeTransport transportConfiguration) /// public void DoNotEnforceBestPractices() => allowMultiplePublishersSameEvent = true; - internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger logger) + internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger logger, ITransportAddressResolver transportAddressResolver) { if (transportConfigurations.Count < 2) { @@ -169,10 +169,7 @@ internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger /// Extension methods to configure the bridge for the .NET Core generic host. @@ -43,7 +44,7 @@ public static IHostBuilder UseNServiceBusBridge( bridgeConfigurationAction(hostBuilderContext, bridgeConfiguration); - serviceCollection.AddSingleton(sp => bridgeConfiguration.FinalizeConfiguration(sp.GetRequiredService>())); + serviceCollection.AddSingleton(sp => bridgeConfiguration.FinalizeConfiguration(sp.GetRequiredService>(), sp.GetRequiredService())); serviceCollection.AddSingleton(deferredLoggerFactory); serviceCollection.AddSingleton(); diff --git a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs index a7dc070d..d8338364 100644 --- a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs +++ b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs @@ -10,7 +10,11 @@ class EndpointProxyFactory { - public EndpointProxyFactory(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider; + public EndpointProxyFactory(IServiceProvider serviceProvider, ITransportAddressResolver transportAddressResolver) + { + this.serviceProvider = serviceProvider; + this.transportAddressResolver = transportAddressResolver; + } public Task CreateProxy( BridgeEndpoint endpointToProxy, @@ -22,11 +26,9 @@ public Task CreateProxy( // NOTE: we have validation to make sure that TransportTransactionMode.TransactionScope is only used when all configured transports can support it var shouldPassTransportTransaction = transportDefinition.TransportTransactionMode == TransportTransactionMode.TransactionScope; -#pragma warning disable CS0618 // Type or member is obsolete // the transport seam assumes the error queue address to be a native address so we need to translate // unfortunately this method is obsoleted but we can't use the one on TransportInfrastructure since that is too late - var translatedErrorQueue = transportDefinition.ToTransportAddress(new QueueAddress(transportConfiguration.ErrorQueue)); -#pragma warning restore CS0618 // Type or member is obsolete + var translatedErrorQueue = transportAddressResolver.ToTransportAddress(new QueueAddress(transportConfiguration.ErrorQueue)); var transportEndpointConfiguration = RawEndpointConfiguration.Create( endpointToProxy.Name, @@ -77,4 +79,5 @@ static bool IsSubscriptionMessage(IReadOnlyDictionary messageCon } readonly IServiceProvider serviceProvider; + readonly ITransportAddressResolver transportAddressResolver; } \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj index 5b9873d6..8736dae0 100644 --- a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj +++ b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj @@ -1,14 +1,14 @@ - net472;net6.0 + net8.0 true ..\NServiceBus.snk NServiceBus.MessagingBridge - + diff --git a/src/UnitTests/BridgeConfigurationTests.cs b/src/UnitTests/BridgeConfigurationTests.cs index cdf2c8c3..ccb351b4 100644 --- a/src/UnitTests/BridgeConfigurationTests.cs +++ b/src/UnitTests/BridgeConfigurationTests.cs @@ -1,7 +1,5 @@ using System; using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using NServiceBus; using NServiceBus.Transport; using NUnit.Framework; @@ -314,7 +312,7 @@ public void Should_throw_if_users_tries_to_set_transaction_mode_on_individual_tr FinalizedBridgeConfiguration FinalizeConfiguration(BridgeConfiguration bridgeConfiguration) { - return bridgeConfiguration.FinalizeConfiguration(logger); + return bridgeConfiguration.FinalizeConfiguration(logger, new FakeTransportAddressResolver()); } class SomeScopeSupportingTransport : FakeTransport @@ -327,6 +325,11 @@ class SomeOtherScopeSupportingTransport : FakeTransport public SomeOtherScopeSupportingTransport() : base(TransportTransactionMode.TransactionScope) { } } + class FakeTransportAddressResolver : ITransportAddressResolver + { + public string ToTransportAddress(QueueAddress queueAddress) => queueAddress.ToString(); + } + class SomeTransport : FakeTransport { public Func AddressTranslation = name => name; @@ -334,7 +337,7 @@ class SomeTransport : FakeTransport public override string ToTransportAddress(QueueAddress address) #pragma warning restore CS0672 // Member overrides obsolete member { - return AddressTranslation(address.BaseAddress); + throw new NotImplementedException(); } } class SomeOtherTransport : FakeTransport { } diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index 95f3e576..9ff5dd25 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -1,10 +1,9 @@ - net472;net6.0;net7.0 + net8.0 true ..\NServiceBusTests.snk - 10 @@ -13,7 +12,7 @@ - + From 37de3bd518d28deca214dc0d29d4f94e091536ef Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:03:41 -0400 Subject: [PATCH 02/18] Update packages --- src/AcceptanceTesting/AcceptanceTesting.csproj | 4 ++-- src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj | 6 +++--- src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj | 8 ++++---- .../AcceptanceTests.Learning.csproj | 4 ++-- src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj | 4 ++-- .../AcceptanceTests.RabbitMQ.csproj | 6 +++--- src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj | 6 +++--- src/AcceptanceTests.SQS/SQSCleanup.cs | 3 ++- .../AcceptanceTests.SqlServer.csproj | 4 ++-- src/AcceptanceTests/AcceptanceTests.csproj | 4 ++-- .../NServiceBus.MessagingBridge.csproj | 4 ++-- src/UnitTests/UnitTests.csproj | 2 +- 12 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/AcceptanceTesting/AcceptanceTesting.csproj b/src/AcceptanceTesting/AcceptanceTesting.csproj index 250e83db..9c349bf6 100644 --- a/src/AcceptanceTesting/AcceptanceTesting.csproj +++ b/src/AcceptanceTesting/AcceptanceTesting.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj index 6239ebd2..53839c12 100644 --- a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj +++ b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj @@ -5,13 +5,13 @@ - - + + - + diff --git a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj index f17c340e..222fe8b0 100644 --- a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj +++ b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj @@ -5,14 +5,14 @@ - + - - - + + + diff --git a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj index 26d92cfc..ce42d0c6 100644 --- a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj +++ b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj @@ -5,7 +5,7 @@ - + @@ -19,7 +19,7 @@ - + diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index 817bdaf1..631d86b9 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -5,7 +5,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj index 9bc96727..5a0229e7 100644 --- a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj +++ b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj @@ -5,8 +5,8 @@ - - + + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj index e4b6e038..ff5af838 100644 --- a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj +++ b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj @@ -5,13 +5,13 @@ - - + + - + diff --git a/src/AcceptanceTests.SQS/SQSCleanup.cs b/src/AcceptanceTests.SQS/SQSCleanup.cs index 0a63e4d0..34b0ac1c 100644 --- a/src/AcceptanceTests.SQS/SQSCleanup.cs +++ b/src/AcceptanceTests.SQS/SQSCleanup.cs @@ -5,6 +5,7 @@ using Amazon.Runtime; using Amazon.S3; using Amazon.S3.Model; +using Amazon.S3.Util; using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; using Amazon.SQS; @@ -90,7 +91,7 @@ await Task.WhenAll(listBucketsResponse.Buckets.Where(x => x.BucketName.StartsWit { try { - if (!await s3Client.DoesS3BucketExistAsync(bucketName)) + if (!await AmazonS3Util.DoesS3BucketExistV2Async(s3Client, bucketName)) { return; } diff --git a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj index 87e3e377..a487b292 100644 --- a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj +++ b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj @@ -5,7 +5,7 @@ - + @@ -20,7 +20,7 @@ - + diff --git a/src/AcceptanceTests/AcceptanceTests.csproj b/src/AcceptanceTests/AcceptanceTests.csproj index c1174281..14296334 100644 --- a/src/AcceptanceTests/AcceptanceTests.csproj +++ b/src/AcceptanceTests/AcceptanceTests.csproj @@ -5,7 +5,7 @@ - + @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj index 8736dae0..9c0d4255 100644 --- a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj +++ b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index 9ff5dd25..163b06b2 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -12,7 +12,7 @@ - + From d8afa903b7acab3351960319800fca7125d15bf3 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:16:40 -0400 Subject: [PATCH 03/18] Clean up project files --- src/AcceptanceTesting/AcceptanceTesting.csproj | 12 +++--------- .../AcceptanceTests.ASB.csproj | 14 +++++++------- .../AcceptanceTests.ASQ.csproj | 18 +++++++++--------- .../AcceptanceTests.Learning.csproj | 12 ++++++------ .../AcceptanceTests.Msmq.csproj | 14 +++++++------- .../AcceptanceTests.RabbitMQ.csproj | 14 +++++++------- .../AcceptanceTests.SQS.csproj | 14 +++++++------- .../AcceptanceTests.SqlServer.csproj | 14 +++++++------- src/AcceptanceTests/AcceptanceTests.csproj | 8 ++++---- .../NServiceBus.MessagingBridge.csproj | 4 ++-- 10 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/AcceptanceTesting/AcceptanceTesting.csproj b/src/AcceptanceTesting/AcceptanceTesting.csproj index 9c349bf6..fd90036f 100644 --- a/src/AcceptanceTesting/AcceptanceTesting.csproj +++ b/src/AcceptanceTesting/AcceptanceTesting.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -6,19 +6,13 @@ ..\NServiceBusTests.snk - - - - - - - NServiceBusTests.snk - + + diff --git a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj index 53839c12..a1e25025 100644 --- a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj +++ b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj @@ -1,9 +1,14 @@ - + net8.0 + + + + + @@ -17,12 +22,7 @@ - - - - - - + diff --git a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj index 222fe8b0..171c15f5 100644 --- a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj +++ b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj @@ -1,29 +1,29 @@ - + net8.0 - + + - - + - - - - + + + + - + diff --git a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj index ce42d0c6..1d7e04e0 100644 --- a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj +++ b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj @@ -1,20 +1,16 @@ - + net8.0 - - - - - + @@ -24,4 +20,8 @@ + + + + diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index 631d86b9..d0b1d644 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -1,21 +1,17 @@ - + net8.0 - - - - - - + + @@ -25,4 +21,8 @@ + + + + diff --git a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj index 5a0229e7..d8f070a0 100644 --- a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj +++ b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj @@ -1,21 +1,17 @@ - + net8.0 - - - - - - + + @@ -25,4 +21,8 @@ + + + + diff --git a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj index ff5af838..7d2ce19c 100644 --- a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj +++ b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj @@ -1,9 +1,14 @@ - + net8.0 + + + + + @@ -17,12 +22,7 @@ - - - - - - + diff --git a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj index a487b292..795b96a6 100644 --- a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj +++ b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj @@ -1,21 +1,17 @@ - + net8.0 - - - - - - + + @@ -25,4 +21,8 @@ + + + + diff --git a/src/AcceptanceTests/AcceptanceTests.csproj b/src/AcceptanceTests/AcceptanceTests.csproj index 14296334..8a01930c 100644 --- a/src/AcceptanceTests/AcceptanceTests.csproj +++ b/src/AcceptanceTests/AcceptanceTests.csproj @@ -1,16 +1,16 @@ - + net8.0 - + + - - + diff --git a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj index 9c0d4255..83a45701 100644 --- a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj +++ b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj @@ -1,10 +1,9 @@ - + net8.0 true ..\NServiceBus.snk - NServiceBus.MessagingBridge @@ -20,4 +19,5 @@ + \ No newline at end of file From d02447cfd09f18194281c6d0e448287507b35158 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:17:08 -0400 Subject: [PATCH 04/18] Clean up workflows --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa533566..42a2d935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: shell: pwsh run: | echo "Create extra databases" - sqlcmd -Q "CREATE DATABASE nservicebus2" + sqlcmd -Q "CREATE DATABASE nservicebus2" - name: Setup Azure Service Bus if: matrix.transport == 'AzureServiceBus' uses: Particular/setup-azureservicebus-action@v1.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e73e9cfe..8685602e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout uses: actions/checkout@v4.0.0 with: - fetch-depth: 0 + fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v3.2.0 with: From 7fc732ddf10e5a64940aa63b2ec79d43e957f709 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:21:56 -0400 Subject: [PATCH 05/18] Remove unneeded package --- src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj index 171c15f5..afb7d28f 100644 --- a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj +++ b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj @@ -11,7 +11,6 @@ - From 268dc4351ce5ed4f626cb06bd16e4e981c652e1e Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:27:13 -0400 Subject: [PATCH 06/18] Remove ifdefs and pragmas --- src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs | 4 +--- src/NServiceBus.MessagingBridge/MessageShovel.cs | 6 +----- src/UnitTests/BridgeConfigurationTests.cs | 8 ++------ src/UnitTests/FakeTransport.cs | 3 --- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs index d8338364..35a869c8 100644 --- a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs +++ b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs @@ -73,9 +73,7 @@ static bool IsSubscriptionMessage(IReadOnlyDictionary messageCon Enum.TryParse(messageIntentString, true, out messageIntent); } -#pragma warning disable IDE0078 - return messageIntent == MessageIntent.Subscribe || messageIntent == MessageIntent.Unsubscribe; -#pragma warning restore IDE0078 + return messageIntent is MessageIntent.Subscribe or MessageIntent.Unsubscribe; } readonly IServiceProvider serviceProvider; diff --git a/src/NServiceBus.MessagingBridge/MessageShovel.cs b/src/NServiceBus.MessagingBridge/MessageShovel.cs index ddfc4f69..60c82e90 100644 --- a/src/NServiceBus.MessagingBridge/MessageShovel.cs +++ b/src/NServiceBus.MessagingBridge/MessageShovel.cs @@ -28,7 +28,6 @@ public async Task TransferMessage(TransferContext transferContext, CancellationT var messageToSend = new OutgoingMessage(messageContext.NativeMessageId, messageContext.Headers, messageContext.Body); messageToSend.Headers.Remove(BridgeHeaders.FailedQ); -#if NET var length = transferContext.SourceTransport.Length + targetEndpointDispatcher.TransportName.Length + 2 /* ->*/; var transferDetails = string.Create(length, (Source: transferContext.SourceTransport, Target: targetEndpointDispatcher.TransportName), @@ -41,9 +40,6 @@ public async Task TransferMessage(TransferContext transferContext, CancellationT chars[position++] = '>'; context.Target.AsSpan().CopyTo(chars.Slice(position)); }); -#else - var transferDetails = $"{transferContext.SourceTransport}->{targetEndpointDispatcher.TransportName}"; -#endif if (IsErrorMessage(messageToSend)) { @@ -55,7 +51,7 @@ public async Task TransferMessage(TransferContext transferContext, CancellationT } else if (IsAuditMessage(messageToSend)) { - //This is a message sent to the audit queue. We _do not_ transform any headers. + //This is a message sent to the audit queue. We _do not_ transform any headers. //This check needs to be done _before_ the retry message check because we don't want to treat audited retry messages as retry messages. } else if (IsRetryMessage(messageToSend)) diff --git a/src/UnitTests/BridgeConfigurationTests.cs b/src/UnitTests/BridgeConfigurationTests.cs index ccb351b4..b143400f 100644 --- a/src/UnitTests/BridgeConfigurationTests.cs +++ b/src/UnitTests/BridgeConfigurationTests.cs @@ -333,13 +333,9 @@ class FakeTransportAddressResolver : ITransportAddressResolver class SomeTransport : FakeTransport { public Func AddressTranslation = name => name; -#pragma warning disable CS0672 // Member overrides obsolete member - public override string ToTransportAddress(QueueAddress address) -#pragma warning restore CS0672 // Member overrides obsolete member - { - throw new NotImplementedException(); - } + } + class SomeOtherTransport : FakeTransport { } class MyEvent diff --git a/src/UnitTests/FakeTransport.cs b/src/UnitTests/FakeTransport.cs index da0e1347..87a80f22 100644 --- a/src/UnitTests/FakeTransport.cs +++ b/src/UnitTests/FakeTransport.cs @@ -22,9 +22,6 @@ public FakeTransport(TransportTransactionMode transportTransactionMode = Transpo public override IReadOnlyCollection GetSupportedTransactionModes() => supportedTransactionModes; public override Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) => throw new NotImplementedException(); -#pragma warning disable CS0672 // Member overrides obsolete member - public override string ToTransportAddress(QueueAddress address) => address.BaseAddress; -#pragma warning restore CS0672 // Member overrides obsolete member List supportedTransactionModes = new List(); } \ No newline at end of file From d7364425308e2aebbbf435911e26746510c2d852 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 15:29:05 -0400 Subject: [PATCH 07/18] Bump version number --- src/Custom.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Custom.Build.props b/src/Custom.Build.props index 58889225..57a8a06d 100644 --- a/src/Custom.Build.props +++ b/src/Custom.Build.props @@ -1,7 +1,7 @@ - 1.0 + 3.0 minor From a17147156878e23c117ea3fae117e91e457004f7 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 8 Sep 2023 16:14:52 -0400 Subject: [PATCH 08/18] Add stub MSMQ project --- .../AcceptanceTests.Msmq.csproj | 2 +- .../NServiceBus.MessagingBridge.Msmq.csproj | 15 +++++++++++++++ src/NServiceBus.MessagingBridge.sln | 6 ++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index d0b1d644..bc19dfdc 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -7,11 +7,11 @@ + - diff --git a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj new file mode 100644 index 00000000..a6a36c8c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + + + + + + + + + + + diff --git a/src/NServiceBus.MessagingBridge.sln b/src/NServiceBus.MessagingBridge.sln index 3fdd87ac..22f39a29 100644 --- a/src/NServiceBus.MessagingBridge.sln +++ b/src/NServiceBus.MessagingBridge.sln @@ -35,6 +35,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AcceptanceTests.ASQ", "AcceptanceTests.ASQ\AcceptanceTests.ASQ.csproj", "{6E534115-6E4C-4F4D-AFFA-14FCC81CA7AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NServiceBus.MessagingBridge.Msmq", "NServiceBus.MessagingBridge.Msmq\NServiceBus.MessagingBridge.Msmq.csproj", "{15DA1652-C18F-4A66-9487-500E8F5B3CE3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -85,6 +87,10 @@ Global {6E534115-6E4C-4F4D-AFFA-14FCC81CA7AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E534115-6E4C-4F4D-AFFA-14FCC81CA7AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E534115-6E4C-4F4D-AFFA-14FCC81CA7AE}.Release|Any CPU.Build.0 = Release|Any CPU + {15DA1652-C18F-4A66-9487-500E8F5B3CE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15DA1652-C18F-4A66-9487-500E8F5B3CE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15DA1652-C18F-4A66-9487-500E8F5B3CE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15DA1652-C18F-4A66-9487-500E8F5B3CE3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a7ddabc9f3069a27eaf3c2ee8f780b4c0f991352 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 15 Sep 2023 12:25:04 -0400 Subject: [PATCH 09/18] Use C# 12 collection expressions --- .../Configuration/BridgeTransport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.MessagingBridge/Configuration/BridgeTransport.cs b/src/NServiceBus.MessagingBridge/Configuration/BridgeTransport.cs index 3faad6ed..4cd74882 100644 --- a/src/NServiceBus.MessagingBridge/Configuration/BridgeTransport.cs +++ b/src/NServiceBus.MessagingBridge/Configuration/BridgeTransport.cs @@ -16,7 +16,7 @@ public BridgeTransport(TransportDefinition transportDefinition) { Guard.AgainstNull(nameof(transportDefinition), transportDefinition); - Endpoints = new List(); + Endpoints = []; TransportDefinition = transportDefinition; Name = transportDefinition.GetType().Name.ToLower().Replace("transport", ""); ErrorQueue = "bridge.error"; From 96ae35efb48b7b6d63833c95970e76b366745cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 25 Sep 2023 08:37:02 +0200 Subject: [PATCH 10/18] Update to core 6 and new sqlt --- src/AcceptanceTesting/AcceptanceTesting.csproj | 4 ++-- src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj | 2 +- src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj | 2 +- src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj | 2 +- src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj | 2 +- src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj | 2 +- src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj | 2 +- .../AcceptanceTests.SqlServer.csproj | 4 ++-- .../ConfigureSqlServerTransportTestExecution.cs | 4 ++-- src/AcceptanceTests/AcceptanceTests.csproj | 2 +- .../NServiceBus.MessagingBridge.Msmq.csproj | 2 +- .../NServiceBus.MessagingBridge.csproj | 4 ++-- 12 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/AcceptanceTesting/AcceptanceTesting.csproj b/src/AcceptanceTesting/AcceptanceTesting.csproj index fd90036f..d730210e 100644 --- a/src/AcceptanceTesting/AcceptanceTesting.csproj +++ b/src/AcceptanceTesting/AcceptanceTesting.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj index a1e25025..7fde12b1 100644 --- a/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj +++ b/src/AcceptanceTests.ASB/AcceptanceTests.ASB.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj index afb7d28f..f79888ec 100644 --- a/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj +++ b/src/AcceptanceTests.ASQ/AcceptanceTests.ASQ.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj index 1d7e04e0..0e1911b6 100644 --- a/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj +++ b/src/AcceptanceTests.Learning/AcceptanceTests.Learning.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index bc19dfdc..77d7b10c 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj index d8f070a0..51184ea1 100644 --- a/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj +++ b/src/AcceptanceTests.RabbitMQ/AcceptanceTests.RabbitMQ.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj index 7d2ce19c..61f4fe77 100644 --- a/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj +++ b/src/AcceptanceTests.SQS/AcceptanceTests.SQS.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj index 795b96a6..ec500e7f 100644 --- a/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj +++ b/src/AcceptanceTests.SqlServer/AcceptanceTests.SqlServer.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/src/AcceptanceTests.SqlServer/ConfigureSqlServerTransportTestExecution.cs b/src/AcceptanceTests.SqlServer/ConfigureSqlServerTransportTestExecution.cs index 1a0f57b9..77a32a91 100644 --- a/src/AcceptanceTests.SqlServer/ConfigureSqlServerTransportTestExecution.cs +++ b/src/AcceptanceTests.SqlServer/ConfigureSqlServerTransportTestExecution.cs @@ -9,7 +9,7 @@ class ConfigureSqlServerTransportTestExecution : IConfigureTransportTestExecution { - readonly string connectionString = Environment.GetEnvironmentVariable("SqlServerTransportConnectionString") ?? @"Data Source=.\SQLEXPRESS;Initial Catalog=nservicebus;Integrated Security=True"; + readonly string connectionString = Environment.GetEnvironmentVariable("SqlServerTransportConnectionString"); public BridgeTransportDefinition GetBridgeTransport() { @@ -44,7 +44,7 @@ Task Cleanup(TestableSqlServerTransport transport, CancellationToken cancellatio return connection; } - return transport.ConnectionFactory(CancellationToken.None).Result; + return transport.ConnectionFactory(cancellationToken).Result; }; using (var conn = factory()) diff --git a/src/AcceptanceTests/AcceptanceTests.csproj b/src/AcceptanceTests/AcceptanceTests.csproj index 8a01930c..d4976bc3 100644 --- a/src/AcceptanceTests/AcceptanceTests.csproj +++ b/src/AcceptanceTests/AcceptanceTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj index a6a36c8c..dae54bd3 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj +++ b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj index 83a45701..f757e42e 100644 --- a/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj +++ b/src/NServiceBus.MessagingBridge/NServiceBus.MessagingBridge.csproj @@ -7,8 +7,8 @@ - - + + From 96789eb64f46ebb4e72438e5dab6ecb5d1534a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 25 Sep 2023 12:38:30 +0200 Subject: [PATCH 11/18] Delay address translation --- .../Configuration/BridgeConfiguration.cs | 11 +-------- .../Configuration/BridgeEndpoint.cs | 10 ++++---- .../Configuration/HostBuilderExtensions.cs | 3 +-- .../EndpointProxyFactory.cs | 11 ++++----- .../EndpointRegistry.cs | 22 ++++++++++------- src/UnitTests/BridgeConfigurationTests.cs | 24 +++++-------------- src/UnitTests/MessageShovelTests.cs | 2 +- 7 files changed, 31 insertions(+), 52 deletions(-) diff --git a/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs b/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs index 7b34ee7a..2583efce 100644 --- a/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs +++ b/src/NServiceBus.MessagingBridge/Configuration/BridgeConfiguration.cs @@ -6,7 +6,6 @@ namespace NServiceBus using System.Linq; using System.Text; using Microsoft.Extensions.Logging; - using NServiceBus.Transport; /// /// Configuration options for bridging multiple transports @@ -38,7 +37,7 @@ public void AddTransport(BridgeTransport transportConfiguration) /// public void DoNotEnforceBestPractices() => allowMultiplePublishersSameEvent = true; - internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger logger, ITransportAddressResolver transportAddressResolver) + internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger logger) { if (transportConfigurations.Count < 2) { @@ -164,14 +163,6 @@ internal FinalizedBridgeConfiguration FinalizeConfiguration(ILogger /// Configuration options for a specific endpoint in the bridge @@ -11,11 +12,8 @@ public class BridgeEndpoint /// /// Initializes an endpoint in the bridge with the given name /// - public BridgeEndpoint(string name) + public BridgeEndpoint(string name) : this(name, name) { - Guard.AgainstNullAndEmpty(nameof(name), name); - - Name = name; } /// @@ -28,7 +26,7 @@ public BridgeEndpoint(string name, string queueAddress) Guard.AgainstNullAndEmpty(nameof(queueAddress), queueAddress); Name = name; - QueueAddress = queueAddress; + QueueAddress = new QueueAddress(queueAddress); } /// @@ -85,7 +83,7 @@ public void RegisterPublisher(string eventTypeAssemblyQualifiedName, string publ internal string Name { get; private set; } - internal string QueueAddress { get; set; } + internal QueueAddress QueueAddress { get; private set; } internal IList Subscriptions { get; } = new List(); diff --git a/src/NServiceBus.MessagingBridge/Configuration/HostBuilderExtensions.cs b/src/NServiceBus.MessagingBridge/Configuration/HostBuilderExtensions.cs index 08d4860a..6ae27334 100644 --- a/src/NServiceBus.MessagingBridge/Configuration/HostBuilderExtensions.cs +++ b/src/NServiceBus.MessagingBridge/Configuration/HostBuilderExtensions.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NServiceBus.Logging; - using NServiceBus.Transport; /// /// Extension methods to configure the bridge for the .NET Core generic host. @@ -44,7 +43,7 @@ public static IHostBuilder UseNServiceBusBridge( bridgeConfigurationAction(hostBuilderContext, bridgeConfiguration); - serviceCollection.AddSingleton(sp => bridgeConfiguration.FinalizeConfiguration(sp.GetRequiredService>(), sp.GetRequiredService())); + serviceCollection.AddSingleton(sp => bridgeConfiguration.FinalizeConfiguration(sp.GetRequiredService>())); serviceCollection.AddSingleton(deferredLoggerFactory); serviceCollection.AddSingleton(); diff --git a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs index 35a869c8..6c4f5a06 100644 --- a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs +++ b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs @@ -6,14 +6,12 @@ using Microsoft.Extensions.Logging; using NServiceBus; using NServiceBus.Raw; -using NServiceBus.Transport; class EndpointProxyFactory { - public EndpointProxyFactory(IServiceProvider serviceProvider, ITransportAddressResolver transportAddressResolver) + public EndpointProxyFactory(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; - this.transportAddressResolver = transportAddressResolver; } public Task CreateProxy( @@ -28,7 +26,7 @@ public Task CreateProxy( // the transport seam assumes the error queue address to be a native address so we need to translate // unfortunately this method is obsoleted but we can't use the one on TransportInfrastructure since that is too late - var translatedErrorQueue = transportAddressResolver.ToTransportAddress(new QueueAddress(transportConfiguration.ErrorQueue)); + //TODO: var translatedErrorQueue = transportAddressResolver.ToTransportAddress(new QueueAddress(transportConfiguration.ErrorQueue)); var transportEndpointConfiguration = RawEndpointConfiguration.Create( endpointToProxy.Name, @@ -49,7 +47,7 @@ public Task CreateProxy( return serviceProvider.GetRequiredService() .TransferMessage(transferContext, cancellationToken: ct); }, - translatedErrorQueue); + transportConfiguration.ErrorQueue); if (transportConfiguration.AutoCreateQueues) { @@ -60,7 +58,7 @@ public Task CreateProxy( transportEndpointConfiguration.CustomErrorHandlingPolicy(new MessageShovelErrorHandlingPolicy( serviceProvider.GetRequiredService>(), - translatedErrorQueue)); + transportConfiguration.ErrorQueue)); return RawEndpoint.Create(transportEndpointConfiguration, cancellationToken); } @@ -77,5 +75,4 @@ static bool IsSubscriptionMessage(IReadOnlyDictionary messageCon } readonly IServiceProvider serviceProvider; - readonly ITransportAddressResolver transportAddressResolver; } \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge/EndpointRegistry.cs b/src/NServiceBus.MessagingBridge/EndpointRegistry.cs index cbf2fcc4..32e3373b 100644 --- a/src/NServiceBus.MessagingBridge/EndpointRegistry.cs +++ b/src/NServiceBus.MessagingBridge/EndpointRegistry.cs @@ -10,36 +10,42 @@ class EndpointRegistry : IEndpointRegistry public void RegisterDispatcher( BridgeEndpoint endpoint, string targetTransportName, - IStartableRawEndpoint startableRawEndpoint) + IStartableRawEndpoint rawEndpoint) { registrations.Add(new ProxyRegistration { Endpoint = endpoint, TranportName = targetTransportName, - RawEndpoint = startableRawEndpoint + RawEndpoint = rawEndpoint }); - - endpointAddressMappings[endpoint.Name] = endpoint.QueueAddress; - targetEndpointAddressMappings[endpoint.QueueAddress] = startableRawEndpoint.ToTransportAddress(new QueueAddress(endpoint.Name)); } public void ApplyMappings(IReadOnlyCollection transportConfigurations) { foreach (var registration in registrations) { + var endpoint = registration.Endpoint; + // target transport is the transport where this endpoint is actually running - var targetTransport = transportConfigurations.Single(t => t.Endpoints.Any(e => e.Name == registration.Endpoint.Name)); + var targetTransport = transportConfigurations.Single(t => t.Endpoints.Any(e => e.Name == endpoint.Name)); // just pick the first proxy that is running on the target transport since // we just need to be able to send messages to that transport var proxyEndpoint = registrations .First(r => r.TranportName == targetTransport.Name) - .RawEndpoint; + .RawEndpoint; + + var transportAddress = proxyEndpoint.ToTransportAddress(endpoint.QueueAddress); + + endpointAddressMappings[registration.Endpoint.Name] = transportAddress; + + targetEndpointAddressMappings[transportAddress] = registration.RawEndpoint.ToTransportAddress(new QueueAddress(endpoint.Name)); + targetEndpointDispatchers[registration.Endpoint.Name] = new TargetEndpointDispatcher( targetTransport.Name, proxyEndpoint, - registration.Endpoint.QueueAddress); + transportAddress); } } diff --git a/src/UnitTests/BridgeConfigurationTests.cs b/src/UnitTests/BridgeConfigurationTests.cs index b143400f..830bb607 100644 --- a/src/UnitTests/BridgeConfigurationTests.cs +++ b/src/UnitTests/BridgeConfigurationTests.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using NServiceBus; -using NServiceBus.Transport; using NUnit.Framework; using UnitTests; @@ -208,15 +207,11 @@ public void Should_require_transports_of_the_same_type_to_be_uniquely_identifiab } [Test] - public void Should_translate_endpoint_addresses_if_not_set() + public void Should_default_endpoint_address_if_not_set() { var configuration = new BridgeConfiguration(); - var defaultAddress = "TheDefaultAddress"; - var transportWithDefaultAddress = new BridgeTransport(new SomeTransport - { - AddressTranslation = _ => defaultAddress - }); + var transportWithDefaultAddress = new BridgeTransport(new SomeTransport()); transportWithDefaultAddress.HasEndpoint("EndpointWithDefaultAddress"); @@ -230,11 +225,11 @@ public void Should_translate_endpoint_addresses_if_not_set() var finalizedConfiguration = FinalizeConfiguration(configuration); - Assert.AreEqual(defaultAddress, finalizedConfiguration.TransportConfigurations - .Single(t => t.Name == transportWithDefaultAddress.Name).Endpoints.Single().QueueAddress); + Assert.AreEqual("EndpointWithDefaultAddress", finalizedConfiguration.TransportConfigurations + .Single(t => t.Name == transportWithDefaultAddress.Name).Endpoints.Single().QueueAddress.ToString()); Assert.AreEqual(customAddress, finalizedConfiguration.TransportConfigurations - .Single(t => t.Name == transportWithCustomAddress.Name).Endpoints.Single().QueueAddress); + .Single(t => t.Name == transportWithCustomAddress.Name).Endpoints.Single().QueueAddress.ToString()); } [Test] @@ -312,7 +307,7 @@ public void Should_throw_if_users_tries_to_set_transaction_mode_on_individual_tr FinalizedBridgeConfiguration FinalizeConfiguration(BridgeConfiguration bridgeConfiguration) { - return bridgeConfiguration.FinalizeConfiguration(logger, new FakeTransportAddressResolver()); + return bridgeConfiguration.FinalizeConfiguration(logger); } class SomeScopeSupportingTransport : FakeTransport @@ -325,15 +320,8 @@ class SomeOtherScopeSupportingTransport : FakeTransport public SomeOtherScopeSupportingTransport() : base(TransportTransactionMode.TransactionScope) { } } - class FakeTransportAddressResolver : ITransportAddressResolver - { - public string ToTransportAddress(QueueAddress queueAddress) => queueAddress.ToString(); - } - class SomeTransport : FakeTransport { - public Func AddressTranslation = name => name; - } class SomeOtherTransport : FakeTransport { } diff --git a/src/UnitTests/MessageShovelTests.cs b/src/UnitTests/MessageShovelTests.cs index 48159b25..550ddcd5 100644 --- a/src/UnitTests/MessageShovelTests.cs +++ b/src/UnitTests/MessageShovelTests.cs @@ -191,7 +191,7 @@ public FakeTargetEndpointRegistry(string targetTransport, BridgeEndpoint targetE public TargetEndpointDispatcher GetTargetEndpointDispatcher(string sourceEndpointName) { - return new TargetEndpointDispatcher(targetTransport, rawEndpoint, targetEndpoint.QueueAddress); + return new TargetEndpointDispatcher(targetTransport, rawEndpoint, targetEndpoint.QueueAddress.ToString()); } public string TranslateToTargetAddress(string sourceAddress) From ce1edbc8293b26f6b50cede17c1326e9468342b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 25 Sep 2023 13:01:53 +0200 Subject: [PATCH 12/18] Translate error queue address at a later stage --- .../EndpointProxyFactory.cs | 7 +------ .../MessageShovelErrorHandlingPolicy.cs | 8 +++----- .../RawEndpoints/DefaultErrorHandlingPolicy.cs | 6 ++---- .../RawEndpoints/IErrorHandlingPolicyContext.cs | 7 ++++++- .../RawEndpoints/RawEndpointConfiguration.cs | 2 +- .../RawEndpoints/RawEndpointErrorHandlingPolicy.cs | 12 ++++++++---- .../RawEndpoints/StartableRawEndpoint.cs | 6 +++++- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs index 6c4f5a06..4449997e 100644 --- a/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs +++ b/src/NServiceBus.MessagingBridge/EndpointProxyFactory.cs @@ -24,10 +24,6 @@ public Task CreateProxy( // NOTE: we have validation to make sure that TransportTransactionMode.TransactionScope is only used when all configured transports can support it var shouldPassTransportTransaction = transportDefinition.TransportTransactionMode == TransportTransactionMode.TransactionScope; - // the transport seam assumes the error queue address to be a native address so we need to translate - // unfortunately this method is obsoleted but we can't use the one on TransportInfrastructure since that is too late - //TODO: var translatedErrorQueue = transportAddressResolver.ToTransportAddress(new QueueAddress(transportConfiguration.ErrorQueue)); - var transportEndpointConfiguration = RawEndpointConfiguration.Create( endpointToProxy.Name, transportConfiguration.TransportDefinition, @@ -57,8 +53,7 @@ public Task CreateProxy( transportEndpointConfiguration.LimitMessageProcessingConcurrencyTo(transportConfiguration.Concurrency); transportEndpointConfiguration.CustomErrorHandlingPolicy(new MessageShovelErrorHandlingPolicy( - serviceProvider.GetRequiredService>(), - transportConfiguration.ErrorQueue)); + serviceProvider.GetRequiredService>())); return RawEndpoint.Create(transportEndpointConfiguration, cancellationToken); } diff --git a/src/NServiceBus.MessagingBridge/MessageShovelErrorHandlingPolicy.cs b/src/NServiceBus.MessagingBridge/MessageShovelErrorHandlingPolicy.cs index da76a8b3..90b8bd2a 100644 --- a/src/NServiceBus.MessagingBridge/MessageShovelErrorHandlingPolicy.cs +++ b/src/NServiceBus.MessagingBridge/MessageShovelErrorHandlingPolicy.cs @@ -6,10 +6,9 @@ class MessageShovelErrorHandlingPolicy : IErrorHandlingPolicy { - public MessageShovelErrorHandlingPolicy(ILogger logger, string errorQueue) + public MessageShovelErrorHandlingPolicy(ILogger logger) { this.logger = logger; - this.errorQueue = errorQueue; } public Task OnError( @@ -23,11 +22,10 @@ public Task OnError( return Task.FromResult(ErrorHandleResult.RetryRequired); } - logger.LogError(handlingContext.Error.Exception, "Message shovel operation failed, message will be moved to {ErrorQueue}", errorQueue); + logger.LogError(handlingContext.Error.Exception, "Message shovel operation failed, message will be moved to {ErrorQueue}", handlingContext.ErrorQueue); - return handlingContext.MoveToErrorQueue(errorQueue, cancellationToken: cancellationToken); + return handlingContext.MoveToErrorQueue(cancellationToken); } readonly ILogger logger; - readonly string errorQueue; } \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge/RawEndpoints/DefaultErrorHandlingPolicy.cs b/src/NServiceBus.MessagingBridge/RawEndpoints/DefaultErrorHandlingPolicy.cs index 27798df6..ece939ae 100644 --- a/src/NServiceBus.MessagingBridge/RawEndpoints/DefaultErrorHandlingPolicy.cs +++ b/src/NServiceBus.MessagingBridge/RawEndpoints/DefaultErrorHandlingPolicy.cs @@ -6,9 +6,8 @@ namespace NServiceBus.Raw class DefaultErrorHandlingPolicy : IErrorHandlingPolicy { - public DefaultErrorHandlingPolicy(string errorQueue, int immediateRetryCount) + public DefaultErrorHandlingPolicy(int immediateRetryCount) { - this.errorQueue = errorQueue; this.immediateRetryCount = immediateRetryCount; } @@ -18,10 +17,9 @@ public Task OnError(IErrorHandlingPolicyContext handlingConte { return RetryRequiredTask; } - return handlingContext.MoveToErrorQueue(errorQueue, cancellationToken: cancellationToken); + return handlingContext.MoveToErrorQueue(cancellationToken); } - readonly string errorQueue; readonly int immediateRetryCount; static Task RetryRequiredTask = Task.FromResult(ErrorHandleResult.RetryRequired); diff --git a/src/NServiceBus.MessagingBridge/RawEndpoints/IErrorHandlingPolicyContext.cs b/src/NServiceBus.MessagingBridge/RawEndpoints/IErrorHandlingPolicyContext.cs index b4fc4aa5..3e4f62a6 100644 --- a/src/NServiceBus.MessagingBridge/RawEndpoints/IErrorHandlingPolicyContext.cs +++ b/src/NServiceBus.MessagingBridge/RawEndpoints/IErrorHandlingPolicyContext.cs @@ -12,7 +12,7 @@ interface IErrorHandlingPolicyContext /// /// Moves a given message to the error queue. /// - Task MoveToErrorQueue(string errorQueue, CancellationToken cancellationToken = default); + Task MoveToErrorQueue(CancellationToken cancellationToken = default); /// /// Gets the error information. @@ -23,5 +23,10 @@ interface IErrorHandlingPolicyContext /// The queue from which the failed message has been received. /// string FailedQueue { get; } + + /// + /// The queue to which the failed will be moved if `MoveToErrorQueue` is called + /// + string ErrorQueue { get; } } } \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointConfiguration.cs b/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointConfiguration.cs index 5d535370..e055f701 100644 --- a/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointConfiguration.cs +++ b/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointConfiguration.cs @@ -37,7 +37,7 @@ public static RawEndpointConfiguration Create( TransportDefinition = transportDefinition; OnMessage = onMessage; PoisonMessageQueue = poisonMessageQueue; - ErrorHandlingPolicy = new DefaultErrorHandlingPolicy(poisonMessageQueue, 3); + ErrorHandlingPolicy = new DefaultErrorHandlingPolicy(3); SendOnly = onMessage == null; PushRuntimeSettings = PushRuntimeSettings.Default; OnCriticalError = (_, __) => Task.CompletedTask; diff --git a/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointErrorHandlingPolicy.cs b/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointErrorHandlingPolicy.cs index a486710a..9b425a74 100644 --- a/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointErrorHandlingPolicy.cs +++ b/src/NServiceBus.MessagingBridge/RawEndpoints/RawEndpointErrorHandlingPolicy.cs @@ -9,14 +9,15 @@ namespace NServiceBus.Raw class RawEndpointErrorHandlingPolicy { - public RawEndpointErrorHandlingPolicy(string localAddress, IMessageDispatcher dispatcher, IErrorHandlingPolicy policy) + public RawEndpointErrorHandlingPolicy(string localAddress, string errorQueue, IMessageDispatcher dispatcher, IErrorHandlingPolicy policy) { this.localAddress = localAddress; + this.errorQueue = errorQueue; this.dispatcher = dispatcher; this.policy = policy; } - public Task OnError(ErrorContext errorContext, CancellationToken cancellationToken = default) => policy.OnError(new Context(localAddress, errorContext, MoveToErrorQueue), dispatcher, cancellationToken); + public Task OnError(ErrorContext errorContext, CancellationToken cancellationToken = default) => policy.OnError(new Context(errorQueue, localAddress, errorContext, MoveToErrorQueue), dispatcher, cancellationToken); async Task MoveToErrorQueue(ErrorContext errorContext, string errorQueue, CancellationToken cancellationToken) { @@ -41,20 +42,23 @@ class Context : IErrorHandlingPolicyContext { Func> moveToErrorQueue; - public Context(string failedQueue, ErrorContext error, Func> moveToErrorQueue) + public Context(string errorQueue, string failedQueue, ErrorContext error, Func> moveToErrorQueue) { this.moveToErrorQueue = moveToErrorQueue; Error = error; + ErrorQueue = errorQueue; FailedQueue = failedQueue; } - public Task MoveToErrorQueue(string errorQueue, CancellationToken cancellationToken = default) => moveToErrorQueue(Error, errorQueue, cancellationToken); + public Task MoveToErrorQueue(CancellationToken cancellationToken = default) => moveToErrorQueue(Error, ErrorQueue, cancellationToken); public ErrorContext Error { get; } public string FailedQueue { get; } + public string ErrorQueue { get; } } readonly string localAddress; + readonly string errorQueue; readonly IMessageDispatcher dispatcher; readonly IErrorHandlingPolicy policy; } diff --git a/src/NServiceBus.MessagingBridge/RawEndpoints/StartableRawEndpoint.cs b/src/NServiceBus.MessagingBridge/RawEndpoints/StartableRawEndpoint.cs index 645e2a33..ee506402 100644 --- a/src/NServiceBus.MessagingBridge/RawEndpoints/StartableRawEndpoint.cs +++ b/src/NServiceBus.MessagingBridge/RawEndpoints/StartableRawEndpoint.cs @@ -81,12 +81,16 @@ public async Task Start(CancellationToken cancellationTok RawTransportReceiver BuildMainReceiver() { var endpointName = rawEndpointConfiguration.EndpointName; + + // the transport seam assumes the error queue address to be a native address so we need to translate + var errorQueue = transportInfrastructure.ToTransportAddress(new QueueAddress(rawEndpointConfiguration.PoisonMessageQueue)); + var receiver = new RawTransportReceiver( messagePump, dispatcher, rawEndpointConfiguration.OnMessage, rawEndpointConfiguration.PushRuntimeSettings, - new RawEndpointErrorHandlingPolicy(endpointName, dispatcher, rawEndpointConfiguration.ErrorHandlingPolicy)); + new RawEndpointErrorHandlingPolicy(endpointName, errorQueue, dispatcher, rawEndpointConfiguration.ErrorHandlingPolicy)); return receiver; } From 4ebff91761013cc38596a7922f7dd60485fe44fc Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 27 Sep 2023 12:58:28 -0400 Subject: [PATCH 13/18] Add MSMQ transport implementation --- .../AcceptanceTests.Msmq.csproj | 2 +- .../ConfigureMsmqTransportTestExecution.cs | 2 +- .../ByteArrayExtensions.cs | 46 +++ .../CheckEndpointNameComplianceForMsmq.cs | 16 + .../CheckMachineNameForCompliance.cs | 43 ++ .../DeadLetterQueueOptionExtensions.cs | 35 ++ .../DelayedDelivery/DelayedDeliveryPump.cs | 154 +++++++ .../DelayedDeliverySettings.cs | 98 +++++ .../DelayedDelivery/DelayedMessage.cs | 41 ++ .../DueDelayedMessagePoller.cs | 359 +++++++++++++++++ .../DelayedDelivery/IDelayedMessageStore.cs | 57 +++ .../DelayedDelivery/Sql/SqlConstants.cs | 41 ++ .../DelayedDelivery/Sql/SqlNameHelper.cs | 33 ++ .../Sql/SqlServerDelayedMessageStore.cs | 157 ++++++++ .../Sql/TimeoutTableCreator.cs | 52 +++ .../EndpointInstanceExtensions.cs | 23 ++ .../ExceptionExtensions.cs | 12 + .../ExceptionHeaderHelper.cs | 60 +++ .../FailureRateCircuitBreaker.cs | 53 +++ .../HeaderInfo.cs | 21 + .../InstanceMappingFileFeature.cs | 93 +++++ .../InstanceMappingFileMonitor.cs | 112 ++++++ .../InstanceMappingFileParser.cs | 36 ++ .../InstanceMappingFileSettings.cs | 79 ++++ .../Loaders/IInstanceMappingLoader.cs | 9 + .../Loaders/InstanceMappingFileAccess.cs | 30 ++ .../Loaders/InstanceMappingUriLoader.cs | 29 ++ .../ValidatingInstanceMappingLoader.cs | 25 ++ .../EmbeddedSchemaInstanceMappingValidator.cs | 30 ++ .../FallbackInstanceMappingValidator.cs | 45 +++ .../Validators/IInstanceMappingValidator.cs | 9 + .../InstanceMapping/Validators/endpoints.xsd | 21 + .../Validators/endpointsV2.xsd | 22 + .../JournalOptionExtensions.cs | 36 ++ .../MessagePump.cs | 331 ++++++++++++++++ .../MessageQueueExtensions.cs | 199 ++++++++++ .../MsmqAddress.cs | 135 +++++++ .../MsmqConfigurationExtensions.cs | 195 +++++++++ .../MsmqFailureInfoStorage.cs | 109 +++++ .../MsmqMessageDispatcher.cs | 335 ++++++++++++++++ .../MsmqQueueCreator.cs | 77 ++++ .../MsmqScopeOptions.cs | 36 ++ .../MsmqTransport.cs | 375 ++++++++++++++++++ .../MsmqTransportInfrastructure.cs | 64 +++ .../MsmqUtilities.cs | 216 ++++++++++ .../NServiceBus.MessagingBridge.Msmq.csproj | 11 +- .../NoTransactionStrategy.cs | 53 +++ .../Persistence/MsmqPersistence.cs | 20 + .../IMsmqSubscriptionStorageQueue.cs | 11 + .../MsmqSubscriptionMessage.cs | 28 ++ .../MsmqSubscriptionPersistence.cs | 64 +++ .../MsmqSubscriptionStorage.cs | 238 +++++++++++ ...scriptionStorageConfigurationExtensions.cs | 32 ++ .../MsmqSubscriptionStorageQueue.cs | 68 ++++ .../QueuePermissions.cs | 73 ++++ .../ReadOnlyStream.cs | 45 +++ .../ReceiveOnlyNativeTransactionStrategy.cs | 111 ++++++ .../ReceiveStrategy.cs | 144 +++++++ .../RepeatedFailuresOverTimeCircuitBreaker.cs | 71 ++++ ...micWithReceiveNativeTransactionStrategy.cs | 114 ++++++ .../TimeToBeReceived.cs | 45 +++ .../TimeToBeReceivedOverrideChecker.cs | 25 ++ .../TransactionScopeStrategy.cs | 113 ++++++ .../Utils/AsyncTimer.cs | 62 +++ .../Utils/Guard.cs | 43 ++ .../Utils/IAsyncTimer.cs | 12 + 66 files changed, 5333 insertions(+), 3 deletions(-) create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ByteArrayExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/CheckEndpointNameComplianceForMsmq.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/CheckMachineNameForCompliance.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DeadLetterQueueOptionExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/EndpointInstanceExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ExceptionExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ExceptionHeaderHelper.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/FailureRateCircuitBreaker.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/HeaderInfo.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileFeature.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileMonitor.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileParser.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileSettings.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/IInstanceMappingLoader.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingFileAccess.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingUriLoader.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/ValidatingInstanceMappingLoader.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/EmbeddedSchemaInstanceMappingValidator.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/FallbackInstanceMappingValidator.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/IInstanceMappingValidator.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpoints.xsd create mode 100644 src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpointsV2.xsd create mode 100644 src/NServiceBus.MessagingBridge.Msmq/JournalOptionExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MessagePump.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MessageQueueExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqAddress.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqFailureInfoStorage.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqQueueCreator.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqScopeOptions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/MsmqUtilities.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/NoTransactionStrategy.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/QueuePermissions.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ReadOnlyStream.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ReceiveOnlyNativeTransactionStrategy.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/ReceiveStrategy.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/RepeatedFailuresOverTimeCircuitBreaker.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/SendsAtomicWithReceiveNativeTransactionStrategy.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceived.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceivedOverrideChecker.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/TransactionScopeStrategy.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Utils/AsyncTimer.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Utils/Guard.cs create mode 100644 src/NServiceBus.MessagingBridge.Msmq/Utils/IAsyncTimer.cs diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index 77d7b10c..45450221 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0-windows diff --git a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs index f089648e..c2673b70 100644 --- a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs +++ b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs @@ -1,7 +1,7 @@ using System; //using System.Collections.Generic; //using System.Linq; -//using System.Messaging; +//using MSMQ.Messaging; using System.Threading; using System.Threading.Tasks; using NServiceBus; diff --git a/src/NServiceBus.MessagingBridge.Msmq/ByteArrayExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/ByteArrayExtensions.cs new file mode 100644 index 00000000..3deeb6af --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ByteArrayExtensions.cs @@ -0,0 +1,46 @@ +using System; + +static class ByteArrayExtensions +{ + public static int LastIndexOf(this byte[] data, byte[] pattern) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + if (pattern.Length > data.Length) + { + return -1; + } + + var cycles = data.Length - pattern.Length + 1; + for (var dataIndex = cycles; dataIndex > 0; dataIndex--) + { + if (data[dataIndex] != pattern[0]) + { + continue; + } + + int patternIndex; + for (patternIndex = pattern.Length - 1; patternIndex >= 1; patternIndex--) + { + if (data[dataIndex + patternIndex] != pattern[patternIndex]) + { + break; + } + } + + if (patternIndex == 0) + { + return dataIndex; + } + } + return -1; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/CheckEndpointNameComplianceForMsmq.cs b/src/NServiceBus.MessagingBridge.Msmq/CheckEndpointNameComplianceForMsmq.cs new file mode 100644 index 00000000..e694bc7b --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/CheckEndpointNameComplianceForMsmq.cs @@ -0,0 +1,16 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + static class CheckEndpointNameComplianceForMsmq + { + public static void Check(string endpointName) + { + // .NET Messaging API hardcodes the buffer size to 124. As a result, the entire format name of the queue cannot exceed 123 + var formatName = $"DIRECT=OS:{Environment.MachineName}\\private$\\{endpointName}"; + if (formatName.Length > 150) + { + throw new InvalidOperationException($"The specified endpoint name {endpointName} is too long. The fully formatted queue name for the endpoint:{formatName} must be 150 characters or less."); + } + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/CheckMachineNameForCompliance.cs b/src/NServiceBus.MessagingBridge.Msmq/CheckMachineNameForCompliance.cs new file mode 100644 index 00000000..2e841197 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/CheckMachineNameForCompliance.cs @@ -0,0 +1,43 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Runtime.InteropServices; + using System.Text; + + static class CheckMachineNameForCompliance + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + static extern bool GetComputerNameEx(COMPUTER_NAME_FORMAT nameType, [Out] StringBuilder lpBuffer, ref uint lpnSize); + + /// + /// Checks to see if the NetBios computer name exceeds 15 characters and if so throws an exception. + /// + public static void Check() + { + uint capacity = 24; + var buffer = new StringBuilder((int)capacity); + if (!GetComputerNameEx(COMPUTER_NAME_FORMAT.ComputerNameNetBIOS, buffer, ref capacity)) + { + return; + } + var netbiosName = buffer.ToString(); + if (netbiosName.Length <= 15) + { + return; + } + throw new Exception($"The NetBIOS name {netbiosName} is longer than 15 characters. Shorten the machine name to 15 characters or less for MSMQ to deliver messages."); + } + + enum COMPUTER_NAME_FORMAT + { + ComputerNameNetBIOS, + ComputerNameDnsHostname, + ComputerNameDnsDomain, + ComputerNameDnsFullyQualified, + ComputerNamePhysicalNetBIOS, + ComputerNamePhysicalDnsHostname, + ComputerNamePhysicalDnsDomain, + ComputerNamePhysicalDnsFullyQualified + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DeadLetterQueueOptionExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/DeadLetterQueueOptionExtensions.cs new file mode 100644 index 00000000..54124609 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DeadLetterQueueOptionExtensions.cs @@ -0,0 +1,35 @@ +namespace NServiceBus +{ + using Extensibility; + using NServiceBus.MessagingBridge.Msmq; + using Transport; + + /// + /// Gives users fine grained control over routing via extension methods. + /// + public static class DeadLetterQueueOptionExtensions + { + const string KeyDeadLetterQueue = "MSMQ.UseDeadLetterQueue"; + + /// + /// Enable or disable MSMQ dead letter queueing. + /// + /// Option being extended. + /// Either enable or disabling message dead letter queueing. + public static void UseDeadLetterQueue(this ExtendableOptions options, bool enable = true) + { + Guard.AgainstNull(nameof(options), options); + options.GetDispatchProperties()[KeyDeadLetterQueue] = enable.ToString(); + } + + internal static bool? ShouldUseDeadLetterQueue(this DispatchProperties dispatchProperties) + { + if (dispatchProperties.TryGetValue(KeyDeadLetterQueue, out var boolString)) + { + return bool.Parse(boolString); + } + + return null; + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs new file mode 100644 index 00000000..bc356c1c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs @@ -0,0 +1,154 @@ +namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using System.Transactions; + using Faults; + using Logging; + using NServiceBus.Transport; + using Routing; + + class DelayedDeliveryPump + { + public DelayedDeliveryPump(MsmqMessageDispatcher dispatcher, + DueDelayedMessagePoller poller, + IDelayedMessageStore storage, + MessagePump messagePump, + string errorQueue, + int numberOfRetries, + Action criticalErrorAction, + TimeSpan timeToWaitForStoreCircuitBreaker, + Dictionary faultMetadata, + TransportTransactionMode transportTransactionMode) + { + this.dispatcher = dispatcher; + this.poller = poller; + this.storage = storage; + this.numberOfRetries = numberOfRetries; + this.faultMetadata = faultMetadata; + pump = messagePump; + this.errorQueue = errorQueue; + + txOption = transportTransactionMode == TransportTransactionMode.TransactionScope + ? TransactionScopeOption.Required + : TransactionScopeOption.RequiresNew; + + storeCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("DelayedDeliveryStore", timeToWaitForStoreCircuitBreaker, + ex => criticalErrorAction("Failed to store delayed message", ex, CancellationToken.None)); + } + + public async Task Start(CancellationToken cancellationToken = default) + { + await pump.Initialize(PushRuntimeSettings.Default, TimeoutReceived, OnError, cancellationToken).ConfigureAwait(false); + await pump.StartReceive(cancellationToken).ConfigureAwait(false); + poller.Start(); + } + + public async Task Stop(CancellationToken cancellationToken = default) + { + await pump.StopReceive(cancellationToken).ConfigureAwait(false); + await poller.Stop(cancellationToken).ConfigureAwait(false); + } + + async Task TimeoutReceived(MessageContext context, CancellationToken cancellationToken) + { + if (!context.Headers.TryGetValue(MsmqUtilities.PropertyHeaderPrefix + MsmqMessageDispatcher.TimeoutDestination, out var destination)) + { + throw new Exception("This message does not represent a timeout"); + } + + if (!context.Headers.TryGetValue(MsmqUtilities.PropertyHeaderPrefix + MsmqMessageDispatcher.TimeoutAt, out var atString)) + { + throw new Exception("This message does not represent a timeout"); + } + + var id = context.NativeMessageId; //Use native message ID as a key in the delayed delivery table + var at = DateTimeOffsetHelper.ToDateTimeOffset(atString); + + var message = context.Extensions.Get(); + + var diff = DateTime.UtcNow - at; + + if (diff.Ticks > 0) // Due + { + dispatcher.DispatchDelayedMessage(id, message.Extension, context.Body, destination, context.TransportTransaction); + } + else + { + var timeout = new DelayedMessage + { + Destination = destination, + MessageId = id, + Body = context.Body.ToArray(), + Time = at.UtcDateTime, + Headers = message.Extension + }; + + try + { + using (var tx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + await storage.Store(timeout, cancellationToken).ConfigureAwait(false); + tx.Complete(); + } + + storeCircuitBreaker.Success(); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Shutting down + return; + } + catch (Exception e) + { + await storeCircuitBreaker.Failure(e, cancellationToken).ConfigureAwait(false); + throw new Exception("Error while storing delayed message", e); + } + +#pragma warning disable PS0022 // A DateTime should not be implicitly cast to a DateTimeOffset + poller.Signal(timeout.Time); +#pragma warning restore PS0022 // A DateTime should not be implicitly cast to a DateTimeOffset + } + } + + async Task OnError(ErrorContext errorContext, CancellationToken cancellationToken) + { + Log.Error($"OnError {errorContext.Message.MessageId}", errorContext.Exception); + + if (errorContext.ImmediateProcessingFailures < numberOfRetries) + { + return ErrorHandleResult.RetryRequired; + } + + var message = errorContext.Message; + + ExceptionHeaderHelper.SetExceptionHeaders(message.Headers, errorContext.Exception); + message.Headers[FaultsHeaderKeys.FailedQ] = errorContext.ReceiveAddress; + foreach (var pair in faultMetadata) + { + message.Headers[pair.Key] = pair.Value; + } + + var outgoingMessage = new OutgoingMessage(message.NativeMessageId, message.Headers, message.Body); + var transportOperation = new TransportOperation(outgoingMessage, new UnicastAddressTag(errorQueue)); + await dispatcher.Dispatch(new TransportOperations(transportOperation), errorContext.TransportTransaction, cancellationToken).ConfigureAwait(false); + + return ErrorHandleResult.Handled; + } + + readonly MsmqMessageDispatcher dispatcher; + readonly DueDelayedMessagePoller poller; + readonly IDelayedMessageStore storage; + readonly int numberOfRetries; + readonly MessagePump pump; + readonly Dictionary faultMetadata; + readonly string errorQueue; + RepeatedFailuresOverTimeCircuitBreaker storeCircuitBreaker; + readonly TransactionScopeOption txOption; + readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }; + + static readonly ILog Log = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs new file mode 100644 index 00000000..d4599868 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs @@ -0,0 +1,98 @@ +namespace NServiceBus +{ + using System; + using NServiceBus.MessagingBridge.Msmq; + + /// + /// Configures delayed delivery support. + /// + public class DelayedDeliverySettings + { + int numberOfRetries; + TimeSpan timeToTriggerStoreCircuitBreaker = TimeSpan.FromSeconds(30); + TimeSpan timeToTriggerFetchCircuitBreaker = TimeSpan.FromSeconds(30); + TimeSpan timeToTriggerDispatchCircuitBreaker = TimeSpan.FromSeconds(30); + int maximumRecoveryFailuresPerSecond = 1; + + /// + /// The store to keep delayed messages. + /// + public IDelayedMessageStore DelayedMessageStore { get; } + + /// + /// Number of retries when trying to forward due delayed messages. + /// + public int NumberOfRetries + { + get => numberOfRetries; + set + { + Guard.AgainstNegativeAndZero("value", value); + numberOfRetries = value; + } + } + + /// + /// Time to wait before triggering the circuit breaker that monitors the storing of delayed messages in the database. Defaults to 30 seconds. + /// + public TimeSpan TimeToTriggerStoreCircuitBreaker + { + get => timeToTriggerStoreCircuitBreaker; + set + { + Guard.AgainstNegativeAndZero("value", value); + timeToTriggerStoreCircuitBreaker = value; + } + } + + /// + /// Time to wait before triggering the circuit breaker that monitors the fetching of due delayed messages from the database. Defaults to 30 seconds. + /// + public TimeSpan TimeToTriggerFetchCircuitBreaker + { + get => timeToTriggerFetchCircuitBreaker; + set + { + Guard.AgainstNegativeAndZero("value", value); + timeToTriggerFetchCircuitBreaker = value; + } + } + + /// + /// Time to wait before triggering the circuit breaker that monitors the dispatching of due delayed messages to the destination. Defaults to 30 seconds. + /// + public TimeSpan TimeToTriggerDispatchCircuitBreaker + { + get => timeToTriggerDispatchCircuitBreaker; + set + { + Guard.AgainstNegativeAndZero("value", value); + timeToTriggerDispatchCircuitBreaker = value; + } + } + + /// + /// Maximum number of recovery failures per second that triggers the recovery circuit breaker. Recovery attempts are attempts to increment the failure + /// counter after a failed dispatch and forwarding messages to the error queue. Defaults to 1/s. + /// + public int MaximumRecoveryFailuresPerSecond + { + get => maximumRecoveryFailuresPerSecond; + set + { + Guard.AgainstNegativeAndZero("value", value); + maximumRecoveryFailuresPerSecond = value; + } + } + + /// + /// Configures delayed delivery. + /// + /// The store to keep delayed messages. + public DelayedDeliverySettings(IDelayedMessageStore delayedMessageStore) + { + Guard.AgainstNull(nameof(delayedMessageStore), delayedMessageStore); + DelayedMessageStore = delayedMessageStore; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs new file mode 100644 index 00000000..740d18d6 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs @@ -0,0 +1,41 @@ + +namespace NServiceBus +{ + using System; + + /// + /// Represents a delayed message. + /// + public class DelayedMessage + { + /// + /// Date and time the message is due. + /// + public DateTime Time { get; set; } + + /// + /// Native message ID + /// + public string MessageId { get; set; } + + /// + /// The body of the message. + /// + public byte[] Body { get; set; } + + /// + /// The serialized headers of the message + /// + public byte[] Headers { get; set; } + + /// + /// The address of the destination queue + /// + public string Destination { get; set; } + + /// + /// The number of attempt already made to forward the message to its destination. + /// + public int NumberOfRetries { get; set; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs new file mode 100644 index 00000000..360ee4bc --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs @@ -0,0 +1,359 @@ +namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Channels; + using System.Threading.Tasks; + using System.Transactions; + using Faults; + using Logging; + using NServiceBus.Transport; + using Routing; + using Unicast.Queuing; + + class DueDelayedMessagePoller + { + public DueDelayedMessagePoller(MsmqMessageDispatcher dispatcher, + IDelayedMessageStore delayedMessageStore, + int numberOfRetries, + Action criticalErrorAction, + string timeoutsErrorQueue, + Dictionary faultMetadata, + TransportTransactionMode transportTransactionMode, + TimeSpan timeToTriggerFetchCircuitBreaker, + TimeSpan timeToTriggerDispatchCircuitBreaker, + int maximumRecoveryFailuresPerSecond, + string timeoutsQueueTransportAddress) + { + txOption = transportTransactionMode == TransportTransactionMode.TransactionScope + ? TransactionScopeOption.Required + : TransactionScopeOption.RequiresNew; + this.delayedMessageStore = delayedMessageStore; + errorQueue = timeoutsErrorQueue; + this.faultMetadata = faultMetadata; + this.timeoutsQueueTransportAddress = timeoutsQueueTransportAddress; + this.numberOfRetries = numberOfRetries; + this.dispatcher = dispatcher; + fetchCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqDelayedMessageFetch", timeToTriggerFetchCircuitBreaker, + ex => criticalErrorAction("Failed to fetch due delayed messages from the storage", ex, tokenSource?.Token ?? CancellationToken.None)); + + dispatchCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqDelayedMessageDispatch", timeToTriggerDispatchCircuitBreaker, + ex => criticalErrorAction("Failed to dispatch delayed messages to destination", ex, tokenSource?.Token ?? CancellationToken.None)); + + failureHandlingCircuitBreaker = new FailureRateCircuitBreaker("MsmqDelayedMessageFailureHandling", maximumRecoveryFailuresPerSecond, + ex => criticalErrorAction("Failed to execute error handling for delayed message forwarding", ex, tokenSource?.Token ?? CancellationToken.None)); + + signalQueue = Channel.CreateBounded(1); + taskQueue = Channel.CreateBounded(2); + } + + public void Start() + { + tokenSource = new CancellationTokenSource(); + loopTask = Task.Run(() => Loop(tokenSource.Token)); + completionTask = Task.Run(() => AwaitHandleTasks()); + } + + public async Task Stop(CancellationToken cancellationToken = default) + { + tokenSource.Cancel(); + await loopTask.ConfigureAwait(false); + await completionTask.ConfigureAwait(false); + } + + public void Signal(DateTimeOffset timeoutTime) + { + //If the next timeout is within a minute from now, trigger the poller + if (DateTimeOffset.UtcNow.Add(MaxSleepDuration) > timeoutTime) + { + //If there is something already in the queue we are fine. + signalQueue.Writer.TryWrite(true); + } + } + + /// + /// This method does not accept a cancellation token because it needs to finish all the tasks that have been started even if cancellation is under way. + /// +#pragma warning disable PS0018 // A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext + async Task AwaitHandleTasks() +#pragma warning restore PS0018 // A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext + { + while (await taskQueue.Reader.WaitToReadAsync().ConfigureAwait(false)) //if this returns false the channel is completed + { + while (taskQueue.Reader.TryRead(out var task)) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + Log.Debug("A shutdown was triggered and Poll task has been cancelled."); + } + catch (Exception e) + { + failureHandlingCircuitBreaker.Failure(e); + } + } + } + } + + async Task Loop(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { +#pragma warning disable PS0021 // Highlight when a try block passes multiple cancellation tokens + try +#pragma warning restore PS0021 // Highlight when a try block passes multiple cancellation tokens + { + + //We create a TaskCompletionSource source here and pass it to spawned Poll task. Once the Poll task is done with fetching + //the next due timeout, we continue with the loop. This allows us to run the Poll tasks in an overlapping way so that at any time there is + //one Poll task fetching and one dispatching. + + var completionSource = new TaskCompletionSource(); + var handleTask = Poll(completionSource, cancellationToken); + await taskQueue.Writer.WriteAsync(handleTask, cancellationToken).ConfigureAwait(false); + var nextPoll = await completionSource.Task.ConfigureAwait(false); + + if (nextPoll.HasValue) + { + //We wait either for a signal that a new delayed message has been stored or until next timeout should be due + //After waiting we cancel the token so that the task waiting for the signal is cancelled and does not "eat" the next + //signal when it is raised in the next iteration of this loop + using (var waitCancelled = new CancellationTokenSource()) + using (var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, waitCancelled.Token)) + { + var waitTask = WaitIfNeeded(nextPoll.Value, combinedSource.Token); + var signalTask = WaitForSignal(combinedSource.Token); + + await Task.WhenAny(waitTask, signalTask).ConfigureAwait(false); + waitCancelled.Cancel(); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + Log.Debug("A shutdown was triggered, canceling the Loop operation."); + break; + } + catch (Exception e) + { + Log.Error("Failed to poll and dispatch due timeouts from storage.", e); + //Poll and HandleDueDelayedMessage have their own exception handling logic so any exception here is likely going to be related to the transaction itself + await fetchCircuitBreaker.Failure(e, cancellationToken).ConfigureAwait(false); + } + } + } + finally + { + //No matter what we need to complete the writer so that we can stop gracefully + taskQueue.Writer.Complete(); + } + } + + async Task WaitForSignal(CancellationToken cancellationToken) + { + try + { + await signalQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + Log.Debug("No new delayed messages have been stored while waiting for the next due delayed message."); + } + } + + Task WaitIfNeeded(DateTimeOffset nextPoll, CancellationToken cancellationToken) + { + var waitTime = nextPoll - DateTimeOffset.UtcNow; + if (waitTime > TimeSpan.Zero) + { + if (waitTime > MaxSleepDuration) + { + // Task.Delay() throws for times > int.MaxValue ms, which is ~24.85 days + waitTime = MaxSleepDuration; + } + return Task.Delay(waitTime, cancellationToken); + } + + return Task.CompletedTask; + } + + async Task Poll(TaskCompletionSource result, CancellationToken cancellationToken) + { + DelayedMessage timeout = null; + DateTimeOffset now = DateTimeOffset.UtcNow; + try + { + using (var tx = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + timeout = await delayedMessageStore.FetchNextDueTimeout(now, cancellationToken).ConfigureAwait(false); + fetchCircuitBreaker.Success(); + + if (timeout != null) + { + result.SetResult(null); + HandleDueDelayedMessage(timeout, cancellationToken); + + var success = await delayedMessageStore.Remove(timeout, cancellationToken).ConfigureAwait(false); + if (!success) + { + Log.WarnFormat("Potential more-than-once dispatch as delayed message already removed from storage."); + } + } + else + { + using (new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + var nextDueTimeout = await delayedMessageStore.Next(cancellationToken).ConfigureAwait(false); + if (nextDueTimeout.HasValue) + { + result.SetResult(nextDueTimeout); + } + else + { + //If no timeouts, wait a while + result.SetResult(now.Add(MaxSleepDuration)); + } + } + } + tx.Complete(); + } + } + catch (QueueNotFoundException exception) + { + if (timeout != null) + { + await TrySendDelayedMessageToErrorQueue(timeout, exception, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Shutting down. Bubble up the exception. Will be awaited by AwaitHandleTasks method that catches OperationCanceledExceptions + throw; + } + catch (Exception exception) + { + Log.Error("Failure during timeout polling.", exception); + if (timeout != null) + { + await dispatchCircuitBreaker.Failure(exception, cancellationToken).ConfigureAwait(false); + using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + await delayedMessageStore.IncrementFailureCount(timeout, cancellationToken).ConfigureAwait(false); + scope.Complete(); + } + + if (timeout.NumberOfRetries > numberOfRetries) + { + await TrySendDelayedMessageToErrorQueue(timeout, exception, cancellationToken).ConfigureAwait(false); + } + } + else + { + await fetchCircuitBreaker.Failure(exception, cancellationToken).ConfigureAwait(false); + } + } + finally + { + //In case we failed before SetResult + result.TrySetResult(null); + } + } + + void HandleDueDelayedMessage(DelayedMessage timeout, CancellationToken cancellationToken) + { + TimeSpan diff = DateTimeOffset.UtcNow - new DateTimeOffset(timeout.Time, TimeSpan.Zero); + + Log.DebugFormat("Timeout {0} over due for {1}", timeout.MessageId, diff); + + using (var tx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set(Transaction.Current); + dispatcher.DispatchDelayedMessage( + timeout.MessageId, + timeout.Headers, + timeout.Body, + timeout.Destination, + transportTransaction + ); + + tx.Complete(); + } + + dispatchCircuitBreaker.Success(); + } + + async Task TrySendDelayedMessageToErrorQueue(DelayedMessage timeout, Exception exception, CancellationToken cancellationToken) + { + try + { + bool success = await delayedMessageStore.Remove(timeout, cancellationToken).ConfigureAwait(false); + + if (!success) + { + // Already dispatched + return; + } + + Dictionary headersAndProperties = MsmqUtilities.DeserializeMessageHeaders(timeout.Headers); + + ExceptionHeaderHelper.SetExceptionHeaders(headersAndProperties, exception); + headersAndProperties[FaultsHeaderKeys.FailedQ] = timeoutsQueueTransportAddress; + foreach (KeyValuePair pair in faultMetadata) + { + headersAndProperties[pair.Key] = pair.Value; + } + + Log.InfoFormat("Move {0} to error queue", timeout.MessageId); + using (var transportTx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set(Transaction.Current); + + var outgoingMessage = new OutgoingMessage(timeout.MessageId, headersAndProperties, timeout.Body); + var transportOperation = new TransportOperation(outgoingMessage, new UnicastAddressTag(errorQueue)); + await dispatcher.Dispatch(new TransportOperations(transportOperation), transportTransaction, CancellationToken.None) + .ConfigureAwait(false); + + transportTx.Complete(); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Shutting down + Log.Debug("Aborted sending delayed message to error queue due to shutdown."); + } + catch (Exception ex) + { + Log.Error($"Failed to move delayed message {timeout.MessageId} to the error queue {errorQueue} after {timeout.NumberOfRetries} failed attempts at dispatching it to the destination", ex); + } + } + + static readonly ILog Log = LogManager.GetLogger(); + static readonly TimeSpan MaxSleepDuration = TimeSpan.FromMinutes(1); + + readonly Dictionary faultMetadata; + readonly string timeoutsQueueTransportAddress; + + IDelayedMessageStore delayedMessageStore; + MsmqMessageDispatcher dispatcher; + string errorQueue; + int numberOfRetries; + RepeatedFailuresOverTimeCircuitBreaker fetchCircuitBreaker; + RepeatedFailuresOverTimeCircuitBreaker dispatchCircuitBreaker; + FailureRateCircuitBreaker failureHandlingCircuitBreaker; + Task loopTask; + Task completionTask; + Channel signalQueue; + Channel taskQueue; + CancellationTokenSource tokenSource; + TransactionScopeOption txOption; + TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs new file mode 100644 index 00000000..ae18afdb --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs @@ -0,0 +1,57 @@ +namespace NServiceBus +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents a store for delayed messages. + /// + public interface IDelayedMessageStore + { + /// + /// Initializes the storage e.g. creates required database artifacts etc. + /// + /// Name of the endpoint that hosts the delayed delivery storage. + /// The transaction mode selected for the transport. The storage implementation should throw an exception if it can't support specified + /// transaction mode e.g. TransactionScope mode requires the storage to enlist in a distributed transaction managed by the DTC. + /// The cancellation token set if the endpoint begins to shut down while the Initialize method is executing. + Task Initialize(string endpointName, TransportTransactionMode transactionMode, CancellationToken cancellationToken = default); + + /// + /// Returns the date and time set for the next delayed message to become due or null if there are no delayed messages stored. + /// + /// + Task Next(CancellationToken cancellationToken = default); + + /// + /// Stores a delayed message. + /// + /// Object representing a delayed message. + /// The cancellation token for cooperative cancellation + Task Store(DelayedMessage entity, CancellationToken cancellationToken = default); + + /// + /// Removes a due delayed message that has been dispatched to its destination from the store. + /// + /// Object representing a delayed message previously returned by FetchNextDueTimeout. + /// The cancellation token for cooperative cancellation + /// True if the removal succeeded. False if there was nothing to remove because the delayed message was already gone. + Task Remove(DelayedMessage entity, CancellationToken cancellationToken = default); + + /// + /// Increments the counter of failures for a given due delayed message. + /// + /// Object representing a delayed message previously returned by FetchNextDueTimeout. + /// The cancellation token for cooperative cancellation + /// True if the increment succeeded. False if the delayed message was already gone. + Task IncrementFailureCount(DelayedMessage entity, CancellationToken cancellationToken = default); + + /// + /// Retrieves the oldest due delayed message from the store or returns null if there is no due delayed messages. + /// + /// The point in time to which to compare the due date of the messages. + /// The cancellation token for cooperative cancellation + Task FetchNextDueTimeout(DateTimeOffset at, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs new file mode 100644 index 00000000..8fac1706 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs @@ -0,0 +1,41 @@ +namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql +{ + class SqlConstants + { + public const string SqlInsert = "INSERT INTO {0} (Id, Destination, Time, Headers, State) VALUES (@id, @destination, @time, @headers, @state);"; + public const string SqlFetch = "SELECT TOP 1 Id, Destination, Time, Headers, State, RetryCount FROM {0} WITH (READPAST, UPDLOCK, ROWLOCK) WHERE Time < @time ORDER BY Time"; + public const string SqlDelete = "DELETE {0} WHERE Id = @id"; + public const string SqlUpdate = "UPDATE {0} SET RetryCount = RetryCount + 1 WHERE Id = @id"; + public const string SqlGetNext = "SELECT TOP 1 Time FROM {0} ORDER BY Time"; + public const string SqlCreateTable = @" +if not exists ( + select * from sys.objects + where + object_id = object_id('{0}') + and type in ('U') +) +begin + create table {0} ( + Id nvarchar(250) not null primary key, + Destination nvarchar(200), + State varbinary(max), + Time datetime, + Headers varbinary(max) not null, + RetryCount INT NOT NULL default(0) + ) +end + +if not exists +( + select * + from sys.indexes + where + name = 'Index_Time' and + object_id = object_id('{0}') +) +begin + create index Index_Time on {0} (Time); +end +"; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs new file mode 100644 index 00000000..0fe8446f --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs @@ -0,0 +1,33 @@ +namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql +{ + class SqlNameHelper + { + const string Prefix = "["; + const string Suffix = "]"; + + public static string Quote(string unquotedName) + { + if (unquotedName == null) + { + return null; + } + return Prefix + unquotedName.Replace(Suffix, Suffix + Suffix) + Suffix; + } + + public static string Unquote(string quotedString) + { + if (quotedString == null) + { + return null; + } + + if (!quotedString.StartsWith(Prefix) || !quotedString.EndsWith(Suffix)) + { + return quotedString; + } + + return quotedString + .Substring(Prefix.Length, quotedString.Length - Prefix.Length - Suffix.Length).Replace(Suffix + Suffix, Suffix); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs new file mode 100644 index 00000000..5af1d589 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs @@ -0,0 +1,157 @@ +namespace NServiceBus +{ + using System; + using System.Data; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Data.SqlClient; + using NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql; + + //TODO: Either we should expect that the connection created by the factory is open of we should make it non-async + /// + /// Factory method for creating SQL Server connections. + /// + public delegate Task CreateSqlConnection(CancellationToken cancellationToken = default); + + /// + /// Implementation of the delayed message store based on the SQL Server. + /// + public class SqlServerDelayedMessageStore : IDelayedMessageStore + { + string schema; + string tableName; + CreateSqlConnection createSqlConnection; + + string insertCommand; + string removeCommand; + string bumpFailureCountCommand; + string nextCommand; + string fetchCommand; + + /// + /// Creates a new instance of the SQL Server delayed message store. + /// + /// Connection string to the SQL Server database. + /// (optional) schema to use. Defaults to dbo + /// (optional) name of the table where delayed messages are stored. Defaults to name of the endpoint with .Delayed suffix. + public SqlServerDelayedMessageStore(string connectionString, string schema = null, string tableName = null) + : this(token => Task.FromResult(new SqlConnection(connectionString)), schema, tableName) + { + } + + /// + /// Creates a new instance of the SQL Server delayed message store. + /// + /// Factory for database connections. + /// (optional) schema to use. Defaults to dbo + /// (optional) name of the table where delayed messages are stored. Defaults to name of the endpoint with .Delayed suffix. + public SqlServerDelayedMessageStore(CreateSqlConnection connectionFactory, string schema = null, string tableName = null) + { + createSqlConnection = connectionFactory; + this.tableName = tableName; + this.schema = schema ?? "dbo"; + } + + /// + public async Task Store(DelayedMessage timeout, CancellationToken cancellationToken = default) + { + using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + using (var cmd = new SqlCommand(insertCommand, cn)) + { + cmd.Parameters.AddWithValue("@id", timeout.MessageId); + cmd.Parameters.AddWithValue("@destination", timeout.Destination); + cmd.Parameters.AddWithValue("@time", timeout.Time); + cmd.Parameters.AddWithValue("@headers", timeout.Headers); + cmd.Parameters.AddWithValue("@state", timeout.Body); + await cn.OpenAsync(cancellationToken).ConfigureAwait(false); + _ = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + + + /// + public async Task Remove(DelayedMessage timeout, CancellationToken cancellationToken = default) + { + using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + using (var cmd = new SqlCommand(removeCommand, cn)) + { + cmd.Parameters.AddWithValue("@id", timeout.MessageId); + await cn.OpenAsync(cancellationToken).ConfigureAwait(false); + var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + return affected == 1; + } + } + + /// + public async Task IncrementFailureCount(DelayedMessage timeout, CancellationToken cancellationToken = default) + { + using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + using (var cmd = new SqlCommand(bumpFailureCountCommand, cn)) + { + cmd.Parameters.AddWithValue("@id", timeout.MessageId); + await cn.OpenAsync(cancellationToken).ConfigureAwait(false); + var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + return affected == 1; + } + } + + /// + public async Task Initialize(string queueName, TransportTransactionMode transactionMode, CancellationToken cancellationToken = default) + { + tableName ??= $"{queueName}.timeouts"; + + var quotedFullName = $"{SqlNameHelper.Quote(schema)}.{SqlNameHelper.Quote(tableName)}"; + + var creator = new TimeoutTableCreator(createSqlConnection, quotedFullName); + await creator.CreateIfNecessary(cancellationToken).ConfigureAwait(false); + + insertCommand = string.Format(SqlConstants.SqlInsert, quotedFullName); + removeCommand = string.Format(SqlConstants.SqlDelete, quotedFullName); + bumpFailureCountCommand = string.Format(SqlConstants.SqlUpdate, quotedFullName); + nextCommand = string.Format(SqlConstants.SqlGetNext, quotedFullName); + fetchCommand = string.Format(SqlConstants.SqlFetch, quotedFullName); + } + + /// + public async Task Next(CancellationToken cancellationToken = default) + { + using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + using (var cmd = new SqlCommand(nextCommand, cn)) + { + await cn.OpenAsync(cancellationToken).ConfigureAwait(false); + var result = (DateTime?)await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); + return result.HasValue ? new DateTimeOffset(result.Value, TimeSpan.Zero) : null; + } + } + + /// + public async Task FetchNextDueTimeout(DateTimeOffset at, CancellationToken cancellationToken = default) + { + DelayedMessage result = null; + using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + using (var cmd = new SqlCommand(fetchCommand, cn)) + { + cmd.Parameters.AddWithValue("@time", at.UtcDateTime); + + await cn.OpenAsync(cancellationToken).ConfigureAwait(false); + using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken).ConfigureAwait(false)) + { + if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + result = new DelayedMessage + { + MessageId = (string)reader[0], + Destination = (string)reader[1], + Time = (DateTime)reader[2], + Headers = (byte[])reader[3], + Body = (byte[])reader[4], + NumberOfRetries = (int)reader[5] + }; + } + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs new file mode 100644 index 00000000..09a9acd4 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs @@ -0,0 +1,52 @@ +namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Data.SqlClient; + + class TimeoutTableCreator + { + public TimeoutTableCreator(CreateSqlConnection createSqlConnection, string tableName) + { + this.tableName = tableName; + this.createSqlConnection = createSqlConnection; + } + + public async Task CreateIfNecessary(CancellationToken cancellationToken = default) + { + var sql = string.Format(SqlConstants.SqlCreateTable, tableName); + using (var connection = await createSqlConnection(cancellationToken).ConfigureAwait(false)) + { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + await Execute(sql, connection, cancellationToken).ConfigureAwait(false); + } + } + + static async Task Execute(string sql, SqlConnection connection, CancellationToken cancellationToken) + { + try + { + using (var transaction = connection.BeginTransaction()) + { + using (var command = new SqlCommand(sql, connection, transaction)) + { + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + transaction.Commit(); + } + } + catch (SqlException e) when (e.Number is 2714 or 1913) //Object already exists + { + //Table creation scripts are based on sys.objects metadata views. + //It looks that these views are not fully transactional and might + //not return information on already created table under heavy load. + //This in turn can result in executing table create or index create queries + //for objects that already exists. These queries will fail with + // 2714 (table) and 1913 (index) error codes. + } + } + + CreateSqlConnection createSqlConnection; + string tableName; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/EndpointInstanceExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/EndpointInstanceExtensions.cs new file mode 100644 index 00000000..cb975c92 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/EndpointInstanceExtensions.cs @@ -0,0 +1,23 @@ +namespace NServiceBus +{ + using NServiceBus.MessagingBridge.Msmq; + using Routing; + + /// + /// Provides MSMQ-specific extensions to routing. + /// + public static class EndpointInstanceExtensions + { + /// + /// Returns an endpoint instance bound to a given machine name. + /// + /// A plain instance. + /// Machine name. + public static EndpointInstance AtMachine(this EndpointInstance instance, string machineName) + { + Guard.AgainstNull(nameof(instance), instance); + Guard.AgainstNullAndEmpty(nameof(machineName), machineName); + return instance.SetProperty("machine", machineName); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/ExceptionExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/ExceptionExtensions.cs new file mode 100644 index 00000000..d30eea61 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ExceptionExtensions.cs @@ -0,0 +1,12 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Threading; + + static class ExceptionExtensions + { +#pragma warning disable PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional + public static bool IsCausedBy(this Exception ex, CancellationToken cancellationToken) => ex is OperationCanceledException && cancellationToken.IsCancellationRequested; +#pragma warning restore PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/ExceptionHeaderHelper.cs b/src/NServiceBus.MessagingBridge.Msmq/ExceptionHeaderHelper.cs new file mode 100644 index 00000000..33bd1b95 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ExceptionHeaderHelper.cs @@ -0,0 +1,60 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections; + using System.Collections.Generic; + + static class ExceptionHeaderHelper + { + public static void SetExceptionHeaders(Dictionary headers, Exception e) + { + headers["NServiceBus.ExceptionInfo.ExceptionType"] = e.GetType().FullName; + + if (e.InnerException != null) + { + headers["NServiceBus.ExceptionInfo.InnerExceptionType"] = e.InnerException.GetType().FullName; + } + + headers["NServiceBus.ExceptionInfo.HelpLink"] = e.HelpLink; + headers["NServiceBus.ExceptionInfo.Message"] = e.GetMessage().Truncate(16384); + headers["NServiceBus.ExceptionInfo.Source"] = e.Source; + headers["NServiceBus.ExceptionInfo.StackTrace"] = e.ToString(); + headers["NServiceBus.TimeOfFailure"] = DateTimeOffsetHelper.ToWireFormattedString(DateTimeOffset.UtcNow); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (e.Data == null) + // ReSharper disable HeuristicUnreachableCode + { + return; + } + // ReSharper restore HeuristicUnreachableCode + foreach (DictionaryEntry entry in e.Data) + { + if (entry.Value == null) + { + continue; + } + headers["NServiceBus.ExceptionInfo.Data." + entry.Key] = entry.Value.ToString(); + } + } + + static string GetMessage(this Exception exception) + { + try + { + return exception.Message; + } + catch (Exception) + { + return $"Could not read Message from exception type '{exception.GetType()}'."; + } + } + + static string Truncate(this string value, int maxLength) => + string.IsNullOrEmpty(value) + ? value + : value.Length <= maxLength + ? value + : value.Substring(0, maxLength); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/FailureRateCircuitBreaker.cs b/src/NServiceBus.MessagingBridge.Msmq/FailureRateCircuitBreaker.cs new file mode 100644 index 00000000..a80e9d0e --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/FailureRateCircuitBreaker.cs @@ -0,0 +1,53 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Logging; + + class FailureRateCircuitBreaker : IDisposable + { + public FailureRateCircuitBreaker(string name, int maximumFailuresPerSecond, Action triggerAction) + { + this.name = name; + this.triggerAction = triggerAction; + maximumFailuresPerThirtySeconds = maximumFailuresPerSecond * 30; + timer = new Timer(_ => FlushHistory(), null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30)); + } + + public void Dispose() + { + timer?.Dispose(); + } + + void FlushHistory() + { + Interlocked.Exchange(ref failureCount, 0); + Logger.InfoFormat("The circuit breaker for {0} is now disarmed", name); + } + + public void Failure(Exception lastException) + { + var result = Interlocked.Increment(ref failureCount); + if (result > maximumFailuresPerThirtySeconds) + { + _ = Task.Run(() => + { + Logger.WarnFormat("The circuit breaker for {0} will now be triggered", name); + triggerAction(lastException); + }); + } + else if (result == 1) + { + Logger.WarnFormat("The circuit breaker for {0} is now in the armed state", name); + } + } + + static readonly ILog Logger = LogManager.GetLogger(); + readonly string name; + readonly Action triggerAction; + readonly int maximumFailuresPerThirtySeconds; + readonly Timer timer; + long failureCount; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/HeaderInfo.cs b/src/NServiceBus.MessagingBridge.Msmq/HeaderInfo.cs new file mode 100644 index 00000000..7912914b --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/HeaderInfo.cs @@ -0,0 +1,21 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + + /// + /// Represents the structure of header information passed in a TransportMessage. + /// + [Serializable] + public class HeaderInfo + { + /// + /// The key used to lookup the value in the header collection. + /// + public string Key { get; set; } + + /// + /// The value stored under the key in the header collection. + /// + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileFeature.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileFeature.cs new file mode 100644 index 00000000..f28e6178 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileFeature.cs @@ -0,0 +1,93 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.IO; + using Features; + using Routing; + using Settings; + + class InstanceMappingFileFeature : Feature + { + public InstanceMappingFileFeature() + { + EnableByDefault(); + + var defaultPath = GetRootedPath(DefaultInstanceMappingFileName); + Uri.TryCreate(defaultPath, UriKind.Absolute, out var defaultUri); + + Defaults(s => + { + s.SetDefault(CheckIntervalSettingsKey, TimeSpan.FromSeconds(30)); + s.SetDefault(PathSettingsKey, defaultUri); + }); + + Prerequisite(c => c.Settings.HasExplicitValue(PathSettingsKey) || File.Exists(defaultPath), "No explicit instance mapping file configuration and default file does not exist."); + } + + protected override void Setup(FeatureConfigurationContext context) + { + var instanceMappingLoader = CreateInstanceMappingLoader(context.Settings); + + var checkInterval = context.Settings.Get(CheckIntervalSettingsKey); + var endpointInstances = context.Settings.Get(); + + var instanceMappingTable = new InstanceMappingFileMonitor(checkInterval, new AsyncTimer(), instanceMappingLoader, endpointInstances); + instanceMappingTable.ReloadData(); + context.RegisterStartupTask(instanceMappingTable); + } + + static string GetRootedPath(string filePath) + { + return Path.IsPathRooted(filePath) + ? filePath + : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath); + } + + IInstanceMappingLoader CreateInstanceMappingLoader(IReadOnlySettings settings) + { + var uri = settings.Get(PathSettingsKey); + + IInstanceMappingLoader loader; + + if (!uri.IsAbsoluteUri || uri.IsFile) + { + var filePath = uri.IsAbsoluteUri + ? uri.LocalPath + : GetRootedPath(uri.OriginalString); + + if (!File.Exists(filePath)) + { + throw new FileNotFoundException("The specified instance mapping file does not exist.", filePath); + } + + loader = new InstanceMappingFileLoader(filePath); + } + else + { + loader = new InstanceMappingUriLoader(uri); + } + + IInstanceMappingValidator validator; + + if (settings.GetOrDefault(StrictSchemaValidationKey)) + { + validator = EmbeddedSchemaInstanceMappingValidator.CreateValidatorV2(); + } + else + { + validator = new FallbackInstanceMappingValidator( + EmbeddedSchemaInstanceMappingValidator.CreateValidatorV2(), + EmbeddedSchemaInstanceMappingValidator.CreateValidatorV1(), + "Validation error parsing instance mapping. Falling back on relaxed parsing method. Instance mapping may contain unsupported attributes." + ); + } + + return new ValidatingInstanceMappingLoader(loader, validator); + } + + public const string CheckIntervalSettingsKey = "InstanceMappingFile.CheckInterval"; + public const string PathSettingsKey = "InstanceMappingFile.Path"; + public const string StrictSchemaValidationKey = "InstanceMappingFile.StrictSchemaValidation"; + const string DefaultInstanceMappingFileName = "instance-mapping.xml"; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileMonitor.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileMonitor.cs new file mode 100644 index 00000000..1ca77c5e --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileMonitor.cs @@ -0,0 +1,112 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Features; + using Logging; + using Routing; + + class InstanceMappingFileMonitor : FeatureStartupTask + { + public InstanceMappingFileMonitor(TimeSpan checkInterval, IAsyncTimer timer, IInstanceMappingLoader loader, EndpointInstances endpointInstances) + { + this.checkInterval = checkInterval; + this.timer = timer; + this.loader = loader; + this.endpointInstances = endpointInstances; + } + + internal Task Start(IMessageSession session, CancellationToken cancellationToken = default) + { + return OnStart(session, cancellationToken); + } + + protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) + { + timer.Start(_ => + { + ReloadData(); + return Task.CompletedTask; + }, checkInterval, ex => log.Error("Unable to update instance mapping information because the instance mapping file couldn't be read.", ex)); + return Task.CompletedTask; + } + + public void ReloadData() + { + try + { + var doc = loader.Load(); + var instances = parser.Parse(doc); + LogChanges(instances); + endpointInstances.AddOrReplaceInstances("InstanceMappingFile", instances); + } + catch (Exception exception) + { + throw new Exception($"An error occurred while reading the endpoint instance mapping ({loader}). See the inner exception for more details.", exception); + } + } + + void LogChanges(List instances) + { + var output = new StringBuilder(); + var hasChanges = false; + + var instancesPerEndpoint = instances.GroupBy(i => i.Endpoint).ToDictionary(g => g.Key, g => g.ToArray()); + + output.AppendLine($"Updating instance mapping table from '{loader}':"); + + foreach (var endpoint in instancesPerEndpoint) + { + if (previousInstances.TryGetValue(endpoint.Key, out var existingInstances)) + { + var newInstances = endpoint.Value.Except(existingInstances).Count(); + var removedInstances = existingInstances.Except(endpoint.Value).Count(); + + if (newInstances > 0 || removedInstances > 0) + { + output.AppendLine($"Updated endpoint '{endpoint.Key}': +{Instances(newInstances)}, -{Instances(removedInstances)}"); + hasChanges = true; + } + } + else + { + output.AppendLine($"Added endpoint '{endpoint.Key}' with {Instances(endpoint.Value.Length)}"); + hasChanges = true; + } + } + + foreach (var removedEndpoint in previousInstances.Keys.Except(instancesPerEndpoint.Keys)) + { + output.AppendLine($"Removed all instances of endpoint '{removedEndpoint}'"); + hasChanges = true; + } + + if (hasChanges) + { + log.Info(output.ToString()); + } + + previousInstances = instancesPerEndpoint; + } + + static string Instances(int count) + { + return count > 1 ? $"{count} instances" : $"{count} instance"; + } + + protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => timer.Stop(cancellationToken); + + TimeSpan checkInterval; + IInstanceMappingLoader loader; + EndpointInstances endpointInstances; + InstanceMappingFileParser parser = new InstanceMappingFileParser(); + IAsyncTimer timer; + IDictionary previousInstances = new Dictionary(0); + + static ILog log = LogManager.GetLogger(typeof(InstanceMappingFileMonitor)); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileParser.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileParser.cs new file mode 100644 index 00000000..1b2dca05 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileParser.cs @@ -0,0 +1,36 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using Routing; + + class InstanceMappingFileParser + { + public List Parse(XDocument document) + { + var root = document.Root; + var endpointElements = root.Descendants("endpoint"); + + var instances = new List(); + + foreach (var e in endpointElements) + { + var endpointName = e.Attribute("name").Value; + + foreach (var i in e.Descendants("instance")) + { + var discriminatorAttribute = i.Attribute("discriminator"); + var discriminator = discriminatorAttribute?.Value; + + var properties = i.Attributes().Where(a => a.Name != "discriminator"); + var propertyDictionary = properties.ToDictionary(a => a.Name.LocalName, a => a.Value); + + instances.Add(new EndpointInstance(endpointName, discriminator, propertyDictionary)); + } + } + + return instances; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileSettings.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileSettings.cs new file mode 100644 index 00000000..bef42c8f --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/InstanceMappingFileSettings.cs @@ -0,0 +1,79 @@ +namespace NServiceBus +{ + using System; + using Configuration.AdvancedExtensibility; + using NServiceBus.MessagingBridge.Msmq; + using Settings; + + /// + /// Allows configuring file-based instance mappings. + /// + public class InstanceMappingFileSettings : ExposeSettings + { + /// + /// Creates new instance of . + /// + public InstanceMappingFileSettings(SettingsHolder settings) + : base(settings) + { + } + + /// + /// Specifies the interval between data refresh attempts. + /// The default value is 30 seconds. + /// + /// Refresh interval. Valid values must be between 1 second and less than 1 day. + public InstanceMappingFileSettings RefreshInterval(TimeSpan refreshInterval) + { + if (refreshInterval < TimeSpan.FromSeconds(1)) + { + throw new ArgumentOutOfRangeException(nameof(refreshInterval), "Value must be at least 1 second."); + } + if (refreshInterval > TimeSpan.FromDays(1)) + { + throw new ArgumentOutOfRangeException(nameof(refreshInterval), "Value must be less than 1 day."); + } + + this.GetSettings().Set(InstanceMappingFileFeature.CheckIntervalSettingsKey, refreshInterval); + return this; + } + + /// + /// Specifies the path and file name for the instance mapping XML. The default is instance-mapping.xml. + /// + /// The relative or absolute file path to the instance mapping XML file. + public InstanceMappingFileSettings FilePath(string filePath) + { + Guard.AgainstNullAndEmpty(nameof(filePath), filePath); + var result = Uri.TryCreate(filePath, UriKind.RelativeOrAbsolute, out var uriPath); + if (!result) + { + throw new ArgumentException("Invalid format", nameof(filePath)); + } + + this.GetSettings().Set(InstanceMappingFileFeature.PathSettingsKey, uriPath); + return this; + } + + /// + /// Specifies the uri for the instance mapping XML. + /// + /// The absolute uri to the instance mapping XML. + public InstanceMappingFileSettings Path(Uri uriPath) + { + Guard.AgainstNull(nameof(uriPath), uriPath); + this.GetSettings().Set(InstanceMappingFileFeature.PathSettingsKey, uriPath); + return this; + } + + /// + /// Turns on strict schema validation for the instance mapping XML. + /// Unknown attribtutes will trigger a schema validation exception. + /// + public InstanceMappingFileSettings EnforceStrictSchemaValidation() + { + this.GetSettings().Set(InstanceMappingFileFeature.StrictSchemaValidationKey, true); + return this; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/IInstanceMappingLoader.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/IInstanceMappingLoader.cs new file mode 100644 index 00000000..c78d6d22 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/IInstanceMappingLoader.cs @@ -0,0 +1,9 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Xml.Linq; + + interface IInstanceMappingLoader + { + XDocument Load(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingFileAccess.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingFileAccess.cs new file mode 100644 index 00000000..3cb6a1b8 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingFileAccess.cs @@ -0,0 +1,30 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.IO; + using System.Xml; + using System.Xml.Linq; + + class InstanceMappingFileLoader : IInstanceMappingLoader + { + string path; + + public InstanceMappingFileLoader(string path) + { + this.path = path; + } + + public XDocument Load() + { + using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = XmlReader.Create(file)) + { + return XDocument.Load(reader); + } + } + + public override string ToString() + { + return path; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingUriLoader.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingUriLoader.cs new file mode 100644 index 00000000..a42735c5 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/InstanceMappingUriLoader.cs @@ -0,0 +1,29 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Xml; + using System.Xml.Linq; + + class InstanceMappingUriLoader : IInstanceMappingLoader + { + Uri path; + + public InstanceMappingUriLoader(Uri path) + { + this.path = path; + } + + public XDocument Load() + { + using (var reader = XmlReader.Create(path.ToString())) + { + return XDocument.Load(reader); + } + } + + public override string ToString() + { + return path.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/ValidatingInstanceMappingLoader.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/ValidatingInstanceMappingLoader.cs new file mode 100644 index 00000000..85068934 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Loaders/ValidatingInstanceMappingLoader.cs @@ -0,0 +1,25 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Xml.Linq; + + class ValidatingInstanceMappingLoader : IInstanceMappingLoader + { + public ValidatingInstanceMappingLoader(IInstanceMappingLoader loader, IInstanceMappingValidator validator) + { + this.loader = loader; + this.validator = validator; + } + + public XDocument Load() + { + var doc = loader.Load(); + validator.Validate(doc); + return doc; + } + + public override string ToString() => loader.ToString(); + + IInstanceMappingLoader loader; + IInstanceMappingValidator validator; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/EmbeddedSchemaInstanceMappingValidator.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/EmbeddedSchemaInstanceMappingValidator.cs new file mode 100644 index 00000000..429efca6 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/EmbeddedSchemaInstanceMappingValidator.cs @@ -0,0 +1,30 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Xml; + using System.Xml.Linq; + using System.Xml.Schema; + + class EmbeddedSchemaInstanceMappingValidator : IInstanceMappingValidator + { + public EmbeddedSchemaInstanceMappingValidator(string resourceName) + { + using (var stream = GetType().Assembly.GetManifestResourceStream(resourceName)) + using (var xmlReader = XmlReader.Create(stream ?? throw new InvalidOperationException("Could not load resource."))) + { + schema = new XmlSchemaSet(); + schema.Add("", xmlReader); + } + } + + public void Validate(XDocument document) + { + document.Validate(schema, null, false); + } + + public static IInstanceMappingValidator CreateValidatorV1() => new EmbeddedSchemaInstanceMappingValidator("NServiceBus.Transport.Msmq.InstanceMapping.Validators.endpoints.xsd"); + public static IInstanceMappingValidator CreateValidatorV2() => new EmbeddedSchemaInstanceMappingValidator("NServiceBus.Transport.Msmq.InstanceMapping.Validators.endpointsV2.xsd"); + + XmlSchemaSet schema; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/FallbackInstanceMappingValidator.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/FallbackInstanceMappingValidator.cs new file mode 100644 index 00000000..0c0225e0 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/FallbackInstanceMappingValidator.cs @@ -0,0 +1,45 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Xml.Linq; + using Logging; + + class FallbackInstanceMappingValidator : IInstanceMappingValidator + { + public FallbackInstanceMappingValidator( + IInstanceMappingValidator preferredValidator, + IInstanceMappingValidator fallbackValidator, + string fallbackWarning) + { + this.preferredValidator = preferredValidator; + this.fallbackValidator = fallbackValidator; + this.fallbackWarning = fallbackWarning; + logWarningOnFallback = true; + } + + public void Validate(XDocument document) + { + try + { + preferredValidator.Validate(document); + logWarningOnFallback = true; + } + catch (Exception ex) + { + if (logWarningOnFallback) + { + Logger.Warn(fallbackWarning, ex); + logWarningOnFallback = false; + } + fallbackValidator.Validate(document); + } + } + + IInstanceMappingValidator preferredValidator; + IInstanceMappingValidator fallbackValidator; + string fallbackWarning; + bool logWarningOnFallback; + + static ILog Logger = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/IInstanceMappingValidator.cs b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/IInstanceMappingValidator.cs new file mode 100644 index 00000000..49044b8d --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/IInstanceMappingValidator.cs @@ -0,0 +1,9 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Xml.Linq; + + interface IInstanceMappingValidator + { + void Validate(XDocument document); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpoints.xsd b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpoints.xsd new file mode 100644 index 00000000..6b8b4fb6 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpoints.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpointsV2.xsd b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpointsV2.xsd new file mode 100644 index 00000000..cdb658dd --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/InstanceMapping/Validators/endpointsV2.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NServiceBus.MessagingBridge.Msmq/JournalOptionExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/JournalOptionExtensions.cs new file mode 100644 index 00000000..6cde0562 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/JournalOptionExtensions.cs @@ -0,0 +1,36 @@ +namespace NServiceBus +{ + using Extensibility; + using NServiceBus.MessagingBridge.Msmq; + using Transport; + + /// + /// Gives users fine grained control over routing via extension methods. + /// + public static class JournalOptionExtensions + { + const string KeyJournaling = "MSMQ.UseJournalQueue"; + + /// + /// Enable or disable MSMQ journaling. + /// + /// Option being extended. + /// Either enable or disabling message journaling. + public static void UseJournalQueue(this ExtendableOptions options, bool enable = true) + { + Guard.AgainstNull(nameof(options), options); + + options.GetDispatchProperties()[KeyJournaling] = enable.ToString(); + } + + internal static bool? ShouldUseJournalQueue(this DispatchProperties dispatchProperties) + { + if (dispatchProperties.TryGetValue(KeyJournaling, out var boolString)) + { + return bool.Parse(boolString); + } + + return null; + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/MessagePump.cs b/src/NServiceBus.MessagingBridge.Msmq/MessagePump.cs new file mode 100644 index 00000000..fa46b60c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MessagePump.cs @@ -0,0 +1,331 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using Logging; + using MSMQ.Messaging; + using Support; + using Transport; + + class MessagePump : IMessageReceiver + { + public MessagePump( + Func receiveStrategyFactory, + TimeSpan messageEnumeratorTimeout, + TransportTransactionMode transactionMode, + bool ignoreIncomingTimeToBeReceivedHeaders, + Action criticalErrorAction, + ReceiveSettings receiveSettings) + { + this.receiveStrategyFactory = receiveStrategyFactory; + this.messageEnumeratorTimeout = messageEnumeratorTimeout; + this.transactionMode = transactionMode; + this.ignoreIncomingTimeToBeReceivedHeaders = ignoreIncomingTimeToBeReceivedHeaders; + this.criticalErrorAction = criticalErrorAction; + this.receiveSettings = receiveSettings; + + ReceiveAddress = MsmqTransportInfrastructure.TranslateAddress(receiveSettings.ReceiveAddress); + } + + public Task Initialize(PushRuntimeSettings limitations, OnMessage onMessage, OnError onError, CancellationToken cancellationToken = default) + { + var inputAddress = MsmqAddress.Parse(ReceiveAddress); + var errorAddress = MsmqAddress.Parse(receiveSettings.ErrorQueue); + + if (!string.Equals(inputAddress.Machine, RuntimeEnvironment.MachineName, + StringComparison.OrdinalIgnoreCase)) + { + throw new Exception( + $"MSMQ Dequeuing can only run against the local machine. Invalid inputQueue name '{receiveSettings.ReceiveAddress}'."); + } + + inputQueue = new MessageQueue(inputAddress.FullPath, false, true, QueueAccessMode.Receive); + errorQueue = new MessageQueue(errorAddress.FullPath, false, true, QueueAccessMode.Send); + + if (transactionMode != TransportTransactionMode.None && !QueueIsTransactional()) + { + throw new ArgumentException( + $"Queue must be transactional if you configure the endpoint to be transactional ({receiveSettings.ReceiveAddress})."); + } + + inputQueue.MessageReadPropertyFilter = DefaultReadPropertyFilter; + + if (receiveSettings.PurgeOnStartup) + { + inputQueue.Purge(); + } + + receiveStrategy = receiveStrategyFactory(transactionMode); + receiveStrategy.Init(inputQueue, ReceiveAddress, errorQueue, onMessage, onError, criticalErrorAction, ignoreIncomingTimeToBeReceivedHeaders); + + maxConcurrency = limitations.MaxConcurrency; + concurrencyLimiter = new SemaphoreSlim(limitations.MaxConcurrency, limitations.MaxConcurrency); + return Task.CompletedTask; + } + + public Task StartReceive(CancellationToken cancellationToken = default) + { + MessageQueue.ClearConnectionCache(); + + messagePumpCancellationTokenSource = new CancellationTokenSource(); + messageProcessingCancellationTokenSource = new CancellationTokenSource(); + + peekCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqPeek", TimeSpan.FromSeconds(30), + ex => criticalErrorAction("Failed to peek " + receiveSettings.ReceiveAddress, ex, messageProcessingCancellationTokenSource.Token)); + + receiveCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqReceive", TimeSpan.FromSeconds(30), + ex => criticalErrorAction("Failed to receive from " + receiveSettings.ReceiveAddress, ex, messageProcessingCancellationTokenSource.Token)); + + // Task.Run() so the call returns immediately instead of waiting for the first await or return down the call stack + // LongRunning is useless combined with async/await + messagePumpTask = Task.Run(() => PumpMessagesAndSwallowExceptions(messagePumpCancellationTokenSource.Token), CancellationToken.None); + + return Task.CompletedTask; + } + + public async Task ChangeConcurrency(PushRuntimeSettings newLimitations, CancellationToken cancellationToken = default) + { + var oldLimiter = concurrencyLimiter; + var oldMaxConcurrency = maxConcurrency; + concurrencyLimiter = new SemaphoreSlim(newLimitations.MaxConcurrency, newLimitations.MaxConcurrency); + maxConcurrency = newLimitations.MaxConcurrency; + + try + { + //Drain and dispose of the old semaphore + while (oldLimiter.CurrentCount != oldMaxConcurrency) + { + await Task.Delay(50, cancellationToken).ConfigureAwait(false); + } + oldLimiter.Dispose(); + } + catch (Exception ex) when (ex.IsCausedBy(cancellationToken)) + { + //Ignore, we are stopping anyway + } + } + + public async Task StopReceive(CancellationToken cancellationToken = default) + { + messagePumpCancellationTokenSource?.Cancel(); + + using (cancellationToken.Register(() => messageProcessingCancellationTokenSource?.Cancel())) + { + await messagePumpTask.ConfigureAwait(false); + + while (concurrencyLimiter.CurrentCount != maxConcurrency) + { + // We are deliberately not forwarding the cancellation token here because + // this loop is our way of waiting for all pending messaging operations + // to participate in cooperative cancellation or not. + // We do not want to rudely abort them because the cancellation token has been canceled. + // This allows us to preserve the same behaviour in v8 as in v7 in that, + // if CancellationToken.None is passed to this method, + // the method will only return when all in flight messages have been processed. + // If, on the other hand, a non-default CancellationToken is passed, + // all message processing operations have the opportunity to + // participate in cooperative cancellation. + // If we ever require a method of stopping the endpoint such that + // all message processing is canceled immediately, + // we can provide that as a separate feature. + await Task.Delay(50, CancellationToken.None) + .ConfigureAwait(false); + } + } + + concurrencyLimiter?.Dispose(); + inputQueue?.Dispose(); + errorQueue?.Dispose(); + peekCircuitBreaker?.Dispose(); + receiveCircuitBreaker?.Dispose(); + messagePumpCancellationTokenSource?.Dispose(); + messageProcessingCancellationTokenSource?.Dispose(); + } + + [DebuggerNonUserCode] + async Task PumpMessagesAndSwallowExceptions(CancellationToken messagePumpCancellationToken) + { + while (!messagePumpCancellationToken.IsCancellationRequested) + { + try + { + try + { + await PumpMessages(messagePumpCancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCausedBy(messagePumpCancellationToken)) + { + Logger.Error("MSMQ Message pump failed", ex); + await peekCircuitBreaker.Failure(ex, messagePumpCancellationToken).ConfigureAwait(false); + } + } + catch (Exception ex) when (ex.IsCausedBy(messagePumpCancellationToken)) + { + // private token, sender is being stopped, log the exception in case the stack trace is ever needed for debugging + Logger.Debug("Operation canceled while stopping message pump.", ex); + break; + } + } + } + + async Task PumpMessages(CancellationToken messagePumpCancellationToken) + { + using (var enumerator = inputQueue.GetMessageEnumerator2()) + { + while (true) + { + messagePumpCancellationToken.ThrowIfCancellationRequested(); + + try + { + //note: .Peek will throw an ex if no message is available. It also turns out that .MoveNext is faster since message isn't read + if (!enumerator.MoveNext(messageEnumeratorTimeout)) + { + continue; + } + + peekCircuitBreaker.Success(); + } + catch (Exception ex) + { + Logger.Warn("MSMQ receive operation failed", ex); + await peekCircuitBreaker.Failure(ex, messagePumpCancellationToken).ConfigureAwait(false); + continue; + } + + messagePumpCancellationToken.ThrowIfCancellationRequested(); + + var localLimiter = concurrencyLimiter; + + await localLimiter.WaitAsync(messagePumpCancellationToken).ConfigureAwait(false); + + _ = Task.Factory.StartNew(state => + { + var (messagePump, limiter, cancellationToken) = ((MessagePump, SemaphoreSlim, CancellationToken))state; + return ReceiveMessagesSwallowExceptionsAndReleaseConcurrencyLimiter(messagePump, limiter, cancellationToken); + }, + (this, localLimiter, messagePumpCancellationToken), // We pass a state to make sure we benefit from lamda delegate caching. See https://github.com/Particular/NServiceBus/issues/3884 + CancellationToken.None, // CancellationToken.None is used here since cancelling the task before it can run can cause the concurrencyLimiter to not be released + TaskCreationOptions.DenyChildAttach, + TaskScheduler.Default) + .Unwrap(); + } + } + } + + // This is static to prevent the method from accessing fields in the pump since that causes variable capturing and cause extra allocations + static async Task ReceiveMessagesSwallowExceptionsAndReleaseConcurrencyLimiter(MessagePump messagePump, SemaphoreSlim localConcurrencyLimiter, CancellationToken messagePumpCancellationToken) + { +#pragma warning disable PS0021 // Highlight when a try block passes multiple cancellation tokens - justification: + // The message processing cancellation token is being used for the receive strategies, + // since we want those only to be canceled when the public token passed to Stop() is canceled. + // The message pump token is being used elsewhere, because we want those operations to be canceled as soon as Stop() is called. + // The catch clauses on the inner try are correctly filtered on the message processing cancellation token and + // the catch clause on the outer try is correctly filtered on the message pump cancellation token. + try +#pragma warning restore PS0021 // Highlight when a try block passes multiple cancellation tokens + { + try + { + // we are switching token here so we need to catch cancellation + await messagePump.receiveStrategy.ReceiveMessage(messagePump.messageProcessingCancellationTokenSource.Token).ConfigureAwait(false); + messagePump.receiveCircuitBreaker.Success(); + } + catch (Exception ex) when (ex.IsCausedBy(messagePump.messageProcessingCancellationTokenSource.Token)) + { + Logger.Warn("MSMQ receive operation canceled", ex); + } + catch (Exception ex) + { + Logger.Warn("MSMQ receive operation failed", ex); + await messagePump.receiveCircuitBreaker.Failure(ex, messagePumpCancellationToken).ConfigureAwait(false); + } + } +#pragma warning disable PS0019 // When catching System.Exception, cancellation needs to be properly accounted for - justification: see PS0021 suppression justification + catch (Exception ex) when (ex.IsCausedBy(messagePumpCancellationToken)) +#pragma warning restore PS0019 // When catching System.Exception, cancellation needs to be properly accounted for + { + // private token, sender is being stopped, log the exception in case the stack trace is ever needed for debugging + Logger.Debug("Operation canceled while stopping message pump.", ex); + } + finally + { + localConcurrencyLimiter.Release(); + } + } + + bool QueueIsTransactional() + { + try + { + return inputQueue.Transactional; + } + catch (MessageQueueException msmqEx) + { + var error = + $"There is a problem with the input inputQueue: {inputQueue.Path}. See the enclosed exception for details."; + if (msmqEx.MessageQueueErrorCode == MessageQueueErrorCode.QueueNotFound) + { + error = + $"The queue {inputQueue.Path} does not exist. Run the CreateQueues.ps1 script included in the project output, or enable queue creation on startup using EndpointConfiguration.EnableInstallers()."; + } + + if (msmqEx.MessageQueueErrorCode == MessageQueueErrorCode.AccessDenied) + { + error = + $"Access denied for the queue {inputQueue.Path}. Ensure the user has Get Properties permission on the queue."; + } + + throw new Exception(error, msmqEx); + } + catch (Exception ex) + { + var error = + $"There is a problem with the input inputQueue: {inputQueue.Path}. See the enclosed exception for details."; + throw new Exception(error, ex); + } + } + + public ISubscriptionManager Subscriptions => null; + + public string Id => receiveSettings.Id; + + public string ReceiveAddress { get; } + + CancellationTokenSource messagePumpCancellationTokenSource; + CancellationTokenSource messageProcessingCancellationTokenSource; + int maxConcurrency; + volatile SemaphoreSlim concurrencyLimiter; + MessageQueue errorQueue; + MessageQueue inputQueue; + + Task messagePumpTask; + + ReceiveStrategy receiveStrategy; + + RepeatedFailuresOverTimeCircuitBreaker peekCircuitBreaker; + RepeatedFailuresOverTimeCircuitBreaker receiveCircuitBreaker; + Func receiveStrategyFactory; + TimeSpan messageEnumeratorTimeout; + readonly TransportTransactionMode transactionMode; + readonly bool ignoreIncomingTimeToBeReceivedHeaders; + readonly Action criticalErrorAction; + readonly ReceiveSettings receiveSettings; + + static readonly ILog Logger = LogManager.GetLogger(); + + static readonly MessagePropertyFilter DefaultReadPropertyFilter = new MessagePropertyFilter + { + Body = true, + TimeToBeReceived = true, + Recoverable = true, + Id = true, + ResponseQueue = true, + CorrelationId = true, + Extension = true, + AppSpecific = true + }; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/MessageQueueExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/MessageQueueExtensions.cs new file mode 100644 index 00000000..bdd9ff24 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MessageQueueExtensions.cs @@ -0,0 +1,199 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.ComponentModel; + using System.Runtime.InteropServices; + using System.Security.Principal; + using MSMQ.Messaging; + + /// + /// Reads the Access Control Entries (ACE) from an MSMQ queue. + /// + /// + /// There is no managed API for reading the queue permissions, this has to be done via P/Invoke. by calling + /// MQGetQueueSecurity API. + /// See http://stackoverflow.com/questions/10177255/how-to-get-the-current-permissions-for-an-msmq-private-queue + /// + static class MessageQueueExtensions + { + [DllImport("mqrt.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int MQGetQueueSecurity(string formatName, int SecurityInformation, IntPtr SecurityDescriptor, int length, out int lengthNeeded); + + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool GetSecurityDescriptorDacl(IntPtr pSD, out bool daclPresent, out IntPtr pDacl, out bool daclDefaulted); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool GetAclInformation(IntPtr pAcl, ref ACL_SIZE_INFORMATION pAclInformation, uint nAclInformationLength, ACL_INFORMATION_CLASS dwAclInformationClass); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int GetAce(IntPtr aclPtr, int aceIndex, out IntPtr acePtr); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int GetLengthSid(IntPtr pSID); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool ConvertSidToStringSid([MarshalAs(UnmanagedType.LPArray)] byte[] pSID, out IntPtr ptrSid); + + const string PREFIX_FORMAT_NAME = "FORMATNAME:"; + const int DACL_SECURITY_INFORMATION = 4; + const int MQ_ERROR_SECURITY_DESCRIPTOR_TOO_SMALL = unchecked((int)0xc00e0023); + const int MQ_OK = 0; + static bool administerGranted; + + //Security constants + + // the following constants taken from MessageQueue.cs (see http://referencesource.microsoft.com/#System.Messaging/System/Messaging/MessageQueue.cs) + [StructLayout(LayoutKind.Sequential)] + struct ACE_HEADER + { + public byte AceType; + public byte AceFlags; + public short AceSize; + } + + [StructLayout(LayoutKind.Sequential)] + struct ACCESS_ALLOWED_ACE + { + public ACE_HEADER Header; + public uint Mask; + public int SidStart; + } + + [StructLayout(LayoutKind.Sequential)] + struct ACL_SIZE_INFORMATION + { + public uint AceCount; + public uint AclBytesInUse; + public uint AclBytesFree; + } + + enum ACL_INFORMATION_CLASS + { + AclRevisionInformation = 1, + AclSizeInformation + } + + public static bool TryGetPermissions(this MessageQueue queue, string user, out MessageQueueAccessRights? rights, out AccessControlEntryType? accessType) + { + if (!administerGranted) + { + var permission = new MessageQueuePermission(MessageQueuePermissionAccess.Administer, PREFIX_FORMAT_NAME + queue.FormatName); + permission.Demand(); + + administerGranted = true; + } + + var sid = GetSidForUser(user); + + try + { + rights = GetPermissions(queue.FormatName, sid, out accessType); + return true; + } + catch + { + rights = null; + accessType = null; + return false; + } + } + + static MessageQueueAccessRights GetPermissions(string formatName, string sid, out AccessControlEntryType? aceType) + { + var SecurityDescriptor = new byte[100]; + + var sdHandle = GCHandle.Alloc(SecurityDescriptor, GCHandleType.Pinned); + try + { + var mqResult = MQGetQueueSecurity(formatName, + DACL_SECURITY_INFORMATION, + sdHandle.AddrOfPinnedObject(), + SecurityDescriptor.Length, + out var lengthNeeded); + + if (mqResult == MQ_ERROR_SECURITY_DESCRIPTOR_TOO_SMALL) + { + sdHandle.Free(); + SecurityDescriptor = new byte[lengthNeeded]; + sdHandle = GCHandle.Alloc(SecurityDescriptor, GCHandleType.Pinned); + mqResult = MQGetQueueSecurity(formatName, + DACL_SECURITY_INFORMATION, + sdHandle.AddrOfPinnedObject(), + SecurityDescriptor.Length, + out lengthNeeded); + } + + if (mqResult != MQ_OK) + { + throw new Exception($"Unable to read the security descriptor of queue [{formatName}]"); + } + + var success = GetSecurityDescriptorDacl(sdHandle.AddrOfPinnedObject(), + out _, + out var pDacl, + out _); + + if (!success) + { + throw new Win32Exception(); + } + + var allowedAce = GetAce(pDacl, sid); + + // The ACE_HEADER information contains the access control information as to whether it is allowed or denied. + // In Interop, this value is a byte and can be any of the values defined in here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa374919(v=vs.85).aspx + // If the value is 0, then it equates to Allow. If the value is 1, then it equates to Deny. + // However, you can't cast it directly to the AccessControlEntryType enumeration, as a value of 1 in the enumeration is + // defined to be Allow!! Hence a translation is required. + aceType = allowedAce.Header.AceType switch + { + 0 => (AccessControlEntryType?)AccessControlEntryType.Allow, + 1 => (AccessControlEntryType?)AccessControlEntryType.Deny, + _ => null, + }; + return (MessageQueueAccessRights)allowedAce.Mask; + } + finally + { + if (sdHandle.IsAllocated) + { + sdHandle.Free(); + } + } + } + + static string GetSidForUser(string username) + { + var account = new NTAccount(username); + var sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); + + return sid.ToString(); + } + + static ACCESS_ALLOWED_ACE GetAce(IntPtr pDacl, string sid) + { + var AclSize = new ACL_SIZE_INFORMATION(); + GetAclInformation(pDacl, ref AclSize, (uint)Marshal.SizeOf(typeof(ACL_SIZE_INFORMATION)), ACL_INFORMATION_CLASS.AclSizeInformation); + + for (var i = 0; i < AclSize.AceCount; i++) + { + GetAce(pDacl, i, out var pAce); + var ace = (ACCESS_ALLOWED_ACE)Marshal.PtrToStructure(pAce, typeof(ACCESS_ALLOWED_ACE)); + var iter = (IntPtr)(pAce + (long)Marshal.OffsetOf(typeof(ACCESS_ALLOWED_ACE), "SidStart")); + var size = GetLengthSid(iter); + var bSID = new byte[size]; + Marshal.Copy(iter, bSID, 0, size); + ConvertSidToStringSid(bSID, out var ptrSid); + + var strSID = Marshal.PtrToStringAuto(ptrSid); + + if (strSID == sid) + { + return ace; + } + } + + throw new Exception($"No ACE for SID {sid} found in security descriptor"); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqAddress.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqAddress.cs new file mode 100644 index 00000000..23b8f1d7 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqAddress.cs @@ -0,0 +1,135 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Linq; + using System.Net; + using Support; + + struct MsmqAddress + { + public readonly string Queue; + + public readonly string Machine; + + public static MsmqAddress Parse(string address) + { + Guard.AgainstNullAndEmpty(nameof(address), address); + + var split = address.Split('@'); + + if (split.Length > 2) + { + var message = $"Address contains multiple @ characters. Address supplied: '{address}'"; + throw new ArgumentException(message, nameof(address)); + } + + var queue = split[0]; + if (string.IsNullOrWhiteSpace(queue)) + { + var message = $"Empty queue part of address. Address supplied: '{address}'"; + throw new ArgumentException(message, nameof(address)); + } + + string machineName; + if (split.Length == 2) + { + machineName = split[1]; + if (string.IsNullOrWhiteSpace(machineName)) + { + var message = $"Empty machine part of address. Address supplied: '{address}'"; + throw new ArgumentException(message, nameof(address)); + } + machineName = ApplyLocalMachineConventions(machineName); + } + else + { + machineName = RuntimeEnvironment.MachineName; + } + + return new MsmqAddress(queue, machineName); + } + + public bool IsRemote() => Machine != RuntimeEnvironment.MachineName && IsLocalIpAddress(Machine) != true; + + static bool? IsLocalIpAddress(string hostName) + { + if (string.IsNullOrWhiteSpace(hostName)) + { + return null; + } + try + { + var hostIPs = Dns.GetHostAddresses(hostName); + var localIPs = Dns.GetHostAddresses(Dns.GetHostName()); + + if (hostIPs.Any(hostIp => IPAddress.IsLoopback(hostIp) || localIPs.Contains(hostIp))) + { + return true; + } + } + catch + { + return null; + } + return false; + } + + static string ApplyLocalMachineConventions(string machineName) + { + if ( + machineName == "." || + machineName.ToLower() == "localhost" || + (IPAddress.TryParse(machineName, out var address) && IPAddress.IsLoopback(address)) + ) + { + return RuntimeEnvironment.MachineName; + } + return machineName; + } + + public MsmqAddress(string queueName, string machineName) + { + Queue = queueName; + Machine = machineName; + } + + public MsmqAddress MakeCompatibleWith(MsmqAddress other, Func ipLookup) + { + if (IPAddress.TryParse(other.Machine, out _) && !IPAddress.TryParse(Machine, out _)) + { + return new MsmqAddress(Queue, ipLookup(Machine)); + } + return this; + } + + public string FullPath + { + get + { + if (IPAddress.TryParse(Machine, out _)) + { + return PREFIX_TCP + PathWithoutPrefix; + } + return PREFIX + PathWithoutPrefix; + } + } + + public string PathWithoutPrefix => Machine + PRIVATE + Queue; + + public override string ToString() + { + return Queue + "@" + Machine; + } + + public bool IsEmpty() + { + return string.IsNullOrEmpty(Queue) && string.IsNullOrEmpty(Machine); + } + + const string DIRECTPREFIX_TCP = "DIRECT=TCP:"; + const string PREFIX_TCP = "FormatName:" + DIRECTPREFIX_TCP; + const string PREFIX = "FormatName:" + DIRECTPREFIX; + const string DIRECTPREFIX = "DIRECT=OS:"; + const string PRIVATE = "\\private$\\"; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs new file mode 100644 index 00000000..0df62ff7 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs @@ -0,0 +1,195 @@ +namespace NServiceBus +{ + using System; + using System.Collections.Generic; + using System.Reflection.Emit; + using System.Transactions; + using Configuration.AdvancedExtensibility; + using NServiceBus.MessagingBridge.Msmq; + using Routing; + + /// + /// Adds extensions methods to for configuration purposes. + /// + public static partial class MsmqConfigurationExtensions + { + /// + /// Configures the endpoint to use MSMQ to send and receive messages. + /// + public static TransportExtensions UseTransport(this EndpointConfiguration endpointConfiguration) + where TTransport : MsmqTransport + { + var msmqTransport = new MsmqTransport(); + var routingSettings = endpointConfiguration.UseTransport(msmqTransport); + return new TransportExtensions(msmqTransport, routingSettings); + } + + /// + /// Sets a distribution strategy for a given endpoint. + /// + /// MSMQ Transport configuration object. + /// The instance of a distribution strategy. + public static void SetMessageDistributionStrategy(this RoutingSettings config, DistributionStrategy distributionStrategy) + { + Guard.AgainstNull(nameof(config), config); + Guard.AgainstNull(nameof(distributionStrategy), distributionStrategy); + + config.GetSettings().GetOrCreate>().Add(distributionStrategy); + } + + /// + /// Returns the configuration options for the file based instance mapping file. + /// + /// MSMQ Transport configuration object. + public static InstanceMappingFileSettings InstanceMappingFile(this RoutingSettings config) + { + Guard.AgainstNull(nameof(config), config); + return new InstanceMappingFileSettings(config.GetSettings()); + } + + /// + /// Set a delegate to use for applying the property when sending a message. + /// + /// + /// This delegate will be used for all valid messages sent via MSMQ. + /// This includes, not just standard messages, but also Audits, Errors and all control messages. + /// In some cases it may be useful to use the key to determine if a message is + /// a control message. + /// The only exception to this rule is received messages with corrupted headers. These messages will be forwarded to the + /// error queue with no label applied. + /// + public static TransportExtensions ApplyLabelToMessages( + this TransportExtensions transport, + Func, string> labelGenerator) + { + transport.Transport.ApplyCustomLabelToOutgoingMessages = labelGenerator; + return transport; + } + + /// + /// Allows to change the transaction isolation level and timeout for the `TransactionScope` used to receive messages. + /// + /// + /// If not specified the default transaction timeout of the machine will be used and the isolation level will be set to + /// . + /// + /// The transport settings to configure. + /// Transaction timeout duration. + /// Transaction isolation level. + public static TransportExtensions TransactionScopeOptions( + this TransportExtensions transport, + TimeSpan? timeout = null, + IsolationLevel? isolationLevel = null) + { + transport.Transport.ConfigureTransactionScope(timeout, isolationLevel); + return transport; + } + + /// + /// Moves messages that have exceeded their TimeToBeReceived to the dead letter queue instead of discarding them. + /// + public static TransportExtensions UseDeadLetterQueueForMessagesWithTimeToBeReceived( + this TransportExtensions transport) + { + transport.Transport.UseDeadLetterQueueForMessagesWithTimeToBeReceived = true; + return transport; + } + + /// + /// Disables the automatic queue creation when installers are enabled using `EndpointConfiguration.EnableInstallers()`. + /// + /// + /// With installers enabled, required queues will be created automatically at startup.While this may be convenient for development, + /// we instead recommend that queues are created as part of deployment using the CreateQueues.ps1 script included in the NuGet package. + /// The installers might still need to be enabled to fulfill the installation needs of other components, but this method allows + /// scripts to be used for queue creation instead. + /// + public static TransportExtensions DisableInstaller(this TransportExtensions transport) + { + transport.Transport.CreateQueues = false; + return transport; + } + + /// + /// This setting should be used with caution. It disables the storing of undeliverable messages + /// in the dead letter queue. Therefore this setting must only be used where loss of messages + /// is an acceptable scenario. + /// + public static TransportExtensions DisableDeadLetterQueueing(this TransportExtensions transport) + { + transport.Transport.UseDeadLetterQueue = false; + return transport; + } + + /// + /// Instructs MSMQ to cache connections to a remote queue and re-use them + /// as needed instead of creating new connections for each message. + /// Turning connection caching off will negatively impact the message throughput in + /// most scenarios. + /// + public static TransportExtensions DisableConnectionCachingForSends(this TransportExtensions transport) + { + transport.Transport.UseConnectionCache = false; + return transport; + } + + /// + /// This setting should be used with caution. As the queues are not transactional, any message that has + /// an exception during processing will not be rolled back to the queue. Therefore this setting must only + /// be used where loss of messages is an acceptable scenario. + /// + public static TransportExtensions UseNonTransactionalQueues(this TransportExtensions transport) + { + transport.Transport.UseTransactionalQueues = false; + return transport; + } + + /// + /// Enables the use of journaling messages. Stores a copy of every message received in the journal queue. + /// Should be used ONLY when debugging as it can + /// potentially use up the MSMQ journal storage quota based on the message volume. + /// + public static TransportExtensions EnableJournaling(this TransportExtensions transport) + { + transport.Transport.UseJournalQueue = true; + return transport; + } + + /// + /// Overrides the Time-To-Reach-Queue (TTRQ) timespan. The default value if not set is Message.InfiniteTimeout + /// + public static TransportExtensions TimeToReachQueue(this TransportExtensions transport, TimeSpan timeToReachQueue) + { + transport.Transport.TimeToReachQueue = timeToReachQueue; + return transport; + } + + /// + /// Disables native Time-To-Be-Received (TTBR) when combined with transactions. + /// + public static TransportExtensions DisableNativeTimeToBeReceivedInTransactions(this TransportExtensions transport) + { + transport.Transport.UseNonNativeTimeToBeReceivedInTransactions = true; + return transport; + } + + /// + /// Configures native delayed delivery. + /// + public static DelayedDeliverySettings NativeDelayedDelivery(this TransportExtensions config, IDelayedMessageStore delayedMessageStore) + { + Guard.AgainstNull(nameof(delayedMessageStore), delayedMessageStore); + config.Transport.DelayedDelivery = new DelayedDeliverySettings(delayedMessageStore); + return config.Transport.DelayedDelivery; + } + + /// + /// Ignore incoming Time-To-Be-Received (TTBR) headers. By default an expired TTBR header will result in the message to be discarded. + /// + public static TransportExtensions IgnoreIncomingTimeToBeReceivedHeaders(this TransportExtensions transport) + { + transport.Transport.IgnoreIncomingTimeToBeReceivedHeaders = true; + return transport; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqFailureInfoStorage.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqFailureInfoStorage.cs new file mode 100644 index 00000000..c97d9f55 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqFailureInfoStorage.cs @@ -0,0 +1,109 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + using System.Runtime.ExceptionServices; + using NServiceBus.Extensibility; + + // The data structure has fixed maximum size. When the data structure reaches its maximum size, + // the least recently used (LRU) message processing failure is removed from the storage. + class MsmqFailureInfoStorage + { + public MsmqFailureInfoStorage(int maxElements) + { + this.maxElements = maxElements; + } + + public void RecordFailureInfoForMessage(string messageId, Exception exception, ContextBag context) + { + lock (lockObject) + { + if (failureInfoPerMessage.TryGetValue(messageId, out var node)) + { + // We have seen this message before, just update the counter and store exception. + node.FailureInfo = new ProcessingFailureInfo(node.FailureInfo.NumberOfProcessingAttempts + 1, ExceptionDispatchInfo.Capture(exception), context); + + // Maintain invariant: leastRecentlyUsedMessages.First contains the LRU item. + leastRecentlyUsedMessages.Remove(node.LeastRecentlyUsedEntry); + leastRecentlyUsedMessages.AddLast(node.LeastRecentlyUsedEntry); + } + else + { + if (failureInfoPerMessage.Count == maxElements) + { + // We have reached the maximum allowed capacity. Remove the LRU item. + var leastRecentlyUsedEntry = leastRecentlyUsedMessages.First; + failureInfoPerMessage.Remove(leastRecentlyUsedEntry.Value); + leastRecentlyUsedMessages.RemoveFirst(); + } + + var newNode = new FailureInfoNode( + messageId, + new ProcessingFailureInfo(1, ExceptionDispatchInfo.Capture(exception), context)); + + failureInfoPerMessage[messageId] = newNode; + + // Maintain invariant: leastRecentlyUsedMessages.First contains the LRU item. + leastRecentlyUsedMessages.AddLast(newNode.LeastRecentlyUsedEntry); + } + } + } + + public bool TryGetFailureInfoForMessage(string messageId, out ProcessingFailureInfo processingFailureInfo) + { + lock (lockObject) + { + if (!failureInfoPerMessage.TryGetValue(messageId, out var node)) + { + processingFailureInfo = null; + return false; + } + processingFailureInfo = node.FailureInfo; + + return true; + } + } + + public void ClearFailureInfoForMessage(string messageId) + { + lock (lockObject) + { + failureInfoPerMessage.Remove(messageId); + leastRecentlyUsedMessages.Remove(messageId); + } + } + + Dictionary failureInfoPerMessage = new Dictionary(); + LinkedList leastRecentlyUsedMessages = new LinkedList(); + object lockObject = new object(); + + int maxElements; + + class FailureInfoNode + { + public FailureInfoNode(string messageId, ProcessingFailureInfo failureInfo) + { + FailureInfo = failureInfo; + LeastRecentlyUsedEntry = new LinkedListNode(messageId); + } + + public ProcessingFailureInfo FailureInfo { get; set; } + public LinkedListNode LeastRecentlyUsedEntry { get; } + } + + public class ProcessingFailureInfo + { + public ProcessingFailureInfo(int numberOfProcessingAttempts, ExceptionDispatchInfo exceptionDispatchInfo, ContextBag context) + { + NumberOfProcessingAttempts = numberOfProcessingAttempts; + ExceptionDispatchInfo = exceptionDispatchInfo; + Context = context; + } + + public int NumberOfProcessingAttempts { get; } + public Exception Exception => ExceptionDispatchInfo.SourceException; + public ContextBag Context { get; } + ExceptionDispatchInfo ExceptionDispatchInfo { get; } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs new file mode 100644 index 00000000..528cc26c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs @@ -0,0 +1,335 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using System.Transactions; + using Performance.TimeToBeReceived; + using Transport; + using Unicast.Queuing; + + class MsmqMessageDispatcher : IMessageDispatcher + { + readonly MsmqTransport transportSettings; + readonly string timeoutsQueue; + readonly Action onSendCallback; + + public MsmqMessageDispatcher(MsmqTransport transportSettings, string timeoutsQueue, Action onSendCallback = null) + { + this.transportSettings = transportSettings; + this.timeoutsQueue = timeoutsQueue; + this.onSendCallback = onSendCallback; + } + + public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, + CancellationToken cancellationToken = default) + { + Guard.AgainstNull(nameof(outgoingMessages), outgoingMessages); + + if (outgoingMessages.MulticastTransportOperations.Any()) + { + throw new Exception("The MSMQ transport only supports unicast transport operations."); + } + + foreach (var unicastTransportOperation in outgoingMessages.UnicastTransportOperations) + { + ExecuteTransportOperation(transaction, unicastTransportOperation); + } + + return Task.CompletedTask; + } + + public void DispatchDelayedMessage(string id, byte[] extension, ReadOnlyMemory body, string destination, TransportTransaction transportTransaction) + { + var headersAndProperties = MsmqUtilities.DeserializeMessageHeaders(extension); + var headers = new Dictionary(); + var properties = new DispatchProperties(); + + foreach (var kvp in headersAndProperties) + { + if (kvp.Key.StartsWith(MsmqUtilities.PropertyHeaderPrefix)) + { + properties[kvp.Key] = kvp.Value; + } + else + { + headers[kvp.Key] = kvp.Value; + } + } + + var request = new OutgoingMessage(id, headers, body); + + SendToDestination(transportTransaction, new UnicastTransportOperation(request, destination, properties)); + } + + public const string TimeoutDestination = "NServiceBus.Timeout.Destination"; + public const string TimeoutAt = "NServiceBus.Timeout.Expire"; + + void ExecuteTransportOperation(TransportTransaction transaction, UnicastTransportOperation transportOperation) + { + bool isDelayedMessage = transportOperation.Properties.DelayDeliveryWith != null || transportOperation.Properties.DoNotDeliverBefore != null; + + if (isDelayedMessage) + { + SendToDelayedDeliveryQueue(transaction, transportOperation); + } + else + { + SendToDestination(transaction, transportOperation); + } + } + + void SendToDelayedDeliveryQueue(TransportTransaction transaction, UnicastTransportOperation transportOperation) + { + onSendCallback?.Invoke(transaction, transportOperation); + var message = transportOperation.Message; + + transportOperation.Properties[TimeoutDestination] = transportOperation.Destination; + DateTimeOffset deliverAt; + + if (transportOperation.Properties.DelayDeliveryWith != null) + { + deliverAt = DateTimeOffset.UtcNow + transportOperation.Properties.DelayDeliveryWith.Delay; + } + else // transportOperation.Properties.DoNotDeliverBefore != null + { + deliverAt = transportOperation.Properties.DoNotDeliverBefore.At; + } + + transportOperation.Properties[TimeoutDestination] = transportOperation.Destination; + transportOperation.Properties[TimeoutAt] = DateTimeOffsetHelper.ToWireFormattedString(deliverAt); + + var destinationAddress = MsmqAddress.Parse(timeoutsQueue); + + foreach (var kvp in transportOperation.Properties) + { + //Use add to force exception if user adds a custom header that has the same name as the prefix + property name + transportOperation.Message.Headers.Add($"{MsmqUtilities.PropertyHeaderPrefix}{kvp.Key}", kvp.Value); + } + + try + { + using (var q = new MessageQueue(destinationAddress.FullPath, false, transportSettings.UseConnectionCache, QueueAccessMode.Send)) + { + using (var toSend = MsmqUtilities.Convert(message)) + { + toSend.UseDeadLetterQueue = true; //Always used DLQ for delayed messages + toSend.UseJournalQueue = transportSettings.UseJournalQueue; + + if (transportOperation.RequiredDispatchConsistency == DispatchConsistency.Isolated) + { + q.Send(toSend, string.Empty, GetIsolatedTransactionType()); + return; + } + + if (TryGetNativeTransaction(transaction, out var activeTransaction)) + { + q.Send(toSend, string.Empty, activeTransaction); + return; + } + + q.Send(toSend, string.Empty, GetTransactionTypeForSend()); + } + } + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.QueueNotFound) + { + throw new QueueNotFoundException(timeoutsQueue, $"Failed to send the message to the local delayed delivery queue [{timeoutsQueue}]: queue does not exist.", ex); + } + + ThrowFailedToSendException(timeoutsQueue, ex); + } + catch (Exception ex) + { + ThrowFailedToSendException(timeoutsQueue, ex); + } + } + + void SendToDestination(TransportTransaction transaction, UnicastTransportOperation transportOperation) + { + onSendCallback?.Invoke(transaction, transportOperation); + var message = transportOperation.Message; + var destinationAddress = MsmqAddress.Parse(transportOperation.Destination); + + var dispatchProperties = transportOperation.Properties; + + if (IsCombiningTimeToBeReceivedWithTransactions( + transaction, + transportOperation.RequiredDispatchConsistency, + dispatchProperties)) + { + if (transportSettings.UseNonNativeTimeToBeReceivedInTransactions) + { + dispatchProperties.DiscardIfNotReceivedBefore = + new DiscardIfNotReceivedBefore(Message.InfiniteTimeout); + } + else + { + throw new Exception( + $"Failed to send message to address: {destinationAddress.Queue}@{destinationAddress.Machine}. Sending messages with a custom TimeToBeReceived is not supported on transactional MSMQ."); + } + } + + try + { + using (var q = new MessageQueue(destinationAddress.FullPath, false, transportSettings.UseConnectionCache, QueueAccessMode.Send)) + { + using (var toSend = MsmqUtilities.Convert(message)) + { + if (dispatchProperties.DiscardIfNotReceivedBefore?.MaxTime < MessageQueue.InfiniteTimeout) + { + toSend.TimeToBeReceived = dispatchProperties.DiscardIfNotReceivedBefore.MaxTime; + } + + var useDeadLetterQueue = dispatchProperties.ShouldUseDeadLetterQueue(); + if (useDeadLetterQueue.HasValue) + { + toSend.UseDeadLetterQueue = useDeadLetterQueue.Value; + } + else + { + var ttbrRequested = toSend.TimeToBeReceived < MessageQueue.InfiniteTimeout; + toSend.UseDeadLetterQueue = ttbrRequested + ? transportSettings.UseDeadLetterQueueForMessagesWithTimeToBeReceived + : transportSettings.UseDeadLetterQueue; + } + + toSend.UseJournalQueue = dispatchProperties.ShouldUseJournalQueue() ?? + transportSettings.UseJournalQueue; + + toSend.TimeToReachQueue = transportSettings.TimeToReachQueue; + + if (message.Headers.TryGetValue(Headers.ReplyToAddress, out var replyToAddress)) + { + toSend.ResponseQueue = new MessageQueue(MsmqAddress.Parse(replyToAddress).FullPath); + } + + var label = GetLabel(message); + + if (transportOperation.RequiredDispatchConsistency == DispatchConsistency.Isolated) + { + q.Send(toSend, label, GetIsolatedTransactionType()); + return; + } + + if (TryGetNativeTransaction(transaction, out var activeTransaction)) + { + q.Send(toSend, label, activeTransaction); + return; + } + + q.Send(toSend, label, GetTransactionTypeForSend()); + } + } + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.QueueNotFound) + { + var msg = transportOperation.Destination == null + ? "Failed to send message. Target address is null." + : $"Failed to send message to address: [{transportOperation.Destination}]"; + + throw new QueueNotFoundException(transportOperation.Destination, msg, ex); + } + + ThrowFailedToSendException(transportOperation.Destination, ex); + } + catch (Exception ex) + { + ThrowFailedToSendException(transportOperation.Destination, ex); + } + } + + bool IsCombiningTimeToBeReceivedWithTransactions(TransportTransaction transaction, + DispatchConsistency requiredDispatchConsistency, DispatchProperties dispatchProperties) + { + if (!transportSettings.UseTransactionalQueues) + { + return false; + } + + if (requiredDispatchConsistency == DispatchConsistency.Isolated) + { + return false; + } + + var timeToBeReceivedRequested = + dispatchProperties.DiscardIfNotReceivedBefore?.MaxTime < MessageQueue.InfiniteTimeout; + + if (!timeToBeReceivedRequested) + { + return false; + } + + if (Transaction.Current != null) + { + return true; + } + + + return TryGetNativeTransaction(transaction, out _); + } + + static bool TryGetNativeTransaction(TransportTransaction transportTransaction, + out MessageQueueTransaction transaction) + { + return transportTransaction.TryGet(out transaction); + } + + MessageQueueTransactionType GetIsolatedTransactionType() + { + return transportSettings.UseTransactionalQueues + ? MessageQueueTransactionType.Single + : MessageQueueTransactionType.None; + } + + string GetLabel(OutgoingMessage message) + { + var messageLabel = + transportSettings.ApplyCustomLabelToOutgoingMessages( + new ReadOnlyDictionary(message.Headers)); + if (messageLabel == null) + { + throw new Exception( + "MSMQ label convention returned a null. Either return a valid value or a String.Empty to indicate 'no value'."); + } + + if (messageLabel.Length > 240) + { + throw new Exception( + "MSMQ label convention returned a value longer than 240 characters. This is not supported."); + } + + return messageLabel; + } + + static void ThrowFailedToSendException(string address, Exception ex) + { + if (address == null) + { + throw new Exception("Failed to send message.", ex); + } + + throw new Exception($"Failed to send message to address: {address}", ex); + } + + MessageQueueTransactionType GetTransactionTypeForSend() + { + if (!transportSettings.UseTransactionalQueues) + { + return MessageQueueTransactionType.None; + } + + return Transaction.Current != null + ? MessageQueueTransactionType.Automatic + : MessageQueueTransactionType.Single; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqQueueCreator.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqQueueCreator.cs new file mode 100644 index 00000000..e9797e99 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqQueueCreator.cs @@ -0,0 +1,77 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Collections.Generic; + using MSMQ.Messaging; + using System.Security.Principal; + using Logging; + + class MsmqQueueCreator + { + public MsmqQueueCreator(bool useTransactionalQueues, string installerUser) + { + this.useTransactionalQueues = useTransactionalQueues; + this.installerUser = installerUser; + } + + public void CreateQueueIfNecessary(IEnumerable queues) + { + foreach (var queue in queues) + { + CreateQueueIfNecessary(queue); + } + } + + void CreateQueueIfNecessary(string address) + { + var msmqAddress = MsmqAddress.Parse(address); + + Logger.Debug($"Creating '{address}' if needed."); + + if (msmqAddress.IsRemote()) + { + Logger.Info($"'{address}' is a remote queue and won't be created"); + return; + } + + var queuePath = msmqAddress.PathWithoutPrefix; + + if (MessageQueue.Exists(queuePath)) + { + Logger.Debug($"'{address}' already exists"); + return; + } + + try + { + using (var queue = MessageQueue.Create(queuePath, useTransactionalQueues)) + { + Logger.Debug($"Created queue, path: [{queuePath}], identity: [{installerUser}], transactional: [{useTransactionalQueues}]"); + + try + { + queue.SetPermissions(installerUser, MessageQueueAccessRights.WriteMessage); + queue.SetPermissions(installerUser, MessageQueueAccessRights.ReceiveMessage); + queue.SetPermissions(installerUser, MessageQueueAccessRights.PeekMessage); + queue.SetPermissions(installerUser, MessageQueueAccessRights.GetQueueProperties); + + queue.SetPermissions(LocalAdministratorsGroupName, MessageQueueAccessRights.FullControl); + } + catch (MessageQueueException permissionException) when (permissionException.MessageQueueErrorCode == MessageQueueErrorCode.FormatNameBufferTooSmall) + { + Logger.Warn($"The name for queue '{queue.FormatName}' is too long for permissions to be applied. Please consider a shorter endpoint name.", permissionException); + } + } + } + catch (MessageQueueException ex) when (ex.MessageQueueErrorCode == MessageQueueErrorCode.QueueExists) + { + //Solves the race condition problem when multiple endpoints try to create same queue (e.g. error queue). + } + } + + readonly bool useTransactionalQueues; + readonly string installerUser; + + static readonly string LocalAdministratorsGroupName = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount)).ToString(); + static readonly ILog Logger = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqScopeOptions.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqScopeOptions.cs new file mode 100644 index 00000000..aba2217a --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqScopeOptions.cs @@ -0,0 +1,36 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Transactions; + + class MsmqScopeOptions + { + public MsmqScopeOptions(TimeSpan? requestedTimeout = null, IsolationLevel? requestedIsolationLevel = null) + { + var timeout = TransactionManager.DefaultTimeout; + var isolationLevel = IsolationLevel.ReadCommitted; + if (requestedTimeout.HasValue) + { + if (requestedTimeout.Value > TransactionManager.MaximumTimeout) + { + throw new ArgumentOutOfRangeException(nameof(requestedTimeout), requestedTimeout.Value, "Timeout requested is longer than the maximum value for this machine. Override using the maxTimeout setting of the system.transactions section in machine.config"); + } + + timeout = requestedTimeout.Value; + } + + if (requestedIsolationLevel.HasValue) + { + isolationLevel = requestedIsolationLevel.Value; + } + + TransactionOptions = new TransactionOptions + { + IsolationLevel = isolationLevel, + Timeout = timeout + }; + } + + public TransactionOptions TransactionOptions { get; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs new file mode 100644 index 00000000..1970c965 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs @@ -0,0 +1,375 @@ +namespace NServiceBus +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Transactions; + using Features; + using MSMQ.Messaging; + using NServiceBus.MessagingBridge.Msmq; + using NServiceBus.MessagingBridge.Msmq.DelayedDelivery; + using Routing; + using Support; + using Transport; + + /// + /// Transport definition for MSMQ. + /// + public partial class MsmqTransport : TransportDefinition, IMessageDrivenSubscriptionTransport + { + const string TimeoutsQueueQualifier = "timeouts"; + + /// + /// Creates a new instance of for configuration. + /// + public MsmqTransport() : base(TransportTransactionMode.TransactionScope, true, false, true) + { + } + + /// + public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) + { + Guard.AgainstNull(nameof(hostSettings), hostSettings); + Guard.AgainstNull(nameof(receivers), receivers); + Guard.AgainstNull(nameof(sendingAddresses), sendingAddresses); + + CheckMachineNameForCompliance.Check(); + ValidateIfDtcIsAvailable(); + + var queuesToCreate = new HashSet(sendingAddresses); + + var requiresDelayedDelivery = DelayedDelivery != null; + + string timeoutsErrorQueue = null; + MessagePump delayedDeliveryMessagePump = null; + + + if (requiresDelayedDelivery) + { + QueueAddress timeoutsQueue; + if (receivers.Length > 0) + { + var mainReceiver = receivers[0]; + timeoutsQueue = new QueueAddress(mainReceiver.ReceiveAddress.BaseAddress, qualifier: TimeoutsQueueQualifier); + timeoutsErrorQueue = mainReceiver.ErrorQueue; + } + else + { + if (hostSettings.CoreSettings != null) + { + if (!hostSettings.CoreSettings.TryGetExplicitlyConfiguredErrorQueueAddress(out var coreErrorQueue)) + { + throw new Exception("Delayed delivery requires an error queue to be specified using 'EndpointConfiguration.SendFailedMessagesTo()'"); + } + + timeoutsErrorQueue = coreErrorQueue; + timeoutsQueue = new QueueAddress(hostSettings.Name, qualifier: TimeoutsQueueQualifier); //Use name of the endpoint as the timeouts queue name. + } + else + { + throw new Exception("Timeouts are not supported for send-only configurations outside of an NServiceBus endpoint."); + } + } + + delayedDeliveryMessagePump = new MessagePump(mode => SelectReceiveStrategy(mode, TransactionScopeOptions.TransactionOptions), + MessageEnumeratorTimeout, TransportTransactionMode, false, hostSettings.CriticalErrorAction, + new ReceiveSettings("DelayedDelivery", timeoutsQueue, false, false, timeoutsErrorQueue)); + + queuesToCreate.Add(delayedDeliveryMessagePump.ReceiveAddress); + queuesToCreate.Add(timeoutsErrorQueue); + } + + var messageReceivers = CreateReceivers(receivers, hostSettings.CriticalErrorAction, queuesToCreate); + + var dispatcher = new MsmqMessageDispatcher(this, delayedDeliveryMessagePump?.ReceiveAddress, OnSendCallbackForTesting); + + if (hostSettings.CoreSettings != null) + { + // enforce an explicitly configured error queue when using MSMQ transport with NServiceBus + if (receivers.Length > 0 && !hostSettings.CoreSettings.TryGetExplicitlyConfiguredErrorQueueAddress(out _)) + { + throw new Exception("Faults forwarding requires an error queue to be specified using 'EndpointConfiguration.SendFailedMessagesTo()'"); + } + + bool outBoxRunning = hostSettings.CoreSettings.IsFeatureActive(typeof(Features.Outbox)); + if (hostSettings.CoreSettings.TryGetAuditMessageExpiration(out var auditMessageExpiration)) + { + TimeToBeReceivedOverrideChecker.Check( + TransportTransactionMode != TransportTransactionMode.None, + outBoxRunning, + auditMessageExpiration > TimeSpan.Zero); + } + + // try to use the configured installer user in Core: + CreateQueuesForUser ??= hostSettings.CoreSettings.GetOrDefault("Installers.UserName"); + } + + if (hostSettings.SetupInfrastructure && CreateQueues) + { + var installerUser = GetInstallationUserName(); + var queueCreator = new MsmqQueueCreator(UseTransactionalQueues, installerUser); + + if (requiresDelayedDelivery) + { + await DelayedDelivery.DelayedMessageStore.Initialize(hostSettings.Name, TransportTransactionMode, cancellationToken).ConfigureAwait(false); + } + + queueCreator.CreateQueueIfNecessary(queuesToCreate); + } + + foreach (var address in sendingAddresses.Concat(messageReceivers.Select(r => r.Value.ReceiveAddress))) + { + QueuePermissions.CheckQueue(address); + } + + DelayedDeliveryPump delayedDeliveryPump = null; + if (requiresDelayedDelivery) + { + QueuePermissions.CheckQueue(delayedDeliveryMessagePump.ReceiveAddress); + QueuePermissions.CheckQueue(timeoutsErrorQueue); + + var staticFaultMetadata = new Dictionary + { + {Headers.ProcessingMachine, RuntimeEnvironment.MachineName}, + {Headers.ProcessingEndpoint, hostSettings.Name}, + {Headers.HostDisplayName, hostSettings.HostDisplayName} + }; + + var dueDelayedMessagePoller = new DueDelayedMessagePoller(dispatcher, DelayedDelivery.DelayedMessageStore, DelayedDelivery.NumberOfRetries, hostSettings.CriticalErrorAction, timeoutsErrorQueue, staticFaultMetadata, TransportTransactionMode, + DelayedDelivery.TimeToTriggerFetchCircuitBreaker, + DelayedDelivery.TimeToTriggerDispatchCircuitBreaker, + DelayedDelivery.MaximumRecoveryFailuresPerSecond, + delayedDeliveryMessagePump.ReceiveAddress); + + delayedDeliveryPump = new DelayedDeliveryPump(dispatcher, dueDelayedMessagePoller, DelayedDelivery.DelayedMessageStore, delayedDeliveryMessagePump, timeoutsErrorQueue, DelayedDelivery.NumberOfRetries, hostSettings.CriticalErrorAction, DelayedDelivery.TimeToTriggerStoreCircuitBreaker, staticFaultMetadata, TransportTransactionMode); + } + + hostSettings.StartupDiagnostic.Add("NServiceBus.Transport.MSMQ", new + { + ExecuteInstaller = CreateQueues, + UseDeadLetterQueue, + UseConnectionCache, + UseTransactionalQueues, + UseJournalQueue, + UseDeadLetterQueueForMessagesWithTimeToBeReceived, + TimeToReachQueue = GetFormattedTimeToReachQueue(TimeToReachQueue), + TimeoutQueue = delayedDeliveryMessagePump?.ReceiveAddress, + TimeoutStorageType = DelayedDelivery?.DelayedMessageStore?.GetType()?.FullName, + }); + + var infrastructure = new MsmqTransportInfrastructure(messageReceivers, dispatcher, delayedDeliveryPump); + await infrastructure.Start(cancellationToken).ConfigureAwait(false); + + return infrastructure; + } + + Dictionary CreateReceivers(ReceiveSettings[] receivers, Action criticalErrorAction, HashSet queuesToCreate) + { + var messagePumps = new Dictionary(receivers.Length); + + foreach (var receiver in receivers) + { + if (receiver.UsePublishSubscribe) + { + throw new NotImplementedException("MSMQ does not support native pub/sub."); + } + + var pump = new MessagePump( + transactionMode => + SelectReceiveStrategy(transactionMode, TransactionScopeOptions.TransactionOptions), + MessageEnumeratorTimeout, + TransportTransactionMode, + IgnoreIncomingTimeToBeReceivedHeaders, + criticalErrorAction, + receiver + ); + + // The following check avoids creating some sub-queues, if the endpoint sub queue has the capability to exceed the max length limitation for queue format name. + CheckEndpointNameComplianceForMsmq.Check(pump.ReceiveAddress); + + queuesToCreate.Add(pump.ReceiveAddress); + messagePumps.Add(pump.Id, pump); + } + + return messagePumps; + } + + static ReceiveStrategy SelectReceiveStrategy(TransportTransactionMode minimumConsistencyGuarantee, TransactionOptions transactionOptions) + { + return minimumConsistencyGuarantee switch + { + TransportTransactionMode.TransactionScope => new TransactionScopeStrategy(transactionOptions, new MsmqFailureInfoStorage(1000)), + TransportTransactionMode.SendsAtomicWithReceive => new SendsAtomicWithReceiveNativeTransactionStrategy(new MsmqFailureInfoStorage(1000)), + TransportTransactionMode.ReceiveOnly => new ReceiveOnlyNativeTransactionStrategy(new MsmqFailureInfoStorage(1000)), + TransportTransactionMode.None => new NoTransactionStrategy(), + _ => throw new NotSupportedException($"TransportTransactionMode {minimumConsistencyGuarantee} is not supported by the MSMQ transport"), + }; + } + + void ValidateIfDtcIsAvailable() + { + if (TransportTransactionMode == TransportTransactionMode.TransactionScope) + { + try + { + using (var ts = new TransactionScope()) + { + TransactionInterop.GetTransmitterPropagationToken(Transaction.Current); // Enforce promotion to MSDTC + ts.Complete(); + } + } + catch (TransactionAbortedException) + { + throw new Exception("Transaction mode is set to `TransactionScope`. This depends on Microsoft Distributed Transaction Coordinator (MSDTC) which is not available. Either enable MSDTC, enable Outbox, or lower the transaction mode to `SendsAtomicWithReceive`."); + } + } + } + + /// + public override IReadOnlyCollection GetSupportedTransactionModes() => + new[] + { + TransportTransactionMode.None, + TransportTransactionMode.ReceiveOnly, + TransportTransactionMode.SendsAtomicWithReceive, + TransportTransactionMode.TransactionScope, + }; + + /// + /// Indicates whether queues should be automatically created. Setting this to false disables the automatic queue creation, even when installers are enabled using `EndpointConfiguration.EnableInstallers()`. + /// + /// + /// With installers enabled, required queues will be created automatically at startup.While this may be convenient for development, + /// we instead recommend that queues are created as part of deployment using the CreateQueues.ps1 script included in the NuGet package. + /// The installers might still need to be enabled to fulfill the installation needs of other components, but this method allows + /// scripts to be used for queue creation instead. + /// + public bool CreateQueues { get; set; } = true; + + /// + /// This setting should be used with caution. Configures whether to store undeliverable messages in the dead letter queue. Disabling the dead letter queue should be used with caution. Setting this to false should only be used where loss of messages is an acceptable. + /// + public bool UseDeadLetterQueue { get; set; } = true; + + /// + /// Configures MSMQ to cache connections to a remote queue and re-use them + /// as needed instead of creating new connections for each message. + /// Turning connection caching off will negatively impact the message throughput in most scenarios. + /// + public bool UseConnectionCache { get; set; } = true; + + /// + /// This setting should be used with caution. When set to false, any message that has + /// an exception during processing will not be rolled back to the queue. Therefore this setting must only + /// be disabled when loss of messages is an acceptable scenario. + /// + public bool UseTransactionalQueues { get; set; } = true; + + /// + /// Controls the use of journaling messages. When journaling is enabled, a copy of every message received will be stored in the journal queue. Disabled by default. + /// Should be used ONLY when debugging as it can potentially use up the MSMQ journal storage quota based on the message volume. + /// + public bool UseJournalQueue { get; set; } = false; + + /// + /// When enabled messages that have exceeded their TimeToBeReceived will be moved to the dead letter queue instead of discarding them. Disabled by default. + /// + public bool UseDeadLetterQueueForMessagesWithTimeToBeReceived { get; set; } = false; + + /// + /// Configures the Time-To-Reach-Queue (TTRQ) timespan. The default value if not set is Message.InfiniteTimeout + /// + public TimeSpan TimeToReachQueue { get; set; } = Message.InfiniteTimeout; + + /// + /// Set a delegate to use for applying the property when sending a message. + /// + /// + /// This delegate will be used for all valid messages sent via MSMQ. + /// This includes not just standard messages, but also Audits, Errors and all control messages. + /// In some cases it may be useful to check the key to determine if a message is + /// a control message. + /// The only exception to this rule is received messages with corrupted headers. These messages will be forwarded to the + /// error queue with no label applied. + /// + public Func, string> ApplyCustomLabelToOutgoingMessages { get; set; } = _ => string.Empty; + + /// + /// Configures whether to ignore incoming Time-To-Be-Received (TTBR) headers. By default an expired TTBR header will result in the message to be discarded. + /// + public bool IgnoreIncomingTimeToBeReceivedHeaders { get; set; } = false; + + /// + /// When set to true, disables native Time-To-Be-Received (TTBR) when combined with transactions. Instead, the receiver will discard incoming messages that have exceeded the specified Time-To-Be-Received. + /// + public bool UseNonNativeTimeToBeReceivedInTransactions { get; set; } = false; + + /// + /// The user account that will be configured with access rights to the queues created by the transport. When not set, the current user will be used. + /// This setting is not relevant, if queue creation has been disabled. + /// + public string CreateQueuesForUser { get; set; } + + /// + /// Enable delayed delivery of messages. Required for delayed retries and Saga timeouts. + /// + public DelayedDeliverySettings DelayedDelivery { get; set; } + + /// + /// The callback that can be used to inject failures to the dispatcher for testing. + /// + internal Action OnSendCallbackForTesting { get; set; } + + /// + /// Allows to change the transaction isolation level and timeout for the `TransactionScope` used to receive messages. + /// + /// + /// If not specified the default transaction timeout of the machine will be used and the isolation level will be set to + /// . + /// + /// Transaction timeout duration. + /// Transaction isolation level. + public void ConfigureTransactionScope(TimeSpan? timeout = null, IsolationLevel? isolationLevel = null) + { + Guard.AgainstNegativeAndZero(nameof(timeout), timeout); + + if (isolationLevel == IsolationLevel.Snapshot) + { + throw new ArgumentException("Isolation level `Snapshot` is not supported by the transport. Consider not sharing the transaction between transport and persistence if persistence should use `IsolationLevel.Snapshot` by using `TransportTransactionMode.SendsAtomicWithReceive` or lower.", nameof(isolationLevel)); + } + + TransactionScopeOptions = new MsmqScopeOptions(timeout, isolationLevel); + } + + static string GetFormattedTimeToReachQueue(TimeSpan timeToReachQueue) + { + return timeToReachQueue == Message.InfiniteTimeout ? "Infinite" + : string.Format("{0:%d} day(s) {0:%hh} hours(s) {0:%mm} minute(s) {0:%ss} second(s)", timeToReachQueue); + } + + string GetInstallationUserName() + { + if (CreateQueuesForUser != null) + { + return CreateQueuesForUser; + } + + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + return $"{Environment.UserDomainName}\\{Environment.UserName}"; + } + + return Environment.UserName; + } + + /// + /// Defines the timeout used for . + /// + protected internal TimeSpan MessageEnumeratorTimeout { get; set; } = TimeSpan.FromSeconds(1); + + internal MsmqScopeOptions TransactionScopeOptions { get; private set; } = new MsmqScopeOptions(); + + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs new file mode 100644 index 00000000..19c20cc1 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs @@ -0,0 +1,64 @@ + +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Collections.Generic; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using DelayedDelivery; + using Support; + using Transport; + + class MsmqTransportInfrastructure : TransportInfrastructure + { + readonly DelayedDeliveryPump delayedDeliveryPump; + + public MsmqTransportInfrastructure(IReadOnlyDictionary receivers, MsmqMessageDispatcher dispatcher, DelayedDeliveryPump delayedDeliveryPump) + { + this.delayedDeliveryPump = delayedDeliveryPump; + Dispatcher = dispatcher; + Receivers = receivers; + } + + public async Task Start(CancellationToken cancellationToken = default) + { + if (delayedDeliveryPump != null) + { + await delayedDeliveryPump.Start(cancellationToken).ConfigureAwait(false); + } + } + + public override async Task Shutdown(CancellationToken cancellationToken = default) + { + if (delayedDeliveryPump != null) + { + await delayedDeliveryPump.Stop(cancellationToken).ConfigureAwait(false); + } + } + + public override string ToTransportAddress(QueueAddress address) => TranslateAddress(address); + + internal static string TranslateAddress(QueueAddress address) + { + if (!address.Properties.TryGetValue("machine", out var machine)) + { + machine = RuntimeEnvironment.MachineName; + } + if (!address.Properties.TryGetValue("queue", out var queueName)) + { + queueName = address.BaseAddress; + } + + var queue = new StringBuilder(queueName); + if (address.Discriminator != null) + { + queue.Append("-" + address.Discriminator); + } + if (address.Qualifier != null) + { + queue.Append("." + address.Qualifier); + } + return $"{queue}@{machine}"; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqUtilities.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqUtilities.cs new file mode 100644 index 00000000..7397074e --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqUtilities.cs @@ -0,0 +1,216 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using MSMQ.Messaging; + using System.Text; + using System.Xml; + using Logging; + using Transport; + + class MsmqUtilities + { + static MsmqAddress GetIndependentAddressForQueue(MessageQueue q) + { + var arr = q.FormatName.Split('\\'); + var queueName = arr[arr.Length - 1]; + + var directPrefixIndex = arr[0].IndexOf(DIRECTPREFIX, StringComparison.Ordinal); + if (directPrefixIndex >= 0) + { + return new MsmqAddress(queueName, arr[0].Substring(directPrefixIndex + DIRECTPREFIX.Length)); + } + + var tcpPrefixIndex = arr[0].IndexOf(DIRECTPREFIX_TCP, StringComparison.Ordinal); + if (tcpPrefixIndex >= 0) + { + return new MsmqAddress(queueName, arr[0].Substring(tcpPrefixIndex + DIRECTPREFIX_TCP.Length)); + } + + try + { + // the pessimistic approach failed, try the optimistic approach + arr = q.QueueName.Split('\\'); + queueName = arr[arr.Length - 1]; + return new MsmqAddress(queueName, q.MachineName); + } + catch + { + throw new Exception($"Could not translate format name to independent name: {q.FormatName}"); + } + } + + public static Dictionary ExtractHeaders(Message msmqMessage) + { + var headers = DeserializeMessageHeaders(msmqMessage.Extension); + + //note: we can drop this line when we no longer support interop btw v3 + v4 + if (msmqMessage.ResponseQueue != null && !headers.ContainsKey(Headers.ReplyToAddress)) + { + headers[Headers.ReplyToAddress] = GetIndependentAddressForQueue(msmqMessage.ResponseQueue).ToString(); + } + + if (Enum.IsDefined(typeof(MessageIntent), msmqMessage.AppSpecific) && !headers.ContainsKey(Headers.MessageIntent)) + { + headers[Headers.MessageIntent] = ((MessageIntent)msmqMessage.AppSpecific).ToString(); + } + + headers[Headers.CorrelationId] = GetCorrelationId(msmqMessage, headers); + return headers; + } + + static string GetCorrelationId(Message message, Dictionary headers) + { + if (headers.TryGetValue(Headers.CorrelationId, out var correlationId)) + { + return correlationId; + } + + if (message.CorrelationId == "00000000-0000-0000-0000-000000000000\\0") + { + return null; + } + + //msmq required the id's to be in the {guid}\{incrementing number} format so we need to fake a \0 at the end that the sender added to make it compatible + //The replace can be removed in v5 since only v3 messages will need this + return message.CorrelationId.Replace("\\0", string.Empty); + } + + public static Dictionary DeserializeMessageHeaders(byte[] data) + { + var result = new Dictionary(); + + if (data.Length == 0) + { + return result; + } + + //This is to make us compatible with v3 messages that are affected by this issue: + //http://stackoverflow.com/questions/3779690/xml-serialization-appending-the-0-backslash-0-or-null-character + var xmlLength = data.LastIndexOf(EndTag) + EndTag.Length; // Ignore any data after last + object o; + using (var stream = new MemoryStream(buffer: data, index: 0, count: xmlLength, writable: false, publiclyVisible: true)) + { + using (var reader = XmlReader.Create(stream, new XmlReaderSettings + { + CheckCharacters = false + })) + { + o = headerSerializer.Deserialize(reader); + } + } + + foreach (var pair in (List)o) + { + if (pair.Key != null) + { + result.Add(pair.Key, pair.Value); + } + } + + return result; + } + + public static Message Convert(OutgoingMessage message) + { + var result = new Message(); + + if (!message.Body.IsEmpty) + { + result.BodyStream = new ReadOnlyStream(message.Body); + } + + AssignMsmqNativeCorrelationId(message, result); + + result.Recoverable = true; + + var addCorrIdHeader = !message.Headers.ContainsKey("CorrId"); + + using (var stream = new MemoryStream()) + { + var headers = message.Headers.Select(pair => new HeaderInfo + { + Key = pair.Key, + Value = pair.Value + }).ToList(); + + if (addCorrIdHeader) + { + headers.Add(new HeaderInfo + { + Key = "CorrId", + Value = result.CorrelationId + }); + } + + headerSerializer.Serialize(stream, headers); + + result.Extension = stream.ToArray(); + } + + var messageIntent = default(MessageIntent); + + if (message.Headers.TryGetValue(Headers.MessageIntent, out var messageIntentString)) + { + Enum.TryParse(messageIntentString, true, out messageIntent); + } + + result.AppSpecific = (int)messageIntent; + + + return result; + } + + + static void AssignMsmqNativeCorrelationId(OutgoingMessage message, Message result) + { + if (!message.Headers.TryGetValue(Headers.CorrelationId, out var correlationIdHeader)) + { + return; + } + + if (string.IsNullOrEmpty(correlationIdHeader)) + { + return; + } + + + if (Guid.TryParse(correlationIdHeader, out _)) + { + //msmq required the id's to be in the {guid}\{incrementing number} format so we need to fake a \0 at the end to make it compatible + result.CorrelationId = $"{correlationIdHeader}\\0"; + return; + } + + try + { + if (correlationIdHeader.Contains("\\")) + { + var parts = correlationIdHeader.Split('\\'); + + + if (parts.Length == 2 && Guid.TryParse(parts.First(), out _) && + int.TryParse(parts[1], out _)) + { + result.CorrelationId = correlationIdHeader; + } + } + } + catch (Exception ex) + { + Logger.Warn($"Failed to assign a native correlation id for message: {message.MessageId}", ex); + } + } + + public const string PropertyHeaderPrefix = "NServiceBus.Timeouts.Properties."; + const string DIRECTPREFIX = "DIRECT=OS:"; + const string DIRECTPREFIX_TCP = "DIRECT=TCP:"; + internal const string PRIVATE = "\\private$\\"; + + static System.Xml.Serialization.XmlSerializer headerSerializer = new System.Xml.Serialization.XmlSerializer(typeof(List)); + static ILog Logger = LogManager.GetLogger(); + static byte[] EndTag = Encoding.UTF8.GetBytes(""); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj index dae54bd3..2095f252 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj +++ b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj @@ -1,10 +1,12 @@  - net8.0 + net8.0-windows + + @@ -12,4 +14,11 @@ + + + + + + + diff --git a/src/NServiceBus.MessagingBridge.Msmq/NoTransactionStrategy.cs b/src/NServiceBus.MessagingBridge.Msmq/NoTransactionStrategy.cs new file mode 100644 index 00000000..53d3680b --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/NoTransactionStrategy.cs @@ -0,0 +1,53 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Buffers; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Extensibility; + using Transport; + + class NoTransactionStrategy : ReceiveStrategy + { + public override async Task ReceiveMessage(CancellationToken cancellationToken = default) + { + if (!TryReceive(MessageQueueTransactionType.None, out var message)) + { + return; + } + + if (!TryExtractHeaders(message, out var headers)) + { + MovePoisonMessageToErrorQueue(message, IsQueuesTransactional ? MessageQueueTransactionType.Single : MessageQueueTransactionType.None); + return; + } + + var transportTransaction = new TransportTransaction(); + var context = new ContextBag(); + + context.Set(message); + + var length = (int)message.BodyStream.Length; + var buffer = ArrayPool.Shared.Rent(length); + try + { + _ = await message.BodyStream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + var body = buffer.AsMemory(0, length); + + try + { + await TryProcessMessage(message.Id, headers, body, transportTransaction, context, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + await HandleError(message, body, ex, transportTransaction, 1, context, cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs new file mode 100644 index 00000000..62275c2d --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs @@ -0,0 +1,20 @@ +namespace NServiceBus +{ + using Features; + using Persistence; + using Persistence.Msmq; + + /// + /// Used to enable Msmq persistence. + /// + public class MsmqPersistence : PersistenceDefinition + { + internal MsmqPersistence() + { + Supports(s => + { + s.EnableFeatureByDefault(); + }); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs new file mode 100644 index 00000000..f3c92d61 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs @@ -0,0 +1,11 @@ +namespace NServiceBus.Persistence.Msmq +{ + using System.Collections.Generic; + + interface IMsmqSubscriptionStorageQueue + { + IEnumerable GetAllMessages(); + string Send(string body, string label); + void TryReceiveById(string messageId); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs new file mode 100644 index 00000000..7e2d5098 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs @@ -0,0 +1,28 @@ +namespace NServiceBus.Persistence.Msmq +{ + using System; + using MSMQ.Messaging; + + class MsmqSubscriptionMessage + { + public MsmqSubscriptionMessage(Message m) + { + Body = m.Body; + Label = m.Label; + Id = m.Id; + ArrivedTime = m.ArrivedTime; + } + + public MsmqSubscriptionMessage() + { + } + + public DateTime ArrivedTime { get; set; } + + public object Body { get; set; } + + public string Label { get; set; } + + public string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs new file mode 100644 index 00000000..bc113fc5 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs @@ -0,0 +1,64 @@ +namespace NServiceBus.Persistence.Msmq +{ + using System; + using Features; + using Logging; + using Microsoft.Extensions.DependencyInjection; + using MSMQ.Messaging; + using NServiceBus.MessagingBridge.Msmq; + using Settings; + using Transport; + using Unicast.Subscriptions.MessageDrivenSubscriptions; + + class MsmqSubscriptionPersistence : Feature + { + protected override void Setup(FeatureConfigurationContext context) + { + var configuredQueueName = DetermineStorageQueueName(context.Settings); + + context.Settings.Get().BindSending(configuredQueueName); + + var transportSettings = context.Settings.Get() as MsmqTransport; + + var queue = new MsmqSubscriptionStorageQueue(MsmqAddress.Parse(configuredQueueName), transportSettings.UseTransactionalQueues); + var storage = new MsmqSubscriptionStorage(queue); + + context.Services.AddSingleton(storage); + } + + internal static string DetermineStorageQueueName(IReadOnlySettings settings) + { + var configuredQueueName = settings.GetConfiguredMsmqPersistenceSubscriptionQueue(); + + if (!string.IsNullOrEmpty(configuredQueueName)) + { + return configuredQueueName; + } + ThrowIfUsingTheOldDefaultSubscriptionsQueue(); + + var defaultQueueName = $"{settings.EndpointName()}.Subscriptions"; + Logger.Info($"The queue used to store subscriptions has not been configured, the default '{defaultQueueName}' will be used."); + return defaultQueueName; + } + + static void ThrowIfUsingTheOldDefaultSubscriptionsQueue() + { + if (DoesOldDefaultQueueExists()) + { + // The user has not configured the subscriptions queue to be "NServiceBus.Subscriptions" but there's a local queue. + // Indicates that the endpoint was using old default queue name. + throw new Exception( + "Detected the presence of an old default queue named `NServiceBus.Subscriptions`. Either migrate the subscriptions to the new default queue `[Your endpoint name].Subscriptions`, see our documentation for more details, or explicitly configure the subscriptions queue name to `NServiceBus.Subscriptions` if you want to use the existing queue."); + } + } + + static bool DoesOldDefaultQueueExists() + { + const string oldDefaultSubscriptionsQueue = "NServiceBus.Subscriptions"; + var path = MsmqAddress.Parse(oldDefaultSubscriptionsQueue).PathWithoutPrefix; + return MessageQueue.Exists(path); + } + + static ILog Logger = LogManager.GetLogger(typeof(MsmqSubscriptionPersistence)); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs new file mode 100644 index 00000000..680cf38d --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs @@ -0,0 +1,238 @@ +namespace NServiceBus.Persistence.Msmq +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Extensibility; + using Logging; + using Unicast.Subscriptions.MessageDrivenSubscriptions; + using MessageType = Unicast.Subscriptions.MessageType; + + class MsmqSubscriptionStorage : ISubscriptionStorage, IDisposable + { + public MsmqSubscriptionStorage(IMsmqSubscriptionStorageQueue storageQueue) + { + this.storageQueue = storageQueue; + + // Required to be lazy loaded as the queue might not exist yet + lookup = new Lazy>>(CreateLookup); + } + + public void Dispose() + { + // Filled in by Janitor.fody + } + + Dictionary> CreateLookup() + { + var output = new Dictionary>(SubscriberComparer); + + var messages = storageQueue.GetAllMessages() + .OrderByDescending(m => m.ArrivedTime) + .ThenBy(x => x.Id) // ensure same order of messages with same timestamp across all endpoints + .ToArray(); + + foreach (var m in messages) + { + var messageTypeString = m.Body as string; + var messageType = new MessageType(messageTypeString); //this will parse both 2.6 and 3.0 type strings + var subscriber = Deserialize(m.Label); + + if (!output.TryGetValue(subscriber, out var endpointSubscriptions)) + { + output[subscriber] = endpointSubscriptions = new Dictionary(); + } + + if (endpointSubscriptions.ContainsKey(messageType)) + { + // this message is stale and can be removed + storageQueue.TryReceiveById(m.Id); + } + else + { + endpointSubscriptions[messageType] = m.Id; + } + } + + return output; + } + + public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) + { + var messagelist = messageTypes.ToArray(); + var result = new HashSet(); + + try + { + // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! + rwLock.EnterReadLock(); + + foreach (var subscribers in lookup.Value) + { + foreach (var messageType in messagelist) + { + if (subscribers.Value.TryGetValue(messageType, out _)) + { + result.Add(subscribers.Key); + } + } + } + } + finally + { + rwLock.ExitReadLock(); + } + + return Task.FromResult>(result); + } + + public Task Subscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) + { + var body = $"{messageType.TypeName}, Version={messageType.Version}"; + var label = Serialize(subscriber); + var messageId = storageQueue.Send(body, label); + + AddToLookup(subscriber, messageType, messageId); + + log.DebugFormat($"Subscriber {subscriber.TransportAddress} added for message {messageType}."); + + return Task.CompletedTask; + } + + public Task Unsubscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) + { + var messageId = RemoveFromLookup(subscriber, messageType); + + if (messageId != null) + { + storageQueue.TryReceiveById(messageId); + } + + log.Debug($"Subscriber {subscriber.TransportAddress} removed for message {messageType}."); + + return Task.CompletedTask; + } + + static string Serialize(Subscriber subscriber) + { + return $"{subscriber.TransportAddress}|{subscriber.Endpoint}"; + } + + static Subscriber Deserialize(string serializedForm) + { + var parts = serializedForm.Split(EntrySeparator, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length is 0 or > 2) + { + log.Error($"Invalid format of subscription entry: {serializedForm}."); + return null; + } + var endpointName = parts.Length > 1 + ? parts[1] + : null; + + return new Subscriber(parts[0], endpointName); + } + + void AddToLookup(Subscriber subscriber, MessageType typeName, string messageId) + { + try + { + // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! + rwLock.EnterWriteLock(); + + if (!lookup.Value.TryGetValue(subscriber, out var dictionary)) + { + dictionary = []; + } + else + { + // replace existing subscriber + lookup.Value.Remove(subscriber); + } + + dictionary[typeName] = messageId; + lookup.Value[subscriber] = dictionary; + } + finally + { + rwLock.ExitWriteLock(); + } + } + + string RemoveFromLookup(Subscriber subscriber, MessageType typeName) + { + try + { + // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! + rwLock.EnterWriteLock(); + + if (lookup.Value.TryGetValue(subscriber, out var subscriptions)) + { + if (subscriptions.TryGetValue(typeName, out var messageId)) + { + subscriptions.Remove(typeName); + if (subscriptions.Count == 0) + { + lookup.Value.Remove(subscriber); + } + + return messageId; + } + } + } + finally + { + rwLock.ExitWriteLock(); + } + + return null; + } + + Lazy>> lookup; + IMsmqSubscriptionStorageQueue storageQueue; + ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + + static ILog log = LogManager.GetLogger(typeof(ISubscriptionStorage)); + static TransportAddressEqualityComparer SubscriberComparer = new TransportAddressEqualityComparer(); + + static readonly char[] EntrySeparator = + { + '|' + }; + + sealed class TransportAddressEqualityComparer : IEqualityComparer + { + public bool Equals(Subscriber x, Subscriber y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null) + { + return false; + } + + if (y is null) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return string.Equals(x.TransportAddress, y.TransportAddress, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(Subscriber obj) + { + return obj.TransportAddress != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.TransportAddress) : 0; + } + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs new file mode 100644 index 00000000..5d7a51a5 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs @@ -0,0 +1,32 @@ +namespace NServiceBus +{ + using Configuration.AdvancedExtensibility; + using NServiceBus.MessagingBridge.Msmq; + using Settings; + + /// + /// Provides configuration extensions when using . + /// + public static class MsmqSubscriptionStorageConfigurationExtensions + { + /// + /// Configures the queue used to store subscriptions. + /// + /// The settings to extend. + /// The queue name. + public static void SubscriptionQueue(this PersistenceExtensions persistenceExtensions, string queue) + { + Guard.AgainstNull(nameof(persistenceExtensions), persistenceExtensions); + Guard.AgainstNull(nameof(queue), queue); + + persistenceExtensions.GetSettings().Set(MsmqPersistenceQueueConfigurationKey, queue); + } + + internal static string GetConfiguredMsmqPersistenceSubscriptionQueue(this IReadOnlySettings settings) + { + return settings.GetOrDefault(MsmqPersistenceQueueConfigurationKey); + } + + internal const string MsmqPersistenceQueueConfigurationKey = "MsmqSubscriptionPersistence.QueueName"; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs new file mode 100644 index 00000000..a3dd3dfc --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs @@ -0,0 +1,68 @@ +namespace NServiceBus.Persistence.Msmq +{ + using System; + using System.Collections.Generic; + using System.Linq; + using MSMQ.Messaging; + using NServiceBus.MessagingBridge.Msmq; + + class MsmqSubscriptionStorageQueue : IMsmqSubscriptionStorageQueue + { + public MsmqSubscriptionStorageQueue(MsmqAddress queueAddress, bool useTransactionalQueue) + { + transactionTypeToUseForSend = useTransactionalQueue ? MessageQueueTransactionType.Single : MessageQueueTransactionType.None; + var messageReadPropertyFilter = new MessagePropertyFilter + { + Id = true, + Body = true, + Label = true, + ArrivedTime = true + }; + queue = new MessageQueue(queueAddress.FullPath) + { + Formatter = new XmlMessageFormatter(new[] + { + typeof(string) + }), + MessageReadPropertyFilter = messageReadPropertyFilter + }; + } + + public IEnumerable GetAllMessages() + { + return queue.GetAllMessages().Select(m => new MsmqSubscriptionMessage(m)); + } + + public string Send(string body, string label) + { + var toSend = new Message + { + Recoverable = true, + Formatter = queue.Formatter, + Body = body, + Label = label + }; + + queue.Send(toSend, transactionTypeToUseForSend); + + return toSend.Id; + } + + public void TryReceiveById(string messageId) + { + try + { + //Use of `None` here is intentional since ReceiveById works properly with this mode + //for both transactional and non-transactional queues + queue.ReceiveById(messageId, MessageQueueTransactionType.None); + } + catch (InvalidOperationException) + { + // thrown when message not found + } + } + + MessageQueueTransactionType transactionTypeToUseForSend; + MessageQueue queue; + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/QueuePermissions.cs b/src/NServiceBus.MessagingBridge.Msmq/QueuePermissions.cs new file mode 100644 index 00000000..3d3e68f8 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/QueuePermissions.cs @@ -0,0 +1,73 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System.Security; + using System.Security.Principal; + using Logging; + using MSMQ.Messaging; + + static class QueuePermissions + { + public static void CheckQueue(string address) + { + var msmqAddress = MsmqAddress.Parse(address); + var queuePath = msmqAddress.PathWithoutPrefix; + + Logger.Debug($"Checking if queue exists: {queuePath}."); + if (msmqAddress.IsRemote()) + { + Logger.Info($"Since {address} is remote, the queue could not be verified. Make sure the queue exists and that the address and permissions are correct. Messages could end up in the dead letter queue if configured incorrectly."); + return; + } + + var path = msmqAddress.PathWithoutPrefix; + + try + { + if (MessageQueue.Exists(path)) + { + using (var messageQueue = new MessageQueue(path)) + { + Logger.DebugFormat("Verified that the queue: [{0}] exists", queuePath); + WarnIfPublicAccess(messageQueue, LocalEveryoneGroupName); + WarnIfPublicAccess(messageQueue, LocalAnonymousLogonName); + } + } + else + { + Logger.WarnFormat("Queue [{0}] does not exist", queuePath); + } + } + catch (MessageQueueException ex) + { + Logger.Warn($"Unable to verify queue at address '{queuePath}'. Make sure the queue exists, and that the address is correct. Processing will still continue.", ex); + } + } + + static void WarnIfPublicAccess(MessageQueue queue, string userGroupName) + { + MessageQueueAccessRights? accessRights; + AccessControlEntryType? accessType; + + try + { + queue.TryGetPermissions(userGroupName, out accessRights, out accessType); + } + catch (SecurityException se) + { + Logger.Warn($"Unable to read permissions for queue [{queue.QueueName}]. Make sure you have administrative access on the target machine", se); + return; + } + + if (accessType == AccessControlEntryType.Allow) + { + var logMessage = $"Queue [{queue.QueueName}] is running with [{userGroupName}] with AccessRights set to [{accessRights}]. Consider setting appropriate permissions, if required by the organization. For more information, consult the documentation."; + Logger.Warn(logMessage); + } + } + + static readonly string LocalEveryoneGroupName = new SecurityIdentifier(WellKnownSidType.WorldSid, null).Translate(typeof(NTAccount)).ToString(); + static readonly string LocalAnonymousLogonName = new SecurityIdentifier(WellKnownSidType.AnonymousSid, null).Translate(typeof(NTAccount)).ToString(); + + static ILog Logger = LogManager.GetLogger(typeof(QueuePermissions)); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/ReadOnlyStream.cs b/src/NServiceBus.MessagingBridge.Msmq/ReadOnlyStream.cs new file mode 100644 index 00000000..05d521d6 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ReadOnlyStream.cs @@ -0,0 +1,45 @@ +namespace NServiceBus +{ + using System; + using System.IO; + + class ReadOnlyStream : Stream + { + ReadOnlyMemory memory; + long position; + + public ReadOnlyStream(ReadOnlyMemory memory) + { + this.memory = memory; + position = 0; + } + + public override void Flush() => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesToCopy = (int)Math.Min(count, memory.Length - position); + + var destination = buffer.AsSpan().Slice(offset, bytesToCopy); + var source = memory.Span.Slice((int)position, bytesToCopy); + + source.CopyTo(destination); + + position += bytesToCopy; + + return bytesToCopy; + } + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => false; + public override long Length => memory.Length; + public override long Position { get => position; set => position = value; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/ReceiveOnlyNativeTransactionStrategy.cs b/src/NServiceBus.MessagingBridge.Msmq/ReceiveOnlyNativeTransactionStrategy.cs new file mode 100644 index 00000000..8547237a --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ReceiveOnlyNativeTransactionStrategy.cs @@ -0,0 +1,111 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Buffers; + using System.Collections.Generic; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Extensibility; + using Transport; + + class ReceiveOnlyNativeTransactionStrategy : ReceiveStrategy + { + public ReceiveOnlyNativeTransactionStrategy(MsmqFailureInfoStorage failureInfoStorage) + { + this.failureInfoStorage = failureInfoStorage; + } + + public override async Task ReceiveMessage(CancellationToken cancellationToken = default) + { + Message message = null; + var context = new ContextBag(); + + try + { + using (var msmqTransaction = new MessageQueueTransaction()) + { + msmqTransaction.Begin(); + + if (!TryReceive(msmqTransaction, out message)) + { + return; + } + context.Set(message); + + if (!TryExtractHeaders(message, out var headers)) + { + MovePoisonMessageToErrorQueue(message, IsQueuesTransactional ? MessageQueueTransactionType.Single : MessageQueueTransactionType.None); + + msmqTransaction.Commit(); + return; + } + + var shouldCommit = await ProcessMessage(message, headers, context, cancellationToken).ConfigureAwait(false); + + if (shouldCommit) + { + msmqTransaction.Commit(); + failureInfoStorage.ClearFailureInfoForMessage(message.Id); + } + else + { + msmqTransaction.Abort(); + } + } + } + // We'll only get here if Commit/Abort/Dispose throws which should be rare. + // Note: If that happens the attempts counter will be inconsistent since the message might be picked up again before we can register the failure in the LRU cache. + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + if (message == null) + { + throw; + } + + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + } + } + + async Task ProcessMessage(Message message, Dictionary headers, ContextBag context, CancellationToken cancellationToken) + { + var length = (int)message.BodyStream.Length; + var buffer = ArrayPool.Shared.Rent(length); + + try + { + _ = await message.BodyStream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + var body = buffer.AsMemory(0, length); + + if (failureInfoStorage.TryGetFailureInfoForMessage(message.Id, out var failureInfo)) + { + var errorHandleResult = await HandleError(message, body, failureInfo.Exception, transportTransaction, failureInfo.NumberOfProcessingAttempts, failureInfo.Context, cancellationToken).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + return true; + } + } + + try + { + await TryProcessMessage(message.Id, headers, body, transportTransaction, context, cancellationToken).ConfigureAwait(false); + return true; + } + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + + return false; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + MsmqFailureInfoStorage failureInfoStorage; + static TransportTransaction transportTransaction = new TransportTransaction(); + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/ReceiveStrategy.cs b/src/NServiceBus.MessagingBridge.Msmq/ReceiveStrategy.cs new file mode 100644 index 00000000..507a164f --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/ReceiveStrategy.cs @@ -0,0 +1,144 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using Extensibility; + using Logging; + using Transport; + + abstract class ReceiveStrategy + { + public abstract Task ReceiveMessage(CancellationToken cancellationToken = default); + + public void Init(MessageQueue inputQueue, string receiveQueueAddress, MessageQueue errorQueue, OnMessage onMessage, OnError onError, Action criticalError, bool ignoreIncomingTimeToBeReceivedHeaders) + { + this.receiveQueueAddress = receiveQueueAddress; + this.inputQueue = inputQueue; + this.errorQueue = errorQueue; + this.onMessage = onMessage; + this.onError = onError; + this.criticalError = criticalError; + this.ignoreIncomingTimeToBeReceivedHeaders = ignoreIncomingTimeToBeReceivedHeaders; + } + + protected bool TryReceive(MessageQueueTransactionType transactionType, out Message message) + { + try + { + message = inputQueue.Receive(TimeSpan.FromMilliseconds(10), transactionType); + + return true; + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) + { + //We should only get an IOTimeout exception here if another process removed the message between us peeking and now. + message = null; + return false; + } + throw; + } + } + + protected bool TryReceive(MessageQueueTransaction transaction, out Message message) + { + try + { + message = inputQueue.Receive(TimeSpan.FromMilliseconds(10), transaction); + + return true; + } + catch (MessageQueueException ex) + { + if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) + { + //We should only get an IOTimeout exception here if another process removed the message between us peeking and now. + message = null; + return false; + } + throw; + } + } + + protected bool TryExtractHeaders(Message message, out Dictionary headers) + { + try + { + headers = MsmqUtilities.ExtractHeaders(message); + return true; + } + catch (Exception ex) + { + var error = $"Message '{message.Id}' has corrupted headers"; + + Logger.Warn(error, ex); + + headers = null; + return false; + } + } + + protected void MovePoisonMessageToErrorQueue(Message message, MessageQueueTransaction transaction) + { + var error = $"Message '{message.Id}' is classified as a poison message and will be moved to the configured error queue."; + + Logger.Error(error); + + errorQueue.Send(message, transaction); + } + + protected void MovePoisonMessageToErrorQueue(Message message, MessageQueueTransactionType transactionType) + { + var error = $"Message '{message.Id}' is classified as a poison message and will be moved to the configured error queue."; + + Logger.Error(error); + + errorQueue.Send(message, transactionType); + } + + protected async Task TryProcessMessage(string messageId, Dictionary headers, ReadOnlyMemory body, TransportTransaction transaction, ContextBag context, CancellationToken cancellationToken = default) + { + if (!ignoreIncomingTimeToBeReceivedHeaders && TimeToBeReceived.HasElapsed(headers)) + { + Logger.Debug($"Discarding message {messageId} due to lapsed Time To Be Received header"); + return; + } + + var messageContext = new MessageContext(messageId, headers, body, transaction, receiveQueueAddress, context); + await onMessage(messageContext, cancellationToken).ConfigureAwait(false); + } + + protected async Task HandleError(Message message, ReadOnlyMemory body, Exception exception, TransportTransaction transportTransaction, int processingAttempts, ContextBag context, CancellationToken cancellationToken = default) + { + try + { + var headers = MsmqUtilities.ExtractHeaders(message); + var errorContext = new ErrorContext(exception, headers, message.Id, body, transportTransaction, processingAttempts, receiveQueueAddress, context); + return await onError(errorContext, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + criticalError($"Failed to execute recoverability policy for message with native ID: `{message.Id}`", ex, cancellationToken); + + //best thing we can do is roll the message back if possible + return ErrorHandleResult.RetryRequired; + } + } + + protected bool IsQueuesTransactional => errorQueue.Transactional; + + MessageQueue inputQueue; + MessageQueue errorQueue; + OnMessage onMessage; + OnError onError; + Action criticalError; + bool ignoreIncomingTimeToBeReceivedHeaders; + + static readonly ILog Logger = LogManager.GetLogger(); + string receiveQueueAddress; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/RepeatedFailuresOverTimeCircuitBreaker.cs b/src/NServiceBus.MessagingBridge.Msmq/RepeatedFailuresOverTimeCircuitBreaker.cs new file mode 100644 index 00000000..1eef0ae9 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/RepeatedFailuresOverTimeCircuitBreaker.cs @@ -0,0 +1,71 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Logging; + + class RepeatedFailuresOverTimeCircuitBreaker : IDisposable + { + public RepeatedFailuresOverTimeCircuitBreaker(string name, TimeSpan timeToWaitBeforeTriggering, Action triggerAction) + { + this.name = name; + this.triggerAction = triggerAction; + this.timeToWaitBeforeTriggering = timeToWaitBeforeTriggering; + + timer = new Timer(CircuitBreakerTriggered); + } + + public void Success() + { + var oldValue = Interlocked.Exchange(ref failureCount, 0); + + if (oldValue == 0) + { + return; + } + + timer.Change(Timeout.Infinite, Timeout.Infinite); + Logger.InfoFormat("The circuit breaker for {0} is now disarmed", name); + } + + public Task Failure(Exception exception, CancellationToken cancellationToken = default) + { + lastException = exception; + var newValue = Interlocked.Increment(ref failureCount); + + if (newValue == 1) + { + timer.Change(timeToWaitBeforeTriggering, NoPeriodicTriggering); + Logger.WarnFormat("The circuit breaker for {0} is now in the armed state", name); + } + + return Task.Delay(TimeSpan.FromSeconds(1), CancellationToken.None); + } + + public void Dispose() + { + //Injected + } + + void CircuitBreakerTriggered(object state) + { + if (Interlocked.Read(ref failureCount) > 0) + { + Logger.WarnFormat("The circuit breaker for {0} will now be triggered", name); + triggerAction(lastException); + } + } + + long failureCount; + Exception lastException; + + string name; + Timer timer; + TimeSpan timeToWaitBeforeTriggering; + Action triggerAction; + + static TimeSpan NoPeriodicTriggering = TimeSpan.FromMilliseconds(-1); + static ILog Logger = LogManager.GetLogger(); + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/SendsAtomicWithReceiveNativeTransactionStrategy.cs b/src/NServiceBus.MessagingBridge.Msmq/SendsAtomicWithReceiveNativeTransactionStrategy.cs new file mode 100644 index 00000000..d1002132 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/SendsAtomicWithReceiveNativeTransactionStrategy.cs @@ -0,0 +1,114 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Buffers; + using System.Collections.Generic; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Extensibility; + using Transport; + + class SendsAtomicWithReceiveNativeTransactionStrategy : ReceiveStrategy + { + public SendsAtomicWithReceiveNativeTransactionStrategy(MsmqFailureInfoStorage failureInfoStorage) + { + this.failureInfoStorage = failureInfoStorage; + } + + public override async Task ReceiveMessage(CancellationToken cancellationToken = default) + { + Message message = null; + var context = new ContextBag(); + + try + { + using (var msmqTransaction = new MessageQueueTransaction()) + { + msmqTransaction.Begin(); + if (!TryReceive(msmqTransaction, out message)) + { + return; + } + + context.Set(message); + + if (!TryExtractHeaders(message, out var headers)) + { + MovePoisonMessageToErrorQueue(message, msmqTransaction); + + msmqTransaction.Commit(); + return; + } + + var shouldCommit = await ProcessMessage(msmqTransaction, message, headers, context, cancellationToken).ConfigureAwait(false); + + if (shouldCommit) + { + msmqTransaction.Commit(); + failureInfoStorage.ClearFailureInfoForMessage(message.Id); + } + else + { + msmqTransaction.Abort(); + } + } + } + // We'll only get here if Commit/Abort/Dispose throws which should be rare. + // Note: If that happens the attempts counter will be inconsistent since the message might be picked up again before we can register the failure in the LRU cache. + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + if (message == null) + { + throw; + } + + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + } + } + + async Task ProcessMessage(MessageQueueTransaction msmqTransaction, Message message, Dictionary headers, ContextBag context, CancellationToken cancellationToken) + { + var transportTransaction = new TransportTransaction(); + + transportTransaction.Set(msmqTransaction); + + var length = (int)message.BodyStream.Length; + var buffer = ArrayPool.Shared.Rent(length); + + try + { + _ = await message.BodyStream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + var body = buffer.AsMemory(0, length); + + if (failureInfoStorage.TryGetFailureInfoForMessage(message.Id, out var failureInfo)) + { + var errorHandleResult = await HandleError(message, body, failureInfo.Exception, transportTransaction, failureInfo.NumberOfProcessingAttempts, failureInfo.Context, cancellationToken).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + return true; + } + } + + try + { + await TryProcessMessage(message.Id, headers, body, transportTransaction, context, cancellationToken).ConfigureAwait(false); + return true; + } + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + + return false; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + MsmqFailureInfoStorage failureInfoStorage; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceived.cs b/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceived.cs new file mode 100644 index 00000000..2456fef3 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceived.cs @@ -0,0 +1,45 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Collections.Generic; + + static class TimeToBeReceived + { + public static bool HasElapsed(Dictionary headers) + { + if (!TryGetTtbr(headers, out var ttbr)) + { + return false; + } + + if (!TryGetTimeSent(headers, out var timeSent)) + { + return false; + } + + var cutOff = timeSent + ttbr; + var receiveTime = DateTimeOffset.UtcNow; + + return cutOff < receiveTime; + } + + static bool TryGetTtbr(Dictionary headers, out TimeSpan ttbr) + { + ttbr = TimeSpan.Zero; + return headers.TryGetValue(Headers.TimeToBeReceived, out var ttbrString) + && TimeSpan.TryParse(ttbrString, out ttbr); + } + + static bool TryGetTimeSent(Dictionary headers, out DateTimeOffset timeSent) + { + if (headers.TryGetValue(Headers.TimeSent, out var timeSentString)) + { + timeSent = DateTimeOffsetHelper.ToDateTimeOffset(timeSentString); + return true; + } + + timeSent = DateTimeOffset.MinValue; + return false; + } + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceivedOverrideChecker.cs b/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceivedOverrideChecker.cs new file mode 100644 index 00000000..d7ee8616 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/TimeToBeReceivedOverrideChecker.cs @@ -0,0 +1,25 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + + class TimeToBeReceivedOverrideChecker + { + public static void Check(bool isTransactional, bool outBoxRunning, bool auditTTBROverridden) + { + if (!isTransactional) + { + return; + } + + if (outBoxRunning) + { + return; + } + + if (auditTTBROverridden) + { + throw new Exception("Setting a custom OverrideTimeToBeReceived for audits is not supported on transactional MSMQ."); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/TransactionScopeStrategy.cs b/src/NServiceBus.MessagingBridge.Msmq/TransactionScopeStrategy.cs new file mode 100644 index 00000000..4847b44c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/TransactionScopeStrategy.cs @@ -0,0 +1,113 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Buffers; + using System.Collections.Generic; + using MSMQ.Messaging; + using System.Threading; + using System.Threading.Tasks; + using System.Transactions; + using NServiceBus.Extensibility; + using Transport; + + class TransactionScopeStrategy : ReceiveStrategy + { + public TransactionScopeStrategy(TransactionOptions transactionOptions, MsmqFailureInfoStorage failureInfoStorage) + { + this.transactionOptions = transactionOptions; + this.failureInfoStorage = failureInfoStorage; + } + + public override async Task ReceiveMessage(CancellationToken cancellationToken = default) + { + Message message = null; + var context = new ContextBag(); + + try + { + using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) + { + if (!TryReceive(MessageQueueTransactionType.Automatic, out message)) + { + return; + } + + context.Set(message); + + if (!TryExtractHeaders(message, out var headers)) + { + MovePoisonMessageToErrorQueue(message, MessageQueueTransactionType.Automatic); + + scope.Complete(); + return; + } + + var shouldCommit = await ProcessMessage(message, headers, context, cancellationToken).ConfigureAwait(false); + + if (!shouldCommit) + { + return; + } + + scope.Complete(); + } + + failureInfoStorage.ClearFailureInfoForMessage(message.Id); + } + // We'll only get here if Complete/Dispose throws which should be rare. + // Note: If that happens the attempts counter will be inconsistent since the message might be picked up again before we can register the failure in the LRU cache. + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + if (message == null) + { + throw; + } + + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + } + } + + async Task ProcessMessage(Message message, Dictionary headers, ContextBag context, CancellationToken cancellationToken) + { + var transportTransaction = new TransportTransaction(); + transportTransaction.Set(Transaction.Current); + + var length = (int)message.BodyStream.Length; + var buffer = ArrayPool.Shared.Rent(length); + + try + { + _ = await message.BodyStream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false); + var body = buffer.AsMemory(0, length); + + if (failureInfoStorage.TryGetFailureInfoForMessage(message.Id, out var failureInfo)) + { + var errorHandleResult = await HandleError(message, body, failureInfo.Exception, transportTransaction, failureInfo.NumberOfProcessingAttempts, failureInfo.Context, cancellationToken).ConfigureAwait(false); + + if (errorHandleResult == ErrorHandleResult.Handled) + { + return true; + } + } + + try + { + await TryProcessMessage(message.Id, headers, body, transportTransaction, context, cancellationToken).ConfigureAwait(false); + return true; + } + catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) + { + failureInfoStorage.RecordFailureInfoForMessage(message.Id, ex, context); + return false; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + TransactionOptions transactionOptions; + MsmqFailureInfoStorage failureInfoStorage; + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/Utils/AsyncTimer.cs b/src/NServiceBus.MessagingBridge.Msmq/Utils/AsyncTimer.cs new file mode 100644 index 00000000..84aed396 --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Utils/AsyncTimer.cs @@ -0,0 +1,62 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using NServiceBus.Logging; + + class AsyncTimer : IAsyncTimer + { + public void Start(Func callback, TimeSpan interval, Action errorCallback) + { + tokenSource = new CancellationTokenSource(); + + // no Task.Run() here because RunAndSwallowExceptions immediately yields with an await + task = RunAndSwallowExceptions(callback, interval, errorCallback, tokenSource.Token); + } + + static async Task RunAndSwallowExceptions(Func callback, TimeSpan interval, Action errorCallback, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(interval, cancellationToken).ConfigureAwait(false); + await callback(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex.IsCausedBy(cancellationToken)) + { + // private token, sender is being stopped, log the exception in case the stack trace is ever needed for debugging + Logger.Debug("Operation canceled while stopping timer.", ex); + break; + } + catch (Exception ex) + { + try + { + errorCallback(ex); + } + catch (Exception errorCallbackEx) + { + Logger.Error("Error callback failed.", errorCallbackEx); + } + } + } + } + + public async Task Stop(CancellationToken cancellationToken = default) + { + tokenSource?.Cancel(); + + // await the task before disposing to avoid an ObjectDisposedException when passing the token to Task.Delay or elsewhere + await (task ?? Task.CompletedTask).ConfigureAwait(false); + + tokenSource?.Dispose(); + } + + Task task; + CancellationTokenSource tokenSource; + + static readonly ILog Logger = LogManager.GetLogger(); + } +} diff --git a/src/NServiceBus.MessagingBridge.Msmq/Utils/Guard.cs b/src/NServiceBus.MessagingBridge.Msmq/Utils/Guard.cs new file mode 100644 index 00000000..3e74378c --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Utils/Guard.cs @@ -0,0 +1,43 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + + static class Guard + { + public static void AgainstNull(string argumentName, object value) + { + if (value == null) + { + throw new ArgumentNullException(argumentName); + } + } + + public static void AgainstNullAndEmpty(string argumentName, string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(argumentName); + } + } + + public static void AgainstNegativeAndZero(string argumentName, TimeSpan? value) + { + if (value == null) + { + return; + } + if (value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(argumentName); + } + } + + public static void AgainstNegativeAndZero(string argumentName, int value) + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(argumentName); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Utils/IAsyncTimer.cs b/src/NServiceBus.MessagingBridge.Msmq/Utils/IAsyncTimer.cs new file mode 100644 index 00000000..ea33662d --- /dev/null +++ b/src/NServiceBus.MessagingBridge.Msmq/Utils/IAsyncTimer.cs @@ -0,0 +1,12 @@ +namespace NServiceBus.MessagingBridge.Msmq +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + interface IAsyncTimer + { + void Start(Func callback, TimeSpan interval, Action errorCallback); + Task Stop(CancellationToken cancellationToken = default); + } +} \ No newline at end of file From 685f0b50e1876626b2c7bd93d64d970d72416147 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 27 Sep 2023 12:59:44 -0400 Subject: [PATCH 14/18] Rename TransportDefinition class --- ...smqTransport.cs => MsmqBridgeTransport.cs} | 6 +-- .../MsmqConfigurationExtensions.cs | 42 +++++++++---------- .../MsmqMessageDispatcher.cs | 4 +- .../MsmqSubscriptionPersistence.cs | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) rename src/NServiceBus.MessagingBridge.Msmq/{MsmqTransport.cs => MsmqBridgeTransport.cs} (98%) diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs similarity index 98% rename from src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs rename to src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs index 1970c965..5daf2a56 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransport.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs @@ -17,14 +17,14 @@ namespace NServiceBus /// /// Transport definition for MSMQ. /// - public partial class MsmqTransport : TransportDefinition, IMessageDrivenSubscriptionTransport + public partial class MsmqBridgeTransport : TransportDefinition, IMessageDrivenSubscriptionTransport { const string TimeoutsQueueQualifier = "timeouts"; /// - /// Creates a new instance of for configuration. + /// Creates a new instance of for configuration. /// - public MsmqTransport() : base(TransportTransactionMode.TransactionScope, true, false, true) + public MsmqBridgeTransport() : base(TransportTransactionMode.TransactionScope, true, false, true) { } diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs index 0df62ff7..b752e962 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs @@ -16,12 +16,12 @@ public static partial class MsmqConfigurationExtensions /// /// Configures the endpoint to use MSMQ to send and receive messages. /// - public static TransportExtensions UseTransport(this EndpointConfiguration endpointConfiguration) - where TTransport : MsmqTransport + public static TransportExtensions UseTransport(this EndpointConfiguration endpointConfiguration) + where TTransport : MsmqBridgeTransport { - var msmqTransport = new MsmqTransport(); + var msmqTransport = new MsmqBridgeTransport(); var routingSettings = endpointConfiguration.UseTransport(msmqTransport); - return new TransportExtensions(msmqTransport, routingSettings); + return new TransportExtensions(msmqTransport, routingSettings); } /// @@ -29,7 +29,7 @@ public static TransportExtensions UseTransport(this E /// /// MSMQ Transport configuration object. /// The instance of a distribution strategy. - public static void SetMessageDistributionStrategy(this RoutingSettings config, DistributionStrategy distributionStrategy) + public static void SetMessageDistributionStrategy(this RoutingSettings config, DistributionStrategy distributionStrategy) { Guard.AgainstNull(nameof(config), config); Guard.AgainstNull(nameof(distributionStrategy), distributionStrategy); @@ -41,7 +41,7 @@ public static void SetMessageDistributionStrategy(this RoutingSettings /// MSMQ Transport configuration object. - public static InstanceMappingFileSettings InstanceMappingFile(this RoutingSettings config) + public static InstanceMappingFileSettings InstanceMappingFile(this RoutingSettings config) { Guard.AgainstNull(nameof(config), config); return new InstanceMappingFileSettings(config.GetSettings()); @@ -58,8 +58,8 @@ public static InstanceMappingFileSettings InstanceMappingFile(this RoutingSettin /// The only exception to this rule is received messages with corrupted headers. These messages will be forwarded to the /// error queue with no label applied. /// - public static TransportExtensions ApplyLabelToMessages( - this TransportExtensions transport, + public static TransportExtensions ApplyLabelToMessages( + this TransportExtensions transport, Func, string> labelGenerator) { transport.Transport.ApplyCustomLabelToOutgoingMessages = labelGenerator; @@ -76,8 +76,8 @@ public static TransportExtensions ApplyLabelToMessages( /// The transport settings to configure. /// Transaction timeout duration. /// Transaction isolation level. - public static TransportExtensions TransactionScopeOptions( - this TransportExtensions transport, + public static TransportExtensions TransactionScopeOptions( + this TransportExtensions transport, TimeSpan? timeout = null, IsolationLevel? isolationLevel = null) { @@ -88,8 +88,8 @@ public static TransportExtensions TransactionScopeOptions( /// /// Moves messages that have exceeded their TimeToBeReceived to the dead letter queue instead of discarding them. /// - public static TransportExtensions UseDeadLetterQueueForMessagesWithTimeToBeReceived( - this TransportExtensions transport) + public static TransportExtensions UseDeadLetterQueueForMessagesWithTimeToBeReceived( + this TransportExtensions transport) { transport.Transport.UseDeadLetterQueueForMessagesWithTimeToBeReceived = true; return transport; @@ -104,7 +104,7 @@ public static TransportExtensions UseDeadLetterQueueForMessagesWi /// The installers might still need to be enabled to fulfill the installation needs of other components, but this method allows /// scripts to be used for queue creation instead. /// - public static TransportExtensions DisableInstaller(this TransportExtensions transport) + public static TransportExtensions DisableInstaller(this TransportExtensions transport) { transport.Transport.CreateQueues = false; return transport; @@ -115,7 +115,7 @@ public static TransportExtensions DisableInstaller(this Transport /// in the dead letter queue. Therefore this setting must only be used where loss of messages /// is an acceptable scenario. /// - public static TransportExtensions DisableDeadLetterQueueing(this TransportExtensions transport) + public static TransportExtensions DisableDeadLetterQueueing(this TransportExtensions transport) { transport.Transport.UseDeadLetterQueue = false; return transport; @@ -127,7 +127,7 @@ public static TransportExtensions DisableDeadLetterQueueing(this /// Turning connection caching off will negatively impact the message throughput in /// most scenarios. /// - public static TransportExtensions DisableConnectionCachingForSends(this TransportExtensions transport) + public static TransportExtensions DisableConnectionCachingForSends(this TransportExtensions transport) { transport.Transport.UseConnectionCache = false; return transport; @@ -138,7 +138,7 @@ public static TransportExtensions DisableConnectionCachingForSend /// an exception during processing will not be rolled back to the queue. Therefore this setting must only /// be used where loss of messages is an acceptable scenario. /// - public static TransportExtensions UseNonTransactionalQueues(this TransportExtensions transport) + public static TransportExtensions UseNonTransactionalQueues(this TransportExtensions transport) { transport.Transport.UseTransactionalQueues = false; return transport; @@ -149,7 +149,7 @@ public static TransportExtensions UseNonTransactionalQueues(this /// Should be used ONLY when debugging as it can /// potentially use up the MSMQ journal storage quota based on the message volume. /// - public static TransportExtensions EnableJournaling(this TransportExtensions transport) + public static TransportExtensions EnableJournaling(this TransportExtensions transport) { transport.Transport.UseJournalQueue = true; return transport; @@ -158,7 +158,7 @@ public static TransportExtensions EnableJournaling(this Transport /// /// Overrides the Time-To-Reach-Queue (TTRQ) timespan. The default value if not set is Message.InfiniteTimeout /// - public static TransportExtensions TimeToReachQueue(this TransportExtensions transport, TimeSpan timeToReachQueue) + public static TransportExtensions TimeToReachQueue(this TransportExtensions transport, TimeSpan timeToReachQueue) { transport.Transport.TimeToReachQueue = timeToReachQueue; return transport; @@ -167,7 +167,7 @@ public static TransportExtensions TimeToReachQueue(this Transport /// /// Disables native Time-To-Be-Received (TTBR) when combined with transactions. /// - public static TransportExtensions DisableNativeTimeToBeReceivedInTransactions(this TransportExtensions transport) + public static TransportExtensions DisableNativeTimeToBeReceivedInTransactions(this TransportExtensions transport) { transport.Transport.UseNonNativeTimeToBeReceivedInTransactions = true; return transport; @@ -176,7 +176,7 @@ public static TransportExtensions DisableNativeTimeToBeReceivedIn /// /// Configures native delayed delivery. /// - public static DelayedDeliverySettings NativeDelayedDelivery(this TransportExtensions config, IDelayedMessageStore delayedMessageStore) + public static DelayedDeliverySettings NativeDelayedDelivery(this TransportExtensions config, IDelayedMessageStore delayedMessageStore) { Guard.AgainstNull(nameof(delayedMessageStore), delayedMessageStore); config.Transport.DelayedDelivery = new DelayedDeliverySettings(delayedMessageStore); @@ -186,7 +186,7 @@ public static DelayedDeliverySettings NativeDelayedDelivery(this TransportExtens /// /// Ignore incoming Time-To-Be-Received (TTBR) headers. By default an expired TTBR header will result in the message to be discarded. /// - public static TransportExtensions IgnoreIncomingTimeToBeReceivedHeaders(this TransportExtensions transport) + public static TransportExtensions IgnoreIncomingTimeToBeReceivedHeaders(this TransportExtensions transport) { transport.Transport.IgnoreIncomingTimeToBeReceivedHeaders = true; return transport; diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs index 528cc26c..e9dfc60a 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs @@ -14,11 +14,11 @@ namespace NServiceBus.MessagingBridge.Msmq class MsmqMessageDispatcher : IMessageDispatcher { - readonly MsmqTransport transportSettings; + readonly MsmqBridgeTransport transportSettings; readonly string timeoutsQueue; readonly Action onSendCallback; - public MsmqMessageDispatcher(MsmqTransport transportSettings, string timeoutsQueue, Action onSendCallback = null) + public MsmqMessageDispatcher(MsmqBridgeTransport transportSettings, string timeoutsQueue, Action onSendCallback = null) { this.transportSettings = transportSettings; this.timeoutsQueue = timeoutsQueue; diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs index bc113fc5..e5fad5e1 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs @@ -18,7 +18,7 @@ protected override void Setup(FeatureConfigurationContext context) context.Settings.Get().BindSending(configuredQueueName); - var transportSettings = context.Settings.Get() as MsmqTransport; + var transportSettings = context.Settings.Get() as MsmqBridgeTransport; var queue = new MsmqSubscriptionStorageQueue(MsmqAddress.Parse(configuredQueueName), transportSettings.UseTransactionalQueues); var storage = new MsmqSubscriptionStorage(queue); From ea5a9a371218d116828dc36e94bc2965153c4afc Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 27 Sep 2023 13:02:45 -0400 Subject: [PATCH 15/18] Re-enable MSMQ tests --- .../ConfigureMsmqTransportTestExecution.cs | 110 +++++++++--------- .../TestableMsmqTransport.cs | 40 +++---- 2 files changed, 74 insertions(+), 76 deletions(-) diff --git a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs index c2673b70..3b8e81e7 100644 --- a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs +++ b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs @@ -1,83 +1,81 @@ using System; -//using System.Collections.Generic; -//using System.Linq; -//using MSMQ.Messaging; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using MSMQ.Messaging; using NServiceBus; using NServiceBus.AcceptanceTesting.Support; -//using NServiceBus.Transport; class ConfigureMsmqTransportTestExecution : IConfigureTransportTestExecution { public BridgeTransportDefinition GetBridgeTransport() { - //var transportDefinition = new TestableMsmqTransport(); + var transportDefinition = new TestableMsmqTransport(); return new BridgeTransportDefinition { - TransportDefinition = null,//transportDefinition, - //Cleanup = (ct) => Cleanup(transportDefinition, ct) + TransportDefinition = transportDefinition, + Cleanup = (ct) => Cleanup(transportDefinition, ct) }; } public Func ConfigureTransportForEndpoint(EndpointConfiguration endpointConfiguration, PublisherMetadata publisherMetadata) { - //var transportDefinition = new TestableMsmqTransport(); - //var routingConfig = endpointConfiguration.UseTransport(transportDefinition); - //endpointConfiguration.UsePersistence(); + var transportDefinition = new TestableMsmqTransport(); + var routingConfig = endpointConfiguration.UseTransport(transportDefinition); + endpointConfiguration.UsePersistence(); - //foreach (var publisher in publisherMetadata.Publishers) - //{ - // foreach (var eventType in publisher.Events) - // { - // routingConfig.RegisterPublisher(eventType, publisher.PublisherName); - // } - //} + foreach (var publisher in publisherMetadata.Publishers) + { + foreach (var eventType in publisher.Events) + { + routingConfig.RegisterPublisher(eventType, publisher.PublisherName); + } + } - //return (ct) => Cleanup(transportDefinition, ct); - return (_) => Task.CompletedTask; + return (ct) => Cleanup(transportDefinition, ct); } - //static Task Cleanup(TestableMsmqTransport msmqTransport, CancellationToken cancellationToken) - //{ - // var allQueues = MessageQueue.GetPrivateQueuesByMachine("localhost"); - // var queuesToBeDeleted = new List(); + static Task Cleanup(TestableMsmqTransport msmqTransport, CancellationToken cancellationToken) + { + var allQueues = MessageQueue.GetPrivateQueuesByMachine("localhost"); + var queuesToBeDeleted = new List(); - // foreach (var messageQueue in allQueues) - // { - // using (messageQueue) - // { - // if (msmqTransport.ReceiveQueues.Any(ra => - // { - // var indexOfAt = ra.IndexOf("@", StringComparison.Ordinal); - // if (indexOfAt >= 0) - // { - // ra = ra.Substring(0, indexOfAt); - // } - // return messageQueue.QueueName.StartsWith(@"private$\" + ra, StringComparison.OrdinalIgnoreCase); - // })) - // { - // queuesToBeDeleted.Add(messageQueue.Path); - // } - // } - // } + foreach (var messageQueue in allQueues) + { + using (messageQueue) + { + if (msmqTransport.ReceiveQueues.Any(ra => + { + var indexOfAt = ra.IndexOf("@", StringComparison.Ordinal); + if (indexOfAt >= 0) + { + ra = ra.Substring(0, indexOfAt); + } + return messageQueue.QueueName.StartsWith(@"private$\" + ra, StringComparison.OrdinalIgnoreCase); + })) + { + queuesToBeDeleted.Add(messageQueue.Path); + } + } + } - // foreach (var queuePath in queuesToBeDeleted) - // { - // try - // { - // MessageQueue.Delete(queuePath); - // Console.WriteLine("Deleted '{0}' queue", queuePath); - // } - // catch (Exception) - // { - // Console.WriteLine("Could not delete queue '{0}'", queuePath); - // } - // } + foreach (var queuePath in queuesToBeDeleted) + { + try + { + MessageQueue.Delete(queuePath); + Console.WriteLine("Deleted '{0}' queue", queuePath); + } + catch (Exception) + { + Console.WriteLine("Could not delete queue '{0}'", queuePath); + } + } - // MessageQueue.ClearConnectionCache(); + MessageQueue.ClearConnectionCache(); - // return Task.CompletedTask; - //} + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs b/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs index 45085776..4e5f23ba 100644 --- a/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs +++ b/src/AcceptanceTests.Msmq/TestableMsmqTransport.cs @@ -1,24 +1,24 @@ -//using System; -//using System.Linq; -//using System.Threading; -//using System.Threading.Tasks; -//using NServiceBus; -//using NServiceBus.Transport; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NServiceBus; +using NServiceBus.Transport; -///// -///// A dedicated subclass of the MsmqTransport that enables us to intercept the receive queues for the test. -///// -//class TestableMsmqTransport : MsmqTransport -//{ -// public string[] ReceiveQueues = new string[0]; +/// +/// A dedicated subclass of the MsmqBridgeTransport that enables us to intercept the receive queues for the test. +/// +class TestableMsmqTransport : MsmqBridgeTransport +{ + public string[] ReceiveQueues = Array.Empty(); -// public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) -// { -// MessageEnumeratorTimeout = TimeSpan.FromMilliseconds(10); + public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default) + { + MessageEnumeratorTimeout = TimeSpan.FromMilliseconds(10); -// var infrastructure = await base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken).ConfigureAwait(false); -// ReceiveQueues = infrastructure.Receivers.Select(r => r.Value.ReceiveAddress).ToArray(); + var infrastructure = await base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken).ConfigureAwait(false); + ReceiveQueues = infrastructure.Receivers.Select(r => r.Value.ReceiveAddress).ToArray(); -// return infrastructure; -// } -//} \ No newline at end of file + return infrastructure; + } +} \ No newline at end of file From 21af24a807b5b1aee5ef43b435e1f1daac6cc883 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Wed, 27 Sep 2023 13:39:34 -0400 Subject: [PATCH 16/18] Add workaround for NETSDK1073 error --- src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj | 6 ++++++ .../NServiceBus.MessagingBridge.Msmq.csproj | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj index 45450221..2ea63437 100644 --- a/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj +++ b/src/AcceptanceTests.Msmq/AcceptanceTests.Msmq.csproj @@ -25,4 +25,10 @@ + + + + false + + diff --git a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj index 2095f252..b8d82f04 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj +++ b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj @@ -21,4 +21,10 @@ + + + + false + + From 9c648469b076945c8c1b0d1368c2be736884e161 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 29 Sep 2023 12:18:30 -0400 Subject: [PATCH 17/18] Remove delayed delivery support in MSMQ --- .../DelayedDelivery/DelayedDeliveryPump.cs | 154 -------- .../DelayedDeliverySettings.cs | 98 ----- .../DelayedDelivery/DelayedMessage.cs | 41 -- .../DueDelayedMessagePoller.cs | 359 ------------------ .../DelayedDelivery/IDelayedMessageStore.cs | 57 --- .../DelayedDelivery/Sql/SqlConstants.cs | 41 -- .../DelayedDelivery/Sql/SqlNameHelper.cs | 33 -- .../Sql/SqlServerDelayedMessageStore.cs | 157 -------- .../Sql/TimeoutTableCreator.cs | 52 --- .../MsmqBridgeTransport.cs | 85 +---- .../MsmqConfigurationExtensions.cs | 10 - .../MsmqMessageDispatcher.cs | 112 +----- .../MsmqTransportInfrastructure.cs | 22 +- .../NServiceBus.MessagingBridge.Msmq.csproj | 1 - 14 files changed, 9 insertions(+), 1213 deletions(-) delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs deleted file mode 100644 index bc356c1c..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliveryPump.cs +++ /dev/null @@ -1,154 +0,0 @@ -namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using System.Transactions; - using Faults; - using Logging; - using NServiceBus.Transport; - using Routing; - - class DelayedDeliveryPump - { - public DelayedDeliveryPump(MsmqMessageDispatcher dispatcher, - DueDelayedMessagePoller poller, - IDelayedMessageStore storage, - MessagePump messagePump, - string errorQueue, - int numberOfRetries, - Action criticalErrorAction, - TimeSpan timeToWaitForStoreCircuitBreaker, - Dictionary faultMetadata, - TransportTransactionMode transportTransactionMode) - { - this.dispatcher = dispatcher; - this.poller = poller; - this.storage = storage; - this.numberOfRetries = numberOfRetries; - this.faultMetadata = faultMetadata; - pump = messagePump; - this.errorQueue = errorQueue; - - txOption = transportTransactionMode == TransportTransactionMode.TransactionScope - ? TransactionScopeOption.Required - : TransactionScopeOption.RequiresNew; - - storeCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("DelayedDeliveryStore", timeToWaitForStoreCircuitBreaker, - ex => criticalErrorAction("Failed to store delayed message", ex, CancellationToken.None)); - } - - public async Task Start(CancellationToken cancellationToken = default) - { - await pump.Initialize(PushRuntimeSettings.Default, TimeoutReceived, OnError, cancellationToken).ConfigureAwait(false); - await pump.StartReceive(cancellationToken).ConfigureAwait(false); - poller.Start(); - } - - public async Task Stop(CancellationToken cancellationToken = default) - { - await pump.StopReceive(cancellationToken).ConfigureAwait(false); - await poller.Stop(cancellationToken).ConfigureAwait(false); - } - - async Task TimeoutReceived(MessageContext context, CancellationToken cancellationToken) - { - if (!context.Headers.TryGetValue(MsmqUtilities.PropertyHeaderPrefix + MsmqMessageDispatcher.TimeoutDestination, out var destination)) - { - throw new Exception("This message does not represent a timeout"); - } - - if (!context.Headers.TryGetValue(MsmqUtilities.PropertyHeaderPrefix + MsmqMessageDispatcher.TimeoutAt, out var atString)) - { - throw new Exception("This message does not represent a timeout"); - } - - var id = context.NativeMessageId; //Use native message ID as a key in the delayed delivery table - var at = DateTimeOffsetHelper.ToDateTimeOffset(atString); - - var message = context.Extensions.Get(); - - var diff = DateTime.UtcNow - at; - - if (diff.Ticks > 0) // Due - { - dispatcher.DispatchDelayedMessage(id, message.Extension, context.Body, destination, context.TransportTransaction); - } - else - { - var timeout = new DelayedMessage - { - Destination = destination, - MessageId = id, - Body = context.Body.ToArray(), - Time = at.UtcDateTime, - Headers = message.Extension - }; - - try - { - using (var tx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - await storage.Store(timeout, cancellationToken).ConfigureAwait(false); - tx.Complete(); - } - - storeCircuitBreaker.Success(); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - //Shutting down - return; - } - catch (Exception e) - { - await storeCircuitBreaker.Failure(e, cancellationToken).ConfigureAwait(false); - throw new Exception("Error while storing delayed message", e); - } - -#pragma warning disable PS0022 // A DateTime should not be implicitly cast to a DateTimeOffset - poller.Signal(timeout.Time); -#pragma warning restore PS0022 // A DateTime should not be implicitly cast to a DateTimeOffset - } - } - - async Task OnError(ErrorContext errorContext, CancellationToken cancellationToken) - { - Log.Error($"OnError {errorContext.Message.MessageId}", errorContext.Exception); - - if (errorContext.ImmediateProcessingFailures < numberOfRetries) - { - return ErrorHandleResult.RetryRequired; - } - - var message = errorContext.Message; - - ExceptionHeaderHelper.SetExceptionHeaders(message.Headers, errorContext.Exception); - message.Headers[FaultsHeaderKeys.FailedQ] = errorContext.ReceiveAddress; - foreach (var pair in faultMetadata) - { - message.Headers[pair.Key] = pair.Value; - } - - var outgoingMessage = new OutgoingMessage(message.NativeMessageId, message.Headers, message.Body); - var transportOperation = new TransportOperation(outgoingMessage, new UnicastAddressTag(errorQueue)); - await dispatcher.Dispatch(new TransportOperations(transportOperation), errorContext.TransportTransaction, cancellationToken).ConfigureAwait(false); - - return ErrorHandleResult.Handled; - } - - readonly MsmqMessageDispatcher dispatcher; - readonly DueDelayedMessagePoller poller; - readonly IDelayedMessageStore storage; - readonly int numberOfRetries; - readonly MessagePump pump; - readonly Dictionary faultMetadata; - readonly string errorQueue; - RepeatedFailuresOverTimeCircuitBreaker storeCircuitBreaker; - readonly TransactionScopeOption txOption; - readonly TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }; - - static readonly ILog Log = LogManager.GetLogger(); - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs deleted file mode 100644 index d4599868..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedDeliverySettings.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace NServiceBus -{ - using System; - using NServiceBus.MessagingBridge.Msmq; - - /// - /// Configures delayed delivery support. - /// - public class DelayedDeliverySettings - { - int numberOfRetries; - TimeSpan timeToTriggerStoreCircuitBreaker = TimeSpan.FromSeconds(30); - TimeSpan timeToTriggerFetchCircuitBreaker = TimeSpan.FromSeconds(30); - TimeSpan timeToTriggerDispatchCircuitBreaker = TimeSpan.FromSeconds(30); - int maximumRecoveryFailuresPerSecond = 1; - - /// - /// The store to keep delayed messages. - /// - public IDelayedMessageStore DelayedMessageStore { get; } - - /// - /// Number of retries when trying to forward due delayed messages. - /// - public int NumberOfRetries - { - get => numberOfRetries; - set - { - Guard.AgainstNegativeAndZero("value", value); - numberOfRetries = value; - } - } - - /// - /// Time to wait before triggering the circuit breaker that monitors the storing of delayed messages in the database. Defaults to 30 seconds. - /// - public TimeSpan TimeToTriggerStoreCircuitBreaker - { - get => timeToTriggerStoreCircuitBreaker; - set - { - Guard.AgainstNegativeAndZero("value", value); - timeToTriggerStoreCircuitBreaker = value; - } - } - - /// - /// Time to wait before triggering the circuit breaker that monitors the fetching of due delayed messages from the database. Defaults to 30 seconds. - /// - public TimeSpan TimeToTriggerFetchCircuitBreaker - { - get => timeToTriggerFetchCircuitBreaker; - set - { - Guard.AgainstNegativeAndZero("value", value); - timeToTriggerFetchCircuitBreaker = value; - } - } - - /// - /// Time to wait before triggering the circuit breaker that monitors the dispatching of due delayed messages to the destination. Defaults to 30 seconds. - /// - public TimeSpan TimeToTriggerDispatchCircuitBreaker - { - get => timeToTriggerDispatchCircuitBreaker; - set - { - Guard.AgainstNegativeAndZero("value", value); - timeToTriggerDispatchCircuitBreaker = value; - } - } - - /// - /// Maximum number of recovery failures per second that triggers the recovery circuit breaker. Recovery attempts are attempts to increment the failure - /// counter after a failed dispatch and forwarding messages to the error queue. Defaults to 1/s. - /// - public int MaximumRecoveryFailuresPerSecond - { - get => maximumRecoveryFailuresPerSecond; - set - { - Guard.AgainstNegativeAndZero("value", value); - maximumRecoveryFailuresPerSecond = value; - } - } - - /// - /// Configures delayed delivery. - /// - /// The store to keep delayed messages. - public DelayedDeliverySettings(IDelayedMessageStore delayedMessageStore) - { - Guard.AgainstNull(nameof(delayedMessageStore), delayedMessageStore); - DelayedMessageStore = delayedMessageStore; - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs deleted file mode 100644 index 740d18d6..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DelayedMessage.cs +++ /dev/null @@ -1,41 +0,0 @@ - -namespace NServiceBus -{ - using System; - - /// - /// Represents a delayed message. - /// - public class DelayedMessage - { - /// - /// Date and time the message is due. - /// - public DateTime Time { get; set; } - - /// - /// Native message ID - /// - public string MessageId { get; set; } - - /// - /// The body of the message. - /// - public byte[] Body { get; set; } - - /// - /// The serialized headers of the message - /// - public byte[] Headers { get; set; } - - /// - /// The address of the destination queue - /// - public string Destination { get; set; } - - /// - /// The number of attempt already made to forward the message to its destination. - /// - public int NumberOfRetries { get; set; } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs deleted file mode 100644 index 360ee4bc..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/DueDelayedMessagePoller.cs +++ /dev/null @@ -1,359 +0,0 @@ -namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Channels; - using System.Threading.Tasks; - using System.Transactions; - using Faults; - using Logging; - using NServiceBus.Transport; - using Routing; - using Unicast.Queuing; - - class DueDelayedMessagePoller - { - public DueDelayedMessagePoller(MsmqMessageDispatcher dispatcher, - IDelayedMessageStore delayedMessageStore, - int numberOfRetries, - Action criticalErrorAction, - string timeoutsErrorQueue, - Dictionary faultMetadata, - TransportTransactionMode transportTransactionMode, - TimeSpan timeToTriggerFetchCircuitBreaker, - TimeSpan timeToTriggerDispatchCircuitBreaker, - int maximumRecoveryFailuresPerSecond, - string timeoutsQueueTransportAddress) - { - txOption = transportTransactionMode == TransportTransactionMode.TransactionScope - ? TransactionScopeOption.Required - : TransactionScopeOption.RequiresNew; - this.delayedMessageStore = delayedMessageStore; - errorQueue = timeoutsErrorQueue; - this.faultMetadata = faultMetadata; - this.timeoutsQueueTransportAddress = timeoutsQueueTransportAddress; - this.numberOfRetries = numberOfRetries; - this.dispatcher = dispatcher; - fetchCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqDelayedMessageFetch", timeToTriggerFetchCircuitBreaker, - ex => criticalErrorAction("Failed to fetch due delayed messages from the storage", ex, tokenSource?.Token ?? CancellationToken.None)); - - dispatchCircuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker("MsmqDelayedMessageDispatch", timeToTriggerDispatchCircuitBreaker, - ex => criticalErrorAction("Failed to dispatch delayed messages to destination", ex, tokenSource?.Token ?? CancellationToken.None)); - - failureHandlingCircuitBreaker = new FailureRateCircuitBreaker("MsmqDelayedMessageFailureHandling", maximumRecoveryFailuresPerSecond, - ex => criticalErrorAction("Failed to execute error handling for delayed message forwarding", ex, tokenSource?.Token ?? CancellationToken.None)); - - signalQueue = Channel.CreateBounded(1); - taskQueue = Channel.CreateBounded(2); - } - - public void Start() - { - tokenSource = new CancellationTokenSource(); - loopTask = Task.Run(() => Loop(tokenSource.Token)); - completionTask = Task.Run(() => AwaitHandleTasks()); - } - - public async Task Stop(CancellationToken cancellationToken = default) - { - tokenSource.Cancel(); - await loopTask.ConfigureAwait(false); - await completionTask.ConfigureAwait(false); - } - - public void Signal(DateTimeOffset timeoutTime) - { - //If the next timeout is within a minute from now, trigger the poller - if (DateTimeOffset.UtcNow.Add(MaxSleepDuration) > timeoutTime) - { - //If there is something already in the queue we are fine. - signalQueue.Writer.TryWrite(true); - } - } - - /// - /// This method does not accept a cancellation token because it needs to finish all the tasks that have been started even if cancellation is under way. - /// -#pragma warning disable PS0018 // A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext - async Task AwaitHandleTasks() -#pragma warning restore PS0018 // A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext - { - while (await taskQueue.Reader.WaitToReadAsync().ConfigureAwait(false)) //if this returns false the channel is completed - { - while (taskQueue.Reader.TryRead(out var task)) - { - try - { - await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Log.Debug("A shutdown was triggered and Poll task has been cancelled."); - } - catch (Exception e) - { - failureHandlingCircuitBreaker.Failure(e); - } - } - } - } - - async Task Loop(CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { -#pragma warning disable PS0021 // Highlight when a try block passes multiple cancellation tokens - try -#pragma warning restore PS0021 // Highlight when a try block passes multiple cancellation tokens - { - - //We create a TaskCompletionSource source here and pass it to spawned Poll task. Once the Poll task is done with fetching - //the next due timeout, we continue with the loop. This allows us to run the Poll tasks in an overlapping way so that at any time there is - //one Poll task fetching and one dispatching. - - var completionSource = new TaskCompletionSource(); - var handleTask = Poll(completionSource, cancellationToken); - await taskQueue.Writer.WriteAsync(handleTask, cancellationToken).ConfigureAwait(false); - var nextPoll = await completionSource.Task.ConfigureAwait(false); - - if (nextPoll.HasValue) - { - //We wait either for a signal that a new delayed message has been stored or until next timeout should be due - //After waiting we cancel the token so that the task waiting for the signal is cancelled and does not "eat" the next - //signal when it is raised in the next iteration of this loop - using (var waitCancelled = new CancellationTokenSource()) - using (var combinedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, waitCancelled.Token)) - { - var waitTask = WaitIfNeeded(nextPoll.Value, combinedSource.Token); - var signalTask = WaitForSignal(combinedSource.Token); - - await Task.WhenAny(waitTask, signalTask).ConfigureAwait(false); - waitCancelled.Cancel(); - } - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - Log.Debug("A shutdown was triggered, canceling the Loop operation."); - break; - } - catch (Exception e) - { - Log.Error("Failed to poll and dispatch due timeouts from storage.", e); - //Poll and HandleDueDelayedMessage have their own exception handling logic so any exception here is likely going to be related to the transaction itself - await fetchCircuitBreaker.Failure(e, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - //No matter what we need to complete the writer so that we can stop gracefully - taskQueue.Writer.Complete(); - } - } - - async Task WaitForSignal(CancellationToken cancellationToken) - { - try - { - await signalQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - Log.Debug("No new delayed messages have been stored while waiting for the next due delayed message."); - } - } - - Task WaitIfNeeded(DateTimeOffset nextPoll, CancellationToken cancellationToken) - { - var waitTime = nextPoll - DateTimeOffset.UtcNow; - if (waitTime > TimeSpan.Zero) - { - if (waitTime > MaxSleepDuration) - { - // Task.Delay() throws for times > int.MaxValue ms, which is ~24.85 days - waitTime = MaxSleepDuration; - } - return Task.Delay(waitTime, cancellationToken); - } - - return Task.CompletedTask; - } - - async Task Poll(TaskCompletionSource result, CancellationToken cancellationToken) - { - DelayedMessage timeout = null; - DateTimeOffset now = DateTimeOffset.UtcNow; - try - { - using (var tx = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - timeout = await delayedMessageStore.FetchNextDueTimeout(now, cancellationToken).ConfigureAwait(false); - fetchCircuitBreaker.Success(); - - if (timeout != null) - { - result.SetResult(null); - HandleDueDelayedMessage(timeout, cancellationToken); - - var success = await delayedMessageStore.Remove(timeout, cancellationToken).ConfigureAwait(false); - if (!success) - { - Log.WarnFormat("Potential more-than-once dispatch as delayed message already removed from storage."); - } - } - else - { - using (new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - var nextDueTimeout = await delayedMessageStore.Next(cancellationToken).ConfigureAwait(false); - if (nextDueTimeout.HasValue) - { - result.SetResult(nextDueTimeout); - } - else - { - //If no timeouts, wait a while - result.SetResult(now.Add(MaxSleepDuration)); - } - } - } - tx.Complete(); - } - } - catch (QueueNotFoundException exception) - { - if (timeout != null) - { - await TrySendDelayedMessageToErrorQueue(timeout, exception, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - //Shutting down. Bubble up the exception. Will be awaited by AwaitHandleTasks method that catches OperationCanceledExceptions - throw; - } - catch (Exception exception) - { - Log.Error("Failure during timeout polling.", exception); - if (timeout != null) - { - await dispatchCircuitBreaker.Failure(exception, cancellationToken).ConfigureAwait(false); - using (var scope = new TransactionScope(TransactionScopeOption.RequiresNew, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - await delayedMessageStore.IncrementFailureCount(timeout, cancellationToken).ConfigureAwait(false); - scope.Complete(); - } - - if (timeout.NumberOfRetries > numberOfRetries) - { - await TrySendDelayedMessageToErrorQueue(timeout, exception, cancellationToken).ConfigureAwait(false); - } - } - else - { - await fetchCircuitBreaker.Failure(exception, cancellationToken).ConfigureAwait(false); - } - } - finally - { - //In case we failed before SetResult - result.TrySetResult(null); - } - } - - void HandleDueDelayedMessage(DelayedMessage timeout, CancellationToken cancellationToken) - { - TimeSpan diff = DateTimeOffset.UtcNow - new DateTimeOffset(timeout.Time, TimeSpan.Zero); - - Log.DebugFormat("Timeout {0} over due for {1}", timeout.MessageId, diff); - - using (var tx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set(Transaction.Current); - dispatcher.DispatchDelayedMessage( - timeout.MessageId, - timeout.Headers, - timeout.Body, - timeout.Destination, - transportTransaction - ); - - tx.Complete(); - } - - dispatchCircuitBreaker.Success(); - } - - async Task TrySendDelayedMessageToErrorQueue(DelayedMessage timeout, Exception exception, CancellationToken cancellationToken) - { - try - { - bool success = await delayedMessageStore.Remove(timeout, cancellationToken).ConfigureAwait(false); - - if (!success) - { - // Already dispatched - return; - } - - Dictionary headersAndProperties = MsmqUtilities.DeserializeMessageHeaders(timeout.Headers); - - ExceptionHeaderHelper.SetExceptionHeaders(headersAndProperties, exception); - headersAndProperties[FaultsHeaderKeys.FailedQ] = timeoutsQueueTransportAddress; - foreach (KeyValuePair pair in faultMetadata) - { - headersAndProperties[pair.Key] = pair.Value; - } - - Log.InfoFormat("Move {0} to error queue", timeout.MessageId); - using (var transportTx = new TransactionScope(txOption, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) - { - var transportTransaction = new TransportTransaction(); - transportTransaction.Set(Transaction.Current); - - var outgoingMessage = new OutgoingMessage(timeout.MessageId, headersAndProperties, timeout.Body); - var transportOperation = new TransportOperation(outgoingMessage, new UnicastAddressTag(errorQueue)); - await dispatcher.Dispatch(new TransportOperations(transportOperation), transportTransaction, CancellationToken.None) - .ConfigureAwait(false); - - transportTx.Complete(); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - //Shutting down - Log.Debug("Aborted sending delayed message to error queue due to shutdown."); - } - catch (Exception ex) - { - Log.Error($"Failed to move delayed message {timeout.MessageId} to the error queue {errorQueue} after {timeout.NumberOfRetries} failed attempts at dispatching it to the destination", ex); - } - } - - static readonly ILog Log = LogManager.GetLogger(); - static readonly TimeSpan MaxSleepDuration = TimeSpan.FromMinutes(1); - - readonly Dictionary faultMetadata; - readonly string timeoutsQueueTransportAddress; - - IDelayedMessageStore delayedMessageStore; - MsmqMessageDispatcher dispatcher; - string errorQueue; - int numberOfRetries; - RepeatedFailuresOverTimeCircuitBreaker fetchCircuitBreaker; - RepeatedFailuresOverTimeCircuitBreaker dispatchCircuitBreaker; - FailureRateCircuitBreaker failureHandlingCircuitBreaker; - Task loopTask; - Task completionTask; - Channel signalQueue; - Channel taskQueue; - CancellationTokenSource tokenSource; - TransactionScopeOption txOption; - TransactionOptions transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }; - } -} diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs deleted file mode 100644 index ae18afdb..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/IDelayedMessageStore.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace NServiceBus -{ - using System; - using System.Threading; - using System.Threading.Tasks; - - /// - /// Represents a store for delayed messages. - /// - public interface IDelayedMessageStore - { - /// - /// Initializes the storage e.g. creates required database artifacts etc. - /// - /// Name of the endpoint that hosts the delayed delivery storage. - /// The transaction mode selected for the transport. The storage implementation should throw an exception if it can't support specified - /// transaction mode e.g. TransactionScope mode requires the storage to enlist in a distributed transaction managed by the DTC. - /// The cancellation token set if the endpoint begins to shut down while the Initialize method is executing. - Task Initialize(string endpointName, TransportTransactionMode transactionMode, CancellationToken cancellationToken = default); - - /// - /// Returns the date and time set for the next delayed message to become due or null if there are no delayed messages stored. - /// - /// - Task Next(CancellationToken cancellationToken = default); - - /// - /// Stores a delayed message. - /// - /// Object representing a delayed message. - /// The cancellation token for cooperative cancellation - Task Store(DelayedMessage entity, CancellationToken cancellationToken = default); - - /// - /// Removes a due delayed message that has been dispatched to its destination from the store. - /// - /// Object representing a delayed message previously returned by FetchNextDueTimeout. - /// The cancellation token for cooperative cancellation - /// True if the removal succeeded. False if there was nothing to remove because the delayed message was already gone. - Task Remove(DelayedMessage entity, CancellationToken cancellationToken = default); - - /// - /// Increments the counter of failures for a given due delayed message. - /// - /// Object representing a delayed message previously returned by FetchNextDueTimeout. - /// The cancellation token for cooperative cancellation - /// True if the increment succeeded. False if the delayed message was already gone. - Task IncrementFailureCount(DelayedMessage entity, CancellationToken cancellationToken = default); - - /// - /// Retrieves the oldest due delayed message from the store or returns null if there is no due delayed messages. - /// - /// The point in time to which to compare the due date of the messages. - /// The cancellation token for cooperative cancellation - Task FetchNextDueTimeout(DateTimeOffset at, CancellationToken cancellationToken = default); - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs deleted file mode 100644 index 8fac1706..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlConstants.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql -{ - class SqlConstants - { - public const string SqlInsert = "INSERT INTO {0} (Id, Destination, Time, Headers, State) VALUES (@id, @destination, @time, @headers, @state);"; - public const string SqlFetch = "SELECT TOP 1 Id, Destination, Time, Headers, State, RetryCount FROM {0} WITH (READPAST, UPDLOCK, ROWLOCK) WHERE Time < @time ORDER BY Time"; - public const string SqlDelete = "DELETE {0} WHERE Id = @id"; - public const string SqlUpdate = "UPDATE {0} SET RetryCount = RetryCount + 1 WHERE Id = @id"; - public const string SqlGetNext = "SELECT TOP 1 Time FROM {0} ORDER BY Time"; - public const string SqlCreateTable = @" -if not exists ( - select * from sys.objects - where - object_id = object_id('{0}') - and type in ('U') -) -begin - create table {0} ( - Id nvarchar(250) not null primary key, - Destination nvarchar(200), - State varbinary(max), - Time datetime, - Headers varbinary(max) not null, - RetryCount INT NOT NULL default(0) - ) -end - -if not exists -( - select * - from sys.indexes - where - name = 'Index_Time' and - object_id = object_id('{0}') -) -begin - create index Index_Time on {0} (Time); -end -"; - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs deleted file mode 100644 index 0fe8446f..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlNameHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql -{ - class SqlNameHelper - { - const string Prefix = "["; - const string Suffix = "]"; - - public static string Quote(string unquotedName) - { - if (unquotedName == null) - { - return null; - } - return Prefix + unquotedName.Replace(Suffix, Suffix + Suffix) + Suffix; - } - - public static string Unquote(string quotedString) - { - if (quotedString == null) - { - return null; - } - - if (!quotedString.StartsWith(Prefix) || !quotedString.EndsWith(Suffix)) - { - return quotedString; - } - - return quotedString - .Substring(Prefix.Length, quotedString.Length - Prefix.Length - Suffix.Length).Replace(Suffix + Suffix, Suffix); - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs deleted file mode 100644 index 5af1d589..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/SqlServerDelayedMessageStore.cs +++ /dev/null @@ -1,157 +0,0 @@ -namespace NServiceBus -{ - using System; - using System.Data; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Data.SqlClient; - using NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql; - - //TODO: Either we should expect that the connection created by the factory is open of we should make it non-async - /// - /// Factory method for creating SQL Server connections. - /// - public delegate Task CreateSqlConnection(CancellationToken cancellationToken = default); - - /// - /// Implementation of the delayed message store based on the SQL Server. - /// - public class SqlServerDelayedMessageStore : IDelayedMessageStore - { - string schema; - string tableName; - CreateSqlConnection createSqlConnection; - - string insertCommand; - string removeCommand; - string bumpFailureCountCommand; - string nextCommand; - string fetchCommand; - - /// - /// Creates a new instance of the SQL Server delayed message store. - /// - /// Connection string to the SQL Server database. - /// (optional) schema to use. Defaults to dbo - /// (optional) name of the table where delayed messages are stored. Defaults to name of the endpoint with .Delayed suffix. - public SqlServerDelayedMessageStore(string connectionString, string schema = null, string tableName = null) - : this(token => Task.FromResult(new SqlConnection(connectionString)), schema, tableName) - { - } - - /// - /// Creates a new instance of the SQL Server delayed message store. - /// - /// Factory for database connections. - /// (optional) schema to use. Defaults to dbo - /// (optional) name of the table where delayed messages are stored. Defaults to name of the endpoint with .Delayed suffix. - public SqlServerDelayedMessageStore(CreateSqlConnection connectionFactory, string schema = null, string tableName = null) - { - createSqlConnection = connectionFactory; - this.tableName = tableName; - this.schema = schema ?? "dbo"; - } - - /// - public async Task Store(DelayedMessage timeout, CancellationToken cancellationToken = default) - { - using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - using (var cmd = new SqlCommand(insertCommand, cn)) - { - cmd.Parameters.AddWithValue("@id", timeout.MessageId); - cmd.Parameters.AddWithValue("@destination", timeout.Destination); - cmd.Parameters.AddWithValue("@time", timeout.Time); - cmd.Parameters.AddWithValue("@headers", timeout.Headers); - cmd.Parameters.AddWithValue("@state", timeout.Body); - await cn.OpenAsync(cancellationToken).ConfigureAwait(false); - _ = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); - } - } - - - /// - public async Task Remove(DelayedMessage timeout, CancellationToken cancellationToken = default) - { - using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - using (var cmd = new SqlCommand(removeCommand, cn)) - { - cmd.Parameters.AddWithValue("@id", timeout.MessageId); - await cn.OpenAsync(cancellationToken).ConfigureAwait(false); - var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); - return affected == 1; - } - } - - /// - public async Task IncrementFailureCount(DelayedMessage timeout, CancellationToken cancellationToken = default) - { - using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - using (var cmd = new SqlCommand(bumpFailureCountCommand, cn)) - { - cmd.Parameters.AddWithValue("@id", timeout.MessageId); - await cn.OpenAsync(cancellationToken).ConfigureAwait(false); - var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); - return affected == 1; - } - } - - /// - public async Task Initialize(string queueName, TransportTransactionMode transactionMode, CancellationToken cancellationToken = default) - { - tableName ??= $"{queueName}.timeouts"; - - var quotedFullName = $"{SqlNameHelper.Quote(schema)}.{SqlNameHelper.Quote(tableName)}"; - - var creator = new TimeoutTableCreator(createSqlConnection, quotedFullName); - await creator.CreateIfNecessary(cancellationToken).ConfigureAwait(false); - - insertCommand = string.Format(SqlConstants.SqlInsert, quotedFullName); - removeCommand = string.Format(SqlConstants.SqlDelete, quotedFullName); - bumpFailureCountCommand = string.Format(SqlConstants.SqlUpdate, quotedFullName); - nextCommand = string.Format(SqlConstants.SqlGetNext, quotedFullName); - fetchCommand = string.Format(SqlConstants.SqlFetch, quotedFullName); - } - - /// - public async Task Next(CancellationToken cancellationToken = default) - { - using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - using (var cmd = new SqlCommand(nextCommand, cn)) - { - await cn.OpenAsync(cancellationToken).ConfigureAwait(false); - var result = (DateTime?)await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); - return result.HasValue ? new DateTimeOffset(result.Value, TimeSpan.Zero) : null; - } - } - - /// - public async Task FetchNextDueTimeout(DateTimeOffset at, CancellationToken cancellationToken = default) - { - DelayedMessage result = null; - using (var cn = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - using (var cmd = new SqlCommand(fetchCommand, cn)) - { - cmd.Parameters.AddWithValue("@time", at.UtcDateTime); - - await cn.OpenAsync(cancellationToken).ConfigureAwait(false); - using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellationToken).ConfigureAwait(false)) - { - if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - result = new DelayedMessage - { - MessageId = (string)reader[0], - Destination = (string)reader[1], - Time = (DateTime)reader[2], - Headers = (byte[])reader[3], - Body = (byte[])reader[4], - NumberOfRetries = (int)reader[5] - }; - } - } - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs b/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs deleted file mode 100644 index 09a9acd4..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/DelayedDelivery/Sql/TimeoutTableCreator.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace NServiceBus.MessagingBridge.Msmq.DelayedDelivery.Sql -{ - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Data.SqlClient; - - class TimeoutTableCreator - { - public TimeoutTableCreator(CreateSqlConnection createSqlConnection, string tableName) - { - this.tableName = tableName; - this.createSqlConnection = createSqlConnection; - } - - public async Task CreateIfNecessary(CancellationToken cancellationToken = default) - { - var sql = string.Format(SqlConstants.SqlCreateTable, tableName); - using (var connection = await createSqlConnection(cancellationToken).ConfigureAwait(false)) - { - await connection.OpenAsync(cancellationToken).ConfigureAwait(false); - await Execute(sql, connection, cancellationToken).ConfigureAwait(false); - } - } - - static async Task Execute(string sql, SqlConnection connection, CancellationToken cancellationToken) - { - try - { - using (var transaction = connection.BeginTransaction()) - { - using (var command = new SqlCommand(sql, connection, transaction)) - { - await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); - } - transaction.Commit(); - } - } - catch (SqlException e) when (e.Number is 2714 or 1913) //Object already exists - { - //Table creation scripts are based on sys.objects metadata views. - //It looks that these views are not fully transactional and might - //not return information on already created table under heavy load. - //This in turn can result in executing table create or index create queries - //for objects that already exists. These queries will fail with - // 2714 (table) and 1913 (index) error codes. - } - } - - CreateSqlConnection createSqlConnection; - string tableName; - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs index 5daf2a56..c807bb5a 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqBridgeTransport.cs @@ -9,9 +9,7 @@ namespace NServiceBus using Features; using MSMQ.Messaging; using NServiceBus.MessagingBridge.Msmq; - using NServiceBus.MessagingBridge.Msmq.DelayedDelivery; using Routing; - using Support; using Transport; /// @@ -19,12 +17,10 @@ namespace NServiceBus /// public partial class MsmqBridgeTransport : TransportDefinition, IMessageDrivenSubscriptionTransport { - const string TimeoutsQueueQualifier = "timeouts"; - /// /// Creates a new instance of for configuration. /// - public MsmqBridgeTransport() : base(TransportTransactionMode.TransactionScope, true, false, true) + public MsmqBridgeTransport() : base(TransportTransactionMode.TransactionScope, false, false, true) { } @@ -40,50 +36,9 @@ public override async Task Initialize(HostSettings host var queuesToCreate = new HashSet(sendingAddresses); - var requiresDelayedDelivery = DelayedDelivery != null; - - string timeoutsErrorQueue = null; - MessagePump delayedDeliveryMessagePump = null; - - - if (requiresDelayedDelivery) - { - QueueAddress timeoutsQueue; - if (receivers.Length > 0) - { - var mainReceiver = receivers[0]; - timeoutsQueue = new QueueAddress(mainReceiver.ReceiveAddress.BaseAddress, qualifier: TimeoutsQueueQualifier); - timeoutsErrorQueue = mainReceiver.ErrorQueue; - } - else - { - if (hostSettings.CoreSettings != null) - { - if (!hostSettings.CoreSettings.TryGetExplicitlyConfiguredErrorQueueAddress(out var coreErrorQueue)) - { - throw new Exception("Delayed delivery requires an error queue to be specified using 'EndpointConfiguration.SendFailedMessagesTo()'"); - } - - timeoutsErrorQueue = coreErrorQueue; - timeoutsQueue = new QueueAddress(hostSettings.Name, qualifier: TimeoutsQueueQualifier); //Use name of the endpoint as the timeouts queue name. - } - else - { - throw new Exception("Timeouts are not supported for send-only configurations outside of an NServiceBus endpoint."); - } - } - - delayedDeliveryMessagePump = new MessagePump(mode => SelectReceiveStrategy(mode, TransactionScopeOptions.TransactionOptions), - MessageEnumeratorTimeout, TransportTransactionMode, false, hostSettings.CriticalErrorAction, - new ReceiveSettings("DelayedDelivery", timeoutsQueue, false, false, timeoutsErrorQueue)); - - queuesToCreate.Add(delayedDeliveryMessagePump.ReceiveAddress); - queuesToCreate.Add(timeoutsErrorQueue); - } - var messageReceivers = CreateReceivers(receivers, hostSettings.CriticalErrorAction, queuesToCreate); - var dispatcher = new MsmqMessageDispatcher(this, delayedDeliveryMessagePump?.ReceiveAddress, OnSendCallbackForTesting); + var dispatcher = new MsmqMessageDispatcher(this, OnSendCallbackForTesting); if (hostSettings.CoreSettings != null) { @@ -111,11 +66,6 @@ public override async Task Initialize(HostSettings host var installerUser = GetInstallationUserName(); var queueCreator = new MsmqQueueCreator(UseTransactionalQueues, installerUser); - if (requiresDelayedDelivery) - { - await DelayedDelivery.DelayedMessageStore.Initialize(hostSettings.Name, TransportTransactionMode, cancellationToken).ConfigureAwait(false); - } - queueCreator.CreateQueueIfNecessary(queuesToCreate); } @@ -124,28 +74,6 @@ public override async Task Initialize(HostSettings host QueuePermissions.CheckQueue(address); } - DelayedDeliveryPump delayedDeliveryPump = null; - if (requiresDelayedDelivery) - { - QueuePermissions.CheckQueue(delayedDeliveryMessagePump.ReceiveAddress); - QueuePermissions.CheckQueue(timeoutsErrorQueue); - - var staticFaultMetadata = new Dictionary - { - {Headers.ProcessingMachine, RuntimeEnvironment.MachineName}, - {Headers.ProcessingEndpoint, hostSettings.Name}, - {Headers.HostDisplayName, hostSettings.HostDisplayName} - }; - - var dueDelayedMessagePoller = new DueDelayedMessagePoller(dispatcher, DelayedDelivery.DelayedMessageStore, DelayedDelivery.NumberOfRetries, hostSettings.CriticalErrorAction, timeoutsErrorQueue, staticFaultMetadata, TransportTransactionMode, - DelayedDelivery.TimeToTriggerFetchCircuitBreaker, - DelayedDelivery.TimeToTriggerDispatchCircuitBreaker, - DelayedDelivery.MaximumRecoveryFailuresPerSecond, - delayedDeliveryMessagePump.ReceiveAddress); - - delayedDeliveryPump = new DelayedDeliveryPump(dispatcher, dueDelayedMessagePoller, DelayedDelivery.DelayedMessageStore, delayedDeliveryMessagePump, timeoutsErrorQueue, DelayedDelivery.NumberOfRetries, hostSettings.CriticalErrorAction, DelayedDelivery.TimeToTriggerStoreCircuitBreaker, staticFaultMetadata, TransportTransactionMode); - } - hostSettings.StartupDiagnostic.Add("NServiceBus.Transport.MSMQ", new { ExecuteInstaller = CreateQueues, @@ -155,11 +83,9 @@ public override async Task Initialize(HostSettings host UseJournalQueue, UseDeadLetterQueueForMessagesWithTimeToBeReceived, TimeToReachQueue = GetFormattedTimeToReachQueue(TimeToReachQueue), - TimeoutQueue = delayedDeliveryMessagePump?.ReceiveAddress, - TimeoutStorageType = DelayedDelivery?.DelayedMessageStore?.GetType()?.FullName, }); - var infrastructure = new MsmqTransportInfrastructure(messageReceivers, dispatcher, delayedDeliveryPump); + var infrastructure = new MsmqTransportInfrastructure(messageReceivers, dispatcher); await infrastructure.Start(cancellationToken).ConfigureAwait(false); return infrastructure; @@ -312,11 +238,6 @@ public override IReadOnlyCollection GetSupportedTransa /// public string CreateQueuesForUser { get; set; } - /// - /// Enable delayed delivery of messages. Required for delayed retries and Saga timeouts. - /// - public DelayedDeliverySettings DelayedDelivery { get; set; } - /// /// The callback that can be used to inject failures to the dispatcher for testing. /// diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs index b752e962..4d8549b2 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqConfigurationExtensions.cs @@ -173,16 +173,6 @@ public static TransportExtensions DisableNativeTimeToBeRece return transport; } - /// - /// Configures native delayed delivery. - /// - public static DelayedDeliverySettings NativeDelayedDelivery(this TransportExtensions config, IDelayedMessageStore delayedMessageStore) - { - Guard.AgainstNull(nameof(delayedMessageStore), delayedMessageStore); - config.Transport.DelayedDelivery = new DelayedDeliverySettings(delayedMessageStore); - return config.Transport.DelayedDelivery; - } - /// /// Ignore incoming Time-To-Be-Received (TTBR) headers. By default an expired TTBR header will result in the message to be discarded. /// diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs index e9dfc60a..b1075ba8 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqMessageDispatcher.cs @@ -1,13 +1,12 @@ namespace NServiceBus.MessagingBridge.Msmq { using System; - using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; - using MSMQ.Messaging; using System.Threading; using System.Threading.Tasks; using System.Transactions; + using MSMQ.Messaging; using Performance.TimeToBeReceived; using Transport; using Unicast.Queuing; @@ -15,13 +14,11 @@ namespace NServiceBus.MessagingBridge.Msmq class MsmqMessageDispatcher : IMessageDispatcher { readonly MsmqBridgeTransport transportSettings; - readonly string timeoutsQueue; readonly Action onSendCallback; - public MsmqMessageDispatcher(MsmqBridgeTransport transportSettings, string timeoutsQueue, Action onSendCallback = null) + public MsmqMessageDispatcher(MsmqBridgeTransport transportSettings, Action onSendCallback = null) { this.transportSettings = transportSettings; - this.timeoutsQueue = timeoutsQueue; this.onSendCallback = onSendCallback; } @@ -43,112 +40,9 @@ public Task Dispatch(TransportOperations outgoingMessages, TransportTransaction return Task.CompletedTask; } - public void DispatchDelayedMessage(string id, byte[] extension, ReadOnlyMemory body, string destination, TransportTransaction transportTransaction) - { - var headersAndProperties = MsmqUtilities.DeserializeMessageHeaders(extension); - var headers = new Dictionary(); - var properties = new DispatchProperties(); - - foreach (var kvp in headersAndProperties) - { - if (kvp.Key.StartsWith(MsmqUtilities.PropertyHeaderPrefix)) - { - properties[kvp.Key] = kvp.Value; - } - else - { - headers[kvp.Key] = kvp.Value; - } - } - - var request = new OutgoingMessage(id, headers, body); - - SendToDestination(transportTransaction, new UnicastTransportOperation(request, destination, properties)); - } - - public const string TimeoutDestination = "NServiceBus.Timeout.Destination"; - public const string TimeoutAt = "NServiceBus.Timeout.Expire"; - void ExecuteTransportOperation(TransportTransaction transaction, UnicastTransportOperation transportOperation) { - bool isDelayedMessage = transportOperation.Properties.DelayDeliveryWith != null || transportOperation.Properties.DoNotDeliverBefore != null; - - if (isDelayedMessage) - { - SendToDelayedDeliveryQueue(transaction, transportOperation); - } - else - { - SendToDestination(transaction, transportOperation); - } - } - - void SendToDelayedDeliveryQueue(TransportTransaction transaction, UnicastTransportOperation transportOperation) - { - onSendCallback?.Invoke(transaction, transportOperation); - var message = transportOperation.Message; - - transportOperation.Properties[TimeoutDestination] = transportOperation.Destination; - DateTimeOffset deliverAt; - - if (transportOperation.Properties.DelayDeliveryWith != null) - { - deliverAt = DateTimeOffset.UtcNow + transportOperation.Properties.DelayDeliveryWith.Delay; - } - else // transportOperation.Properties.DoNotDeliverBefore != null - { - deliverAt = transportOperation.Properties.DoNotDeliverBefore.At; - } - - transportOperation.Properties[TimeoutDestination] = transportOperation.Destination; - transportOperation.Properties[TimeoutAt] = DateTimeOffsetHelper.ToWireFormattedString(deliverAt); - - var destinationAddress = MsmqAddress.Parse(timeoutsQueue); - - foreach (var kvp in transportOperation.Properties) - { - //Use add to force exception if user adds a custom header that has the same name as the prefix + property name - transportOperation.Message.Headers.Add($"{MsmqUtilities.PropertyHeaderPrefix}{kvp.Key}", kvp.Value); - } - - try - { - using (var q = new MessageQueue(destinationAddress.FullPath, false, transportSettings.UseConnectionCache, QueueAccessMode.Send)) - { - using (var toSend = MsmqUtilities.Convert(message)) - { - toSend.UseDeadLetterQueue = true; //Always used DLQ for delayed messages - toSend.UseJournalQueue = transportSettings.UseJournalQueue; - - if (transportOperation.RequiredDispatchConsistency == DispatchConsistency.Isolated) - { - q.Send(toSend, string.Empty, GetIsolatedTransactionType()); - return; - } - - if (TryGetNativeTransaction(transaction, out var activeTransaction)) - { - q.Send(toSend, string.Empty, activeTransaction); - return; - } - - q.Send(toSend, string.Empty, GetTransactionTypeForSend()); - } - } - } - catch (MessageQueueException ex) - { - if (ex.MessageQueueErrorCode == MessageQueueErrorCode.QueueNotFound) - { - throw new QueueNotFoundException(timeoutsQueue, $"Failed to send the message to the local delayed delivery queue [{timeoutsQueue}]: queue does not exist.", ex); - } - - ThrowFailedToSendException(timeoutsQueue, ex); - } - catch (Exception ex) - { - ThrowFailedToSendException(timeoutsQueue, ex); - } + SendToDestination(transaction, transportOperation); } void SendToDestination(TransportTransaction transaction, UnicastTransportOperation transportOperation) diff --git a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs index 19c20cc1..e06e5495 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs +++ b/src/NServiceBus.MessagingBridge.Msmq/MsmqTransportInfrastructure.cs @@ -5,36 +5,20 @@ namespace NServiceBus.MessagingBridge.Msmq using System.Text; using System.Threading; using System.Threading.Tasks; - using DelayedDelivery; using Support; using Transport; class MsmqTransportInfrastructure : TransportInfrastructure { - readonly DelayedDeliveryPump delayedDeliveryPump; - - public MsmqTransportInfrastructure(IReadOnlyDictionary receivers, MsmqMessageDispatcher dispatcher, DelayedDeliveryPump delayedDeliveryPump) + public MsmqTransportInfrastructure(IReadOnlyDictionary receivers, MsmqMessageDispatcher dispatcher) { - this.delayedDeliveryPump = delayedDeliveryPump; Dispatcher = dispatcher; Receivers = receivers; } - public async Task Start(CancellationToken cancellationToken = default) - { - if (delayedDeliveryPump != null) - { - await delayedDeliveryPump.Start(cancellationToken).ConfigureAwait(false); - } - } + public Task Start(CancellationToken cancellationToken = default) => Task.CompletedTask; - public override async Task Shutdown(CancellationToken cancellationToken = default) - { - if (delayedDeliveryPump != null) - { - await delayedDeliveryPump.Stop(cancellationToken).ConfigureAwait(false); - } - } + public override Task Shutdown(CancellationToken cancellationToken = default) => Task.CompletedTask; public override string ToTransportAddress(QueueAddress address) => TranslateAddress(address); diff --git a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj index b8d82f04..6ba95daf 100644 --- a/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj +++ b/src/NServiceBus.MessagingBridge.Msmq/NServiceBus.MessagingBridge.Msmq.csproj @@ -5,7 +5,6 @@ - From ac0511f5eca4c1382c46a11ca3227571970aaabd Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Mon, 2 Oct 2023 14:24:59 -0400 Subject: [PATCH 18/18] Remove MSMQ subscription persistence --- .../ConfigureMsmqTransportTestExecution.cs | 2 +- .../Persistence/MsmqPersistence.cs | 20 -- .../IMsmqSubscriptionStorageQueue.cs | 11 - .../MsmqSubscriptionMessage.cs | 28 --- .../MsmqSubscriptionPersistence.cs | 64 ----- .../MsmqSubscriptionStorage.cs | 238 ------------------ ...scriptionStorageConfigurationExtensions.cs | 32 --- .../MsmqSubscriptionStorageQueue.cs | 68 ----- 8 files changed, 1 insertion(+), 462 deletions(-) delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs delete mode 100644 src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs diff --git a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs index 3b8e81e7..cd952caf 100644 --- a/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs +++ b/src/AcceptanceTests.Msmq/ConfigureMsmqTransportTestExecution.cs @@ -24,7 +24,7 @@ public Func ConfigureTransportForEndpoint(EndpointConfi { var transportDefinition = new TestableMsmqTransport(); var routingConfig = endpointConfiguration.UseTransport(transportDefinition); - endpointConfiguration.UsePersistence(); + endpointConfiguration.UsePersistence(); foreach (var publisher in publisherMetadata.Publishers) { diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs deleted file mode 100644 index 62275c2d..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/MsmqPersistence.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace NServiceBus -{ - using Features; - using Persistence; - using Persistence.Msmq; - - /// - /// Used to enable Msmq persistence. - /// - public class MsmqPersistence : PersistenceDefinition - { - internal MsmqPersistence() - { - Supports(s => - { - s.EnableFeatureByDefault(); - }); - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs deleted file mode 100644 index f3c92d61..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/IMsmqSubscriptionStorageQueue.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace NServiceBus.Persistence.Msmq -{ - using System.Collections.Generic; - - interface IMsmqSubscriptionStorageQueue - { - IEnumerable GetAllMessages(); - string Send(string body, string label); - void TryReceiveById(string messageId); - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs deleted file mode 100644 index 7e2d5098..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace NServiceBus.Persistence.Msmq -{ - using System; - using MSMQ.Messaging; - - class MsmqSubscriptionMessage - { - public MsmqSubscriptionMessage(Message m) - { - Body = m.Body; - Label = m.Label; - Id = m.Id; - ArrivedTime = m.ArrivedTime; - } - - public MsmqSubscriptionMessage() - { - } - - public DateTime ArrivedTime { get; set; } - - public object Body { get; set; } - - public string Label { get; set; } - - public string Id { get; set; } - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs deleted file mode 100644 index e5fad5e1..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionPersistence.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace NServiceBus.Persistence.Msmq -{ - using System; - using Features; - using Logging; - using Microsoft.Extensions.DependencyInjection; - using MSMQ.Messaging; - using NServiceBus.MessagingBridge.Msmq; - using Settings; - using Transport; - using Unicast.Subscriptions.MessageDrivenSubscriptions; - - class MsmqSubscriptionPersistence : Feature - { - protected override void Setup(FeatureConfigurationContext context) - { - var configuredQueueName = DetermineStorageQueueName(context.Settings); - - context.Settings.Get().BindSending(configuredQueueName); - - var transportSettings = context.Settings.Get() as MsmqBridgeTransport; - - var queue = new MsmqSubscriptionStorageQueue(MsmqAddress.Parse(configuredQueueName), transportSettings.UseTransactionalQueues); - var storage = new MsmqSubscriptionStorage(queue); - - context.Services.AddSingleton(storage); - } - - internal static string DetermineStorageQueueName(IReadOnlySettings settings) - { - var configuredQueueName = settings.GetConfiguredMsmqPersistenceSubscriptionQueue(); - - if (!string.IsNullOrEmpty(configuredQueueName)) - { - return configuredQueueName; - } - ThrowIfUsingTheOldDefaultSubscriptionsQueue(); - - var defaultQueueName = $"{settings.EndpointName()}.Subscriptions"; - Logger.Info($"The queue used to store subscriptions has not been configured, the default '{defaultQueueName}' will be used."); - return defaultQueueName; - } - - static void ThrowIfUsingTheOldDefaultSubscriptionsQueue() - { - if (DoesOldDefaultQueueExists()) - { - // The user has not configured the subscriptions queue to be "NServiceBus.Subscriptions" but there's a local queue. - // Indicates that the endpoint was using old default queue name. - throw new Exception( - "Detected the presence of an old default queue named `NServiceBus.Subscriptions`. Either migrate the subscriptions to the new default queue `[Your endpoint name].Subscriptions`, see our documentation for more details, or explicitly configure the subscriptions queue name to `NServiceBus.Subscriptions` if you want to use the existing queue."); - } - } - - static bool DoesOldDefaultQueueExists() - { - const string oldDefaultSubscriptionsQueue = "NServiceBus.Subscriptions"; - var path = MsmqAddress.Parse(oldDefaultSubscriptionsQueue).PathWithoutPrefix; - return MessageQueue.Exists(path); - } - - static ILog Logger = LogManager.GetLogger(typeof(MsmqSubscriptionPersistence)); - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs deleted file mode 100644 index 680cf38d..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorage.cs +++ /dev/null @@ -1,238 +0,0 @@ -namespace NServiceBus.Persistence.Msmq -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Extensibility; - using Logging; - using Unicast.Subscriptions.MessageDrivenSubscriptions; - using MessageType = Unicast.Subscriptions.MessageType; - - class MsmqSubscriptionStorage : ISubscriptionStorage, IDisposable - { - public MsmqSubscriptionStorage(IMsmqSubscriptionStorageQueue storageQueue) - { - this.storageQueue = storageQueue; - - // Required to be lazy loaded as the queue might not exist yet - lookup = new Lazy>>(CreateLookup); - } - - public void Dispose() - { - // Filled in by Janitor.fody - } - - Dictionary> CreateLookup() - { - var output = new Dictionary>(SubscriberComparer); - - var messages = storageQueue.GetAllMessages() - .OrderByDescending(m => m.ArrivedTime) - .ThenBy(x => x.Id) // ensure same order of messages with same timestamp across all endpoints - .ToArray(); - - foreach (var m in messages) - { - var messageTypeString = m.Body as string; - var messageType = new MessageType(messageTypeString); //this will parse both 2.6 and 3.0 type strings - var subscriber = Deserialize(m.Label); - - if (!output.TryGetValue(subscriber, out var endpointSubscriptions)) - { - output[subscriber] = endpointSubscriptions = new Dictionary(); - } - - if (endpointSubscriptions.ContainsKey(messageType)) - { - // this message is stale and can be removed - storageQueue.TryReceiveById(m.Id); - } - else - { - endpointSubscriptions[messageType] = m.Id; - } - } - - return output; - } - - public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) - { - var messagelist = messageTypes.ToArray(); - var result = new HashSet(); - - try - { - // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! - rwLock.EnterReadLock(); - - foreach (var subscribers in lookup.Value) - { - foreach (var messageType in messagelist) - { - if (subscribers.Value.TryGetValue(messageType, out _)) - { - result.Add(subscribers.Key); - } - } - } - } - finally - { - rwLock.ExitReadLock(); - } - - return Task.FromResult>(result); - } - - public Task Subscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - var body = $"{messageType.TypeName}, Version={messageType.Version}"; - var label = Serialize(subscriber); - var messageId = storageQueue.Send(body, label); - - AddToLookup(subscriber, messageType, messageId); - - log.DebugFormat($"Subscriber {subscriber.TransportAddress} added for message {messageType}."); - - return Task.CompletedTask; - } - - public Task Unsubscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - var messageId = RemoveFromLookup(subscriber, messageType); - - if (messageId != null) - { - storageQueue.TryReceiveById(messageId); - } - - log.Debug($"Subscriber {subscriber.TransportAddress} removed for message {messageType}."); - - return Task.CompletedTask; - } - - static string Serialize(Subscriber subscriber) - { - return $"{subscriber.TransportAddress}|{subscriber.Endpoint}"; - } - - static Subscriber Deserialize(string serializedForm) - { - var parts = serializedForm.Split(EntrySeparator, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length is 0 or > 2) - { - log.Error($"Invalid format of subscription entry: {serializedForm}."); - return null; - } - var endpointName = parts.Length > 1 - ? parts[1] - : null; - - return new Subscriber(parts[0], endpointName); - } - - void AddToLookup(Subscriber subscriber, MessageType typeName, string messageId) - { - try - { - // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! - rwLock.EnterWriteLock(); - - if (!lookup.Value.TryGetValue(subscriber, out var dictionary)) - { - dictionary = []; - } - else - { - // replace existing subscriber - lookup.Value.Remove(subscriber); - } - - dictionary[typeName] = messageId; - lookup.Value[subscriber] = dictionary; - } - finally - { - rwLock.ExitWriteLock(); - } - } - - string RemoveFromLookup(Subscriber subscriber, MessageType typeName) - { - try - { - // note: ReaderWriterLockSlim has a thread affinity and cannot be used with await! - rwLock.EnterWriteLock(); - - if (lookup.Value.TryGetValue(subscriber, out var subscriptions)) - { - if (subscriptions.TryGetValue(typeName, out var messageId)) - { - subscriptions.Remove(typeName); - if (subscriptions.Count == 0) - { - lookup.Value.Remove(subscriber); - } - - return messageId; - } - } - } - finally - { - rwLock.ExitWriteLock(); - } - - return null; - } - - Lazy>> lookup; - IMsmqSubscriptionStorageQueue storageQueue; - ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - - static ILog log = LogManager.GetLogger(typeof(ISubscriptionStorage)); - static TransportAddressEqualityComparer SubscriberComparer = new TransportAddressEqualityComparer(); - - static readonly char[] EntrySeparator = - { - '|' - }; - - sealed class TransportAddressEqualityComparer : IEqualityComparer - { - public bool Equals(Subscriber x, Subscriber y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - - if (x is null) - { - return false; - } - - if (y is null) - { - return false; - } - - if (x.GetType() != y.GetType()) - { - return false; - } - - return string.Equals(x.TransportAddress, y.TransportAddress, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(Subscriber obj) - { - return obj.TransportAddress != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(obj.TransportAddress) : 0; - } - } - } -} diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs deleted file mode 100644 index 5d7a51a5..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageConfigurationExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace NServiceBus -{ - using Configuration.AdvancedExtensibility; - using NServiceBus.MessagingBridge.Msmq; - using Settings; - - /// - /// Provides configuration extensions when using . - /// - public static class MsmqSubscriptionStorageConfigurationExtensions - { - /// - /// Configures the queue used to store subscriptions. - /// - /// The settings to extend. - /// The queue name. - public static void SubscriptionQueue(this PersistenceExtensions persistenceExtensions, string queue) - { - Guard.AgainstNull(nameof(persistenceExtensions), persistenceExtensions); - Guard.AgainstNull(nameof(queue), queue); - - persistenceExtensions.GetSettings().Set(MsmqPersistenceQueueConfigurationKey, queue); - } - - internal static string GetConfiguredMsmqPersistenceSubscriptionQueue(this IReadOnlySettings settings) - { - return settings.GetOrDefault(MsmqPersistenceQueueConfigurationKey); - } - - internal const string MsmqPersistenceQueueConfigurationKey = "MsmqSubscriptionPersistence.QueueName"; - } -} \ No newline at end of file diff --git a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs b/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs deleted file mode 100644 index a3dd3dfc..00000000 --- a/src/NServiceBus.MessagingBridge.Msmq/Persistence/SubscriptionStorage/MsmqSubscriptionStorageQueue.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace NServiceBus.Persistence.Msmq -{ - using System; - using System.Collections.Generic; - using System.Linq; - using MSMQ.Messaging; - using NServiceBus.MessagingBridge.Msmq; - - class MsmqSubscriptionStorageQueue : IMsmqSubscriptionStorageQueue - { - public MsmqSubscriptionStorageQueue(MsmqAddress queueAddress, bool useTransactionalQueue) - { - transactionTypeToUseForSend = useTransactionalQueue ? MessageQueueTransactionType.Single : MessageQueueTransactionType.None; - var messageReadPropertyFilter = new MessagePropertyFilter - { - Id = true, - Body = true, - Label = true, - ArrivedTime = true - }; - queue = new MessageQueue(queueAddress.FullPath) - { - Formatter = new XmlMessageFormatter(new[] - { - typeof(string) - }), - MessageReadPropertyFilter = messageReadPropertyFilter - }; - } - - public IEnumerable GetAllMessages() - { - return queue.GetAllMessages().Select(m => new MsmqSubscriptionMessage(m)); - } - - public string Send(string body, string label) - { - var toSend = new Message - { - Recoverable = true, - Formatter = queue.Formatter, - Body = body, - Label = label - }; - - queue.Send(toSend, transactionTypeToUseForSend); - - return toSend.Id; - } - - public void TryReceiveById(string messageId) - { - try - { - //Use of `None` here is intentional since ReceiveById works properly with this mode - //for both transactional and non-transactional queues - queue.ReceiveById(messageId, MessageQueueTransactionType.None); - } - catch (InvalidOperationException) - { - // thrown when message not found - } - } - - MessageQueueTransactionType transactionTypeToUseForSend; - MessageQueue queue; - } -} \ No newline at end of file