From 22026ba93be7d0adbd3173bd551d3f63da92a76b Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Feb 2026 12:50:55 +0100 Subject: [PATCH 01/25] Add IBM MQ transport support, including custom checks and tests * Introduced `ServiceControl.Transports.IBMMQ` with necessary configurations, transport customization, and manifest file. * Added `ServiceControl.Transports.IBMMQ.Tests` to verify IBM MQ transport functionality. * Updated `ServiceControl.slnx`, `nuget.config`, and `Directory.Packages.props` to include IBM MQ dependencies and projects. --- nuget.config | 4 + src/Directory.Packages.props | 1 + ...rviceControl.Transports.IBMMQ.Tests.csproj | 32 +++++ .../ServiceControlAuditEndpointTests.cs | 8 ++ .../ServiceControlMonitoringEndpointTests.cs | 8 ++ .../ServiceControlPrimaryEndpointTests.cs | 8 ++ .../TransportTestsConfiguration.cs | 31 +++++ .../DeadLetterQueueCheck.cs | 121 ++++++++++++++++++ .../IBMMQTransportCustomization.cs | 34 +++++ .../NoOpQueueLengthProvider.cs | 16 +++ .../ServiceControl.Transports.IBMMQ.csproj | 31 +++++ .../TestConnectionDetails.cs | 54 ++++++++ .../transport.manifest | 14 ++ src/ServiceControl.slnx | 3 + 14 files changed, 365 insertions(+) create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj create mode 100644 src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/transport.manifest diff --git a/nuget.config b/nuget.config index d72d1d3d28..b6348b5d91 100644 --- a/nuget.config +++ b/nuget.config @@ -1,10 +1,14 @@ + + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 52780bef2d..6038ea813d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,6 +52,7 @@ + diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj new file mode 100644 index 0000000000..b8c5306c4e --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControl.Transports.IBMMQ.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs new file mode 100644 index 0000000000..eea8a60cf9 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.Transport.Tests; + +using System; + +partial class ServiceControlAuditEndpointTests +{ + private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs new file mode 100644 index 0000000000..c4804c273e --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.Transport.Tests; + +using System; + +partial class ServiceControlMonitoringEndpointTests +{ + private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs new file mode 100644 index 0000000000..37e321292d --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs @@ -0,0 +1,8 @@ +namespace ServiceControl.Transport.Tests; + +using System; + +partial class ServiceControlPrimaryEndpointTests +{ + private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs new file mode 100644 index 0000000000..22d65e9135 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -0,0 +1,31 @@ +namespace ServiceControl.Transport.Tests +{ + using System; + using System.Threading.Tasks; + using Transports; + using Transports.IBMMQ; + + partial class TransportTestsConfiguration + { + public string ConnectionString { get; private set; } + + public ITransportCustomization TransportCustomization { get; private set; } + + public Task Configure() + { + TransportCustomization = new IBMMQTransportCustomization(); + ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); + + if (string.IsNullOrEmpty(ConnectionString)) + { + throw new Exception($"Environment variable {ConnectionStringKey} is required for IBM MQ transport tests to run"); + } + + return Task.CompletedTask; + } + + public Task Cleanup() => Task.CompletedTask; + + static string ConnectionStringKey = "ServiceControl_TransportTests_IBMMQ_ConnectionString"; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs b/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs new file mode 100644 index 0000000000..6d65a5664a --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs @@ -0,0 +1,121 @@ +// namespace ServiceControl.Transports.IBMMQ; +// +// using System; +// using System.Configuration; +// using System.Diagnostics; +// using System.Threading; +// using System.Threading.Tasks; +// using Microsoft.Extensions.Logging; +// using NServiceBus.CustomChecks; +// using Transports; +// +// public class DeadLetterQueueCheck : CustomCheck +// { +// public DeadLetterQueueCheck(TransportSettings settings, ILogger logger) : +// base("Dead Letter Queue", "Transport", TimeSpan.FromHours(1)) +// { +// runCheck = settings.RunCustomChecks; +// if (!runCheck) +// { +// return; +// } +// +// logger.LogDebug("MSMQ Dead Letter Queue custom check starting"); +// +// categoryName = Read("Msmq/PerformanceCounterCategoryName", "MSMQ Queue"); +// counterName = Read("Msmq/PerformanceCounterName", "Messages in Queue"); +// counterInstanceName = Read("Msmq/PerformanceCounterInstanceName", "Computer Queues"); +// +// try +// { +// dlqPerformanceCounter = new PerformanceCounter(categoryName, counterName, counterInstanceName, readOnly: true); +// } +// catch (InvalidOperationException ex) +// { +// logger.LogError(ex, CounterMightBeLocalized("CategoryName", "CounterName", "CounterInstanceName"), categoryName, counterName, counterInstanceName); +// } +// +// this.logger = logger; +// } +// +// public override Task PerformCheck(CancellationToken cancellationToken = default) +// { +// if (!runCheck) +// { +// return CheckResult.Pass; +// } +// +// logger.LogDebug("Checking Dead Letter Queue length"); +// float currentValue; +// try +// { +// if (dlqPerformanceCounter == null) +// { +// throw new InvalidOperationException("Unable to create performance counter instance."); +// } +// +// currentValue = dlqPerformanceCounter.NextValue(); +// } +// catch (InvalidOperationException ex) +// { +// logger.LogWarning(ex, CounterMightBeLocalized("CategoryName", "CounterName", "CounterInstanceName"), categoryName, counterName, counterInstanceName); +// return CheckResult.Failed(CounterMightBeLocalized(categoryName, counterName, counterInstanceName)); +// } +// +// if (currentValue <= 0) +// { +// logger.LogDebug("No messages in Dead Letter Queue"); +// return CheckResult.Pass; +// } +// +// logger.LogWarning("{DeadLetterMessageCount} messages in the Dead Letter Queue on {MachineName}. This could indicate a problem with ServiceControl's retries. Please submit a support ticket to Particular if you would like help from our engineers to ensure no message loss while resolving these dead letter messages", currentValue, Environment.MachineName); +// return CheckResult.Failed($"{currentValue} messages in the Dead Letter Queue on {Environment.MachineName}. This could indicate a problem with ServiceControl's retries. Please submit a support ticket to Particular if you would like help from our engineers to ensure no message loss while resolving these dead letter messages."); +// } +// +// static string CounterMightBeLocalized(string categoryName, string counterName, string counterInstanceName) +// { +// return +// $"Unable to read the Dead Letter Queue length. The performance counter with category '{categoryName}' and name '{counterName}' and instance name '{counterInstanceName}' is not available. " +// + "It is possible that the counter category, name and instance name have been localized into different languages. " +// + @"Consider overriding the counter category, name and instance name in the application configuration file by adding: +// +// +// +// +// +// "; +// } +// +// // from ConfigFileSettingsReader since we cannot reference ServiceControl +// static string Read(string name, string defaultValue = default) +// { +// return Read("ServiceControl", name, defaultValue); +// } +// +// static string Read(string root, string name, string defaultValue = default) +// { +// return TryRead(root, name, out var value) ? value : defaultValue; +// } +// +// static bool TryRead(string root, string name, out string value) +// { +// var fullKey = $"{root}/{name}"; +// +// if (ConfigurationManager.AppSettings[fullKey] != null) +// { +// value = ConfigurationManager.AppSettings[fullKey]; +// return true; +// } +// +// value = default; +// return false; +// } +// +// PerformanceCounter dlqPerformanceCounter; +// string categoryName; +// string counterName; +// string counterInstanceName; +// bool runCheck; +// +// readonly ILogger logger; +// } diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs new file mode 100644 index 0000000000..5b20dc9159 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -0,0 +1,34 @@ +namespace ServiceControl.Transports.IBMMQ; + +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus; +using NServiceBus.Transport.IbmMq; + +public class IBMMQTransportCustomization : TransportCustomization +{ + protected override void CustomizeTransportForPrimaryEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + transportDefinition.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; + + protected override void CustomizeTransportForAuditEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + transportDefinition.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; + + protected override void CustomizeTransportForMonitoringEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + transportDefinition.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; + + protected override void AddTransportForMonitoringCore(IServiceCollection services, TransportSettings transportSettings) + { + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); + } + + protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) + { + var transport = new IbmMqTransport(TestConnectionDetails.Apply); + transport.TransportTransactionMode = transport.GetSupportedTransactionModes().Contains(preferredTransactionMode) + ? preferredTransactionMode + : TransportTransactionMode.ReceiveOnly; + + return transport; + } +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs b/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs new file mode 100644 index 0000000000..0b3f000a09 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs @@ -0,0 +1,16 @@ +namespace ServiceControl.Transports.IBMMQ; + +using System.Threading; +using System.Threading.Tasks; + +class NoOpQueueLengthProvider : IProvideQueueLength +{ + public void TrackEndpointInputQueue(EndpointToQueueMapping queueToTrack) + { + //This is a no op for MSMQ since the endpoints report their queue length to SC using custom messages + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj new file mode 100644 index 0000000000..d81ba19d66 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + true + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs new file mode 100644 index 0000000000..8247776c9e --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs @@ -0,0 +1,54 @@ +#nullable enable +using System; +using System.Collections.Specialized; +using System.Web; +using NServiceBus.Transport.IbmMq; + +/// +/// Copied directly from: +/// +/// https://github.com/ParticularLabs/NServiceBus.IBMMQ/blob/main/src/Testing/TestConnectionDetails.cs +/// +/// +static class TestConnectionDetails +{ + // mq://admin:passw0rd@localhost:1414/QM1?appname=&sslkeyrepo=&cipherspec=&sslpeername=&topicprefix=DEV&channel=DEV.ADMIN.SVRCONN + static readonly Uri ConnectionUri = new(Environment.GetEnvironmentVariable("IBMMQ_CONNECTIONSTRING") ?? "mq://admin:passw0rd@localhost:1414"); + static readonly NameValueCollection Query = HttpUtility.ParseQueryString(ConnectionUri.Query); + + public static string Host => ConnectionUri.Host; + public static int Port => ConnectionUri.Port > 0 ? ConnectionUri.Port : 1414; + public static string User => Uri.UnescapeDataString(ConnectionUri.UserInfo.Split(':')[0]); + public static string Password => Uri.UnescapeDataString(ConnectionUri.UserInfo.Split(':')[1]); + public static string QueueManagerName => ConnectionUri.AbsolutePath.Trim('/') is { Length: > 0 } path ? Uri.UnescapeDataString(path) : "QM1"; + public static string Channel => Query["channel"] ?? "DEV.ADMIN.SVRCONN"; + public static string TopicPrefix => Query["topicprefix"] ?? "DEV"; + + + public static void Apply(IbmMqTransportOptions options) + { + options.Host = Host; + options.Port = Port; + options.User = User; + options.Password = Password; + options.QueueManagerName = QueueManagerName; + options.Channel = Channel; + + if (Query["appname"] is { } appName) + { + options.ApplicationName = appName; + } + if (Query["sslkeyrepo"] is { } sslKeyRepo) + { + options.SslKeyRepository = sslKeyRepo; + } + if (Query["cipherspec"] is { } cipherSpec) + { + options.CipherSpec = cipherSpec; + } + if (Query["sslpeername"] is { } sslPeerName) + { + options.SslPeerName = sslPeerName; + } + } +} diff --git a/src/ServiceControl.Transports.IBMMQ/transport.manifest b/src/ServiceControl.Transports.IBMMQ/transport.manifest new file mode 100644 index 0000000000..0cb1b0f720 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/transport.manifest @@ -0,0 +1,14 @@ +{ + "Definitions": [ + { + "Name": "IBMMQ", + "DisplayName": "IBM MQ", + "AssemblyName": "ServiceControl.Transports.IBMMQ", + "TypeName": "ServiceControl.Transports.IBMMQ.IBMMQTransportCustomization, ServiceControl.Transports.IBMMQ", + "SampleConnectionString": "mq://admin:passw0rd@localhost:1414/QM1?topicprefix=DEV&channel=DEV.ADMIN.SVRCONN&appname={APPNAME}&sslkeyrepo={SSLKEYREPO}&cipherspec={CIPHERSPEC}&sslpeername={SSLPEERNAME}", + "AvailableInSCMU": true, + "Aliases": [ + ] + } + ] +} \ No newline at end of file diff --git a/src/ServiceControl.slnx b/src/ServiceControl.slnx index 2ff353b79a..fc2f5dd384 100644 --- a/src/ServiceControl.slnx +++ b/src/ServiceControl.slnx @@ -13,6 +13,7 @@ + @@ -78,6 +79,7 @@ + @@ -89,6 +91,7 @@ + From c5da4ff08b16a4a076c90183d1d00459b32f0fbf Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Feb 2026 13:43:41 +0100 Subject: [PATCH 02/25] Update IBM MQ transport tests to set static transport concurrency values --- .../ServiceControlAuditEndpointTests.cs | 2 +- .../ServiceControlMonitoringEndpointTests.cs | 2 +- .../ServiceControlPrimaryEndpointTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs index eea8a60cf9..f70a724658 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlAuditEndpointTests.cs @@ -4,5 +4,5 @@ namespace ServiceControl.Transport.Tests; partial class ServiceControlAuditEndpointTests { - private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); + private static partial int GetTransportDefaultConcurrency() => 32; } \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs index c4804c273e..0a62a8e7d4 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlMonitoringEndpointTests.cs @@ -4,5 +4,5 @@ namespace ServiceControl.Transport.Tests; partial class ServiceControlMonitoringEndpointTests { - private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); + private static partial int GetTransportDefaultConcurrency() => 32; } \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs index 37e321292d..56579e59cd 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/ServiceControlPrimaryEndpointTests.cs @@ -4,5 +4,5 @@ namespace ServiceControl.Transport.Tests; partial class ServiceControlPrimaryEndpointTests { - private static partial int GetTransportDefaultConcurrency() => Math.Max(8, Environment.ProcessorCount); + private static partial int GetTransportDefaultConcurrency() => 10; } \ No newline at end of file From bf07d1af1ad5e49289883ffa72842aed7ca94254 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Feb 2026 13:48:00 +0100 Subject: [PATCH 03/25] Improve IBM MQ transport customization to support sanitized resource names in tests --- .../TransportTestsConfiguration.cs | 23 +++++++++++++++++-- .../IBMMQTransportCustomization.cs | 8 ++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs index 22d65e9135..63dac04060 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -1,4 +1,10 @@ -namespace ServiceControl.Transport.Tests +using System; +using NServiceBus; +using NServiceBus.Transport.IbmMq; +using ServiceControl.Transports; +using ServiceControl.Transports.IBMMQ; + +namespace ServiceControl.Transport.Tests { using System; using System.Threading.Tasks; @@ -13,7 +19,7 @@ partial class TransportTestsConfiguration public Task Configure() { - TransportCustomization = new IBMMQTransportCustomization(); + TransportCustomization = new TestIBMMQTransportCustomization(); ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); if (string.IsNullOrEmpty(ConnectionString)) @@ -28,4 +34,17 @@ public Task Configure() static string ConnectionStringKey = "ServiceControl_TransportTests_IBMMQ_ConnectionString"; } +} + + +sealed class TestIBMMQTransportCustomization : IBMMQTransportCustomization +{ + protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) + { + transportSettings.Set>(o => o.ResourceNameSanitizer = name => name + .Replace("ServiceControlMonitoring", "SCM") // Mitigate max queue name length + .Replace("-", ".") // dash is an illegal char + ); + return base.CreateTransport(transportSettings, preferredTransactionMode); + } } \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs index 5b20dc9159..086d139665 100644 --- a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -1,5 +1,6 @@ namespace ServiceControl.Transports.IBMMQ; +using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; using NServiceBus; @@ -24,7 +25,12 @@ protected override void AddTransportForMonitoringCore(IServiceCollection service protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - var transport = new IbmMqTransport(TestConnectionDetails.Apply); + var overrides = transportSettings.Get>(); + var transport = new IbmMqTransport(o => + { + overrides(o); + TestConnectionDetails.Apply(o); + }); transport.TransportTransactionMode = transport.GetSupportedTransactionModes().Contains(preferredTransactionMode) ? preferredTransactionMode : TransportTransactionMode.ReceiveOnly; From 49f1af7627ce71084e924a30b505195560eab797 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Feb 2026 13:48:59 +0000 Subject: [PATCH 04/25] =?UTF-8?q?=E2=9C=A8=20Implement=20IBM=20MQ=20queue?= =?UTF-8?q?=20length=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace NoOpQueueLengthProvider with a real implementation that connects to the queue manager and queries CurrentDepth via MQOO_INQUIRE. Connection properties are parsed from the MQ URI connection string, matching the same format used by the transport manifest sample. --- .../IBMMQTransportCustomization.cs | 2 +- .../NoOpQueueLengthProvider.cs | 16 -- .../QueueLengthProvider.cs | 159 ++++++++++++++++++ 3 files changed, 160 insertions(+), 17 deletions(-) delete mode 100644 src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs create mode 100644 src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs index 086d139665..b59a150a10 100644 --- a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -19,7 +19,7 @@ protected override void CustomizeTransportForMonitoringEndpoint(EndpointConfigur protected override void AddTransportForMonitoringCore(IServiceCollection services, TransportSettings transportSettings) { - services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(provider => provider.GetRequiredService()); } diff --git a/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs b/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs deleted file mode 100644 index 0b3f000a09..0000000000 --- a/src/ServiceControl.Transports.IBMMQ/NoOpQueueLengthProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ServiceControl.Transports.IBMMQ; - -using System.Threading; -using System.Threading.Tasks; - -class NoOpQueueLengthProvider : IProvideQueueLength -{ - public void TrackEndpointInputQueue(EndpointToQueueMapping queueToTrack) - { - //This is a no op for MSMQ since the endpoints report their queue length to SC using custom messages - } - - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs new file mode 100644 index 0000000000..9d957d77f2 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs @@ -0,0 +1,159 @@ +namespace ServiceControl.Transports.IBMMQ; + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using IBM.WMQ; +using Microsoft.Extensions.Logging; + +class QueueLengthProvider : AbstractQueueLengthProvider +{ + public QueueLengthProvider(TransportSettings settings, Action store, ILogger logger) + : base(settings, store) + { + this.logger = logger; + + var connectionUri = new Uri(ConnectionString); + var query = HttpUtility.ParseQueryString(connectionUri.Query); + + queueManagerName = connectionUri.AbsolutePath.Trim('/') is { Length: > 0 } path + ? Uri.UnescapeDataString(path) + : "QM1"; + + connectionProperties = new Hashtable + { + [MQC.TRANSPORT_PROPERTY] = MQC.TRANSPORT_MQSERIES_MANAGED, + [MQC.HOST_NAME_PROPERTY] = connectionUri.Host, + [MQC.PORT_PROPERTY] = connectionUri.Port > 0 ? connectionUri.Port : 1414, + [MQC.CHANNEL_PROPERTY] = query["channel"] ?? "DEV.ADMIN.SVRCONN" + }; + + var userInfo = connectionUri.UserInfo; + if (!string.IsNullOrEmpty(userInfo)) + { + var parts = userInfo.Split(':'); + var user = Uri.UnescapeDataString(parts[0]); + + if (!string.IsNullOrWhiteSpace(user)) + { + connectionProperties[MQC.USE_MQCSP_AUTHENTICATION_PROPERTY] = true; + connectionProperties[MQC.USER_ID_PROPERTY] = user; + } + + if (parts.Length > 1) + { + var password = Uri.UnescapeDataString(parts[1]); + if (!string.IsNullOrWhiteSpace(password)) + { + connectionProperties[MQC.PASSWORD_PROPERTY] = password; + } + } + } + + if (query["sslkeyrepo"] is { } sslKeyRepo) + { + connectionProperties[MQC.SSL_CERT_STORE_PROPERTY] = sslKeyRepo; + } + + if (query["cipherspec"] is { } cipherSpec) + { + connectionProperties[MQC.SSL_CIPHER_SPEC_PROPERTY] = cipherSpec; + } + + if (query["sslpeername"] is { } sslPeerName) + { + connectionProperties[MQC.SSL_PEER_NAME_PROPERTY] = sslPeerName; + } + } + + public override void TrackEndpointInputQueue(EndpointToQueueMapping queueToTrack) => + endpointQueues.AddOrUpdate(queueToTrack.EndpointName, _ => queueToTrack.InputQueue, (_, currentValue) => + { + if (currentValue != queueToTrack.InputQueue) + { + sizes.TryRemove(currentValue, out var _); + } + + return queueToTrack.InputQueue; + }); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + FetchQueueLengths(); + + UpdateQueueLengthStore(); + + await Task.Delay(QueryDelayInterval, stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + // no-op + } + catch (Exception e) + { + logger.LogError(e, "Queue length query loop failure"); + } + } + } + + void UpdateQueueLengthStore() + { + var nowTicks = DateTime.UtcNow.Ticks; + + foreach (var endpointQueuePair in endpointQueues) + { + if (sizes.TryGetValue(endpointQueuePair.Value, out var size)) + { + Store( + [ + new QueueLengthEntry + { + DateTicks = nowTicks, + Value = size + } + ], + new EndpointToQueueMapping(endpointQueuePair.Key, endpointQueuePair.Value)); + } + } + } + + void FetchQueueLengths() + { + if (endpointQueues.IsEmpty) + { + return; + } + + using var queueManager = new MQQueueManager(queueManagerName, connectionProperties); + + foreach (var endpointQueuePair in endpointQueues) + { + var queueName = endpointQueuePair.Value; + try + { + using var queue = queueManager.AccessQueue(queueName, MQC.MQOO_INQUIRE | MQC.MQOO_FAIL_IF_QUIESCING); + sizes[queueName] = queue.CurrentDepth; + } + catch (Exception e) + { + logger.LogWarning(e, "Error querying queue length for {QueueName}", queueName); + } + } + } + + static readonly TimeSpan QueryDelayInterval = TimeSpan.FromMilliseconds(200); + + readonly ConcurrentDictionary endpointQueues = new(); + readonly ConcurrentDictionary sizes = new(); + + readonly string queueManagerName; + readonly Hashtable connectionProperties; + readonly ILogger logger; +} From b807a1a905b8f803385693786dd3d083c5fdf03e Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Tue, 24 Feb 2026 12:59:56 +0100 Subject: [PATCH 05/25] Make queue length test work for IBMMQ transport --- .../TransportTestsConfiguration.cs | 58 ++++++++++--------- .../TransportTestFixture.cs | 9 ++- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs index 63dac04060..7ff5be9a29 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -1,49 +1,51 @@ -using System; +namespace ServiceControl.Transport.Tests; + +using System; +using System.Threading.Tasks; +using Transports; +using Transports.IBMMQ; using NServiceBus; using NServiceBus.Transport.IbmMq; -using ServiceControl.Transports; -using ServiceControl.Transports.IBMMQ; +using NUnit.Framework; -namespace ServiceControl.Transport.Tests +[SetUpFixture] +public class BootstrapFixture { - using System; - using System.Threading.Tasks; - using Transports; - using Transports.IBMMQ; - - partial class TransportTestsConfiguration - { - public string ConnectionString { get; private set; } + [OneTimeSetUp] + public void RunBeforeAnyTests() => TransportTestFixture.QueueNameSeparator = '.'; +} - public ITransportCustomization TransportCustomization { get; private set; } +class TransportTestsConfiguration +{ + public string ConnectionString { get; private set; } - public Task Configure() - { - TransportCustomization = new TestIBMMQTransportCustomization(); - ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); + public ITransportCustomization TransportCustomization { get; private set; } - if (string.IsNullOrEmpty(ConnectionString)) - { - throw new Exception($"Environment variable {ConnectionStringKey} is required for IBM MQ transport tests to run"); - } + public Task Configure() + { + TransportCustomization = new TestIBMMQTransportCustomization(); + ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); - return Task.CompletedTask; + if (string.IsNullOrEmpty(ConnectionString)) + { + throw new Exception($"Environment variable {ConnectionStringKey} is required for IBM MQ transport tests to run"); } - public Task Cleanup() => Task.CompletedTask; - - static string ConnectionStringKey = "ServiceControl_TransportTests_IBMMQ_ConnectionString"; + return Task.CompletedTask; } -} + public Task Cleanup() => Task.CompletedTask; + + static string ConnectionStringKey = "ServiceControl_TransportTests_IBMMQ_ConnectionString"; +} sealed class TestIBMMQTransportCustomization : IBMMQTransportCustomization { protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { transportSettings.Set>(o => o.ResourceNameSanitizer = name => name - .Replace("ServiceControlMonitoring", "SCM") // Mitigate max queue name length - .Replace("-", ".") // dash is an illegal char + .Replace("ServiceControlMonitoring", "SCM") // Mitigate max queue name length + .Replace("-", ".") // dash is an illegal char ); return base.CreateTransport(transportSettings, preferredTransactionMode); } diff --git a/src/ServiceControl.Transports.Tests/TransportTestFixture.cs b/src/ServiceControl.Transports.Tests/TransportTestFixture.cs index de88f52cac..7c36817a98 100644 --- a/src/ServiceControl.Transports.Tests/TransportTestFixture.cs +++ b/src/ServiceControl.Transports.Tests/TransportTestFixture.cs @@ -21,6 +21,11 @@ abstract class TransportTestFixture { + /// + /// Not all transports support - as a separator, as this is not a source package this can be overridden. + /// + public static char QueueNameSeparator = '-'; + [SetUp] public virtual async Task Setup() { @@ -30,7 +35,7 @@ public virtual async Task Setup() configuration = new TransportTestsConfiguration(); testCancellationTokenSource = Debugger.IsAttached ? new CancellationTokenSource() : new CancellationTokenSource(TestTimeout); registrations = []; - queueSuffix = $"-{Path.GetRandomFileName().Replace(".", string.Empty)}"; + queueSuffix = $"{QueueNameSeparator}{Path.GetRandomFileName().Replace(".", string.Empty)}"; await configuration.Configure(); @@ -70,7 +75,7 @@ public virtual async Task Cleanup() protected IMessageDispatcher Dispatcher => dispatcherTransportInfrastructure.Dispatcher; - protected string GetTestQueueName(string name) => $"{name}-{queueSuffix}"; + protected string GetTestQueueName(string name) => $"{name}{queueSuffix}"; protected TaskCompletionSource CreateTaskCompletionSource() { From 47990917d108e9d228ef7cee4b29e2e7c0a47203 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 09:33:55 +0100 Subject: [PATCH 06/25] =?UTF-8?q?=F0=9F=93=A6=20Update=20NServiceBus.Trans?= =?UTF-8?q?port.IbmMq=20to=201.0.0-alpha.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve breaking API renames: namespace IbmMq → IBMMQ, types IbmMqTransport → IBMMQTransport, IbmMqTransportOptions → IBMMQTransportOptions. --- src/Directory.Packages.props | 2 +- .../TransportTestsConfiguration.cs | 6 +++--- .../IBMMQTransportCustomization.cs | 16 ++++++++-------- .../TestConnectionDetails.cs | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6038ea813d..79de89f7b6 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs index 7ff5be9a29..2e293f7aa3 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -5,7 +5,7 @@ using Transports; using Transports.IBMMQ; using NServiceBus; -using NServiceBus.Transport.IbmMq; +using NServiceBus.Transport.IBMMQ; using NUnit.Framework; [SetUpFixture] @@ -41,9 +41,9 @@ public Task Configure() sealed class TestIBMMQTransportCustomization : IBMMQTransportCustomization { - protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) + protected override IBMMQTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - transportSettings.Set>(o => o.ResourceNameSanitizer = name => name + transportSettings.Set>(o => o.ResourceNameSanitizer = name => name .Replace("ServiceControlMonitoring", "SCM") // Mitigate max queue name length .Replace("-", ".") // dash is an illegal char ); diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs index b59a150a10..2596b3ffd6 100644 --- a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -4,17 +4,17 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; using NServiceBus; -using NServiceBus.Transport.IbmMq; +using NServiceBus.Transport.IBMMQ; -public class IBMMQTransportCustomization : TransportCustomization +public class IBMMQTransportCustomization : TransportCustomization { - protected override void CustomizeTransportForPrimaryEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + protected override void CustomizeTransportForPrimaryEndpoint(EndpointConfiguration endpointConfiguration, IBMMQTransport transportDefinition, TransportSettings transportSettings) => transportDefinition.TransportTransactionMode = TransportTransactionMode.SendsAtomicWithReceive; - protected override void CustomizeTransportForAuditEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + protected override void CustomizeTransportForAuditEndpoint(EndpointConfiguration endpointConfiguration, IBMMQTransport transportDefinition, TransportSettings transportSettings) => transportDefinition.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; - protected override void CustomizeTransportForMonitoringEndpoint(EndpointConfiguration endpointConfiguration, IbmMqTransport transportDefinition, TransportSettings transportSettings) => + protected override void CustomizeTransportForMonitoringEndpoint(EndpointConfiguration endpointConfiguration, IBMMQTransport transportDefinition, TransportSettings transportSettings) => transportDefinition.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; protected override void AddTransportForMonitoringCore(IServiceCollection services, TransportSettings transportSettings) @@ -23,10 +23,10 @@ protected override void AddTransportForMonitoringCore(IServiceCollection service services.AddHostedService(provider => provider.GetRequiredService()); } - protected override IbmMqTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) + protected override IBMMQTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - var overrides = transportSettings.Get>(); - var transport = new IbmMqTransport(o => + var overrides = transportSettings.Get>(); + var transport = new IBMMQTransport(o => { overrides(o); TestConnectionDetails.Apply(o); diff --git a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs index 8247776c9e..1602cfd01f 100644 --- a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs +++ b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Specialized; using System.Web; -using NServiceBus.Transport.IbmMq; +using NServiceBus.Transport.IBMMQ; /// /// Copied directly from: @@ -25,7 +25,7 @@ static class TestConnectionDetails public static string TopicPrefix => Query["topicprefix"] ?? "DEV"; - public static void Apply(IbmMqTransportOptions options) + public static void Apply(IBMMQTransportOptions options) { options.Host = Host; options.Port = Port; From 76f25d8fcaa81600a674970244f676ab1a0fdb86 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 09:34:58 +0100 Subject: [PATCH 07/25] =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20Update=20NuGet=20pac?= =?UTF-8?q?kage=20ID=20casing=20to=20NServiceBus.Transport.IBMMQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Directory.Packages.props | 2 +- .../ServiceControl.Transports.IBMMQ.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 79de89f7b6..f396518336 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + diff --git a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj index d81ba19d66..93bb097b76 100644 --- a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj +++ b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj @@ -12,7 +12,7 @@ - + From 8b79112d247c5f8df752bd569f4234f307470dcb Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 09:37:53 +0100 Subject: [PATCH 08/25] Updated nuget.config, contained local folder used during dev testing --- nuget.config | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nuget.config b/nuget.config index b6348b5d91..d72d1d3d28 100644 --- a/nuget.config +++ b/nuget.config @@ -1,14 +1,10 @@ - - - - From 8d9cfd96430eab6c594aa8691803fa2de4d1acd8 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 10:18:15 +0100 Subject: [PATCH 09/25] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Add=20IBMMQ=20test?= =?UTF-8?q?=20category=20to=20CI=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IBMMQ to test matrix (Linux only) - Add IncludeInIBMMQTestsAttribute and TestsFilter for test filtering - Pass connection string via env block on run-tests step - Unify TestConnectionDetails to read ServiceControl_TransportTests_IBMMQ_ConnectionString --- .github/workflows/ci.yml | 5 ++++- src/ServiceControl.Transports.IBMMQ.Tests/TestsFilter.cs | 1 + .../TransportTestsConfiguration.cs | 3 ++- .../TestConnectionDetails.cs | 7 ++++++- src/TestHelper/IncludeInIBMMQTestsAttribute.cs | 4 ++++ 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/TestsFilter.cs create mode 100644 src/TestHelper/IncludeInIBMMQTestsAttribute.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89d17f3c27..326071fe45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest] - test-category: [ Default, SqlServer, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL ] + test-category: [ Default, SqlServer, AzureServiceBus, RabbitMQ, AzureStorageQueues, MSMQ, SQS, PrimaryRavenAcceptance, PrimaryRavenPersistence, PostgreSQL, IBMMQ ] include: - os: windows-latest os-name: Windows @@ -27,6 +27,8 @@ jobs: exclude: - os: ubuntu-latest test-category: MSMQ + - os: windows-latest + test-category: IBMMQ fail-fast: false steps: - name: Check for secrets @@ -117,6 +119,7 @@ jobs: uses: Particular/run-tests-action@v1.7.0 env: ServiceControl_TESTS_FILTER: ${{ matrix.test-category }} + ServiceControl_TransportTests_IBMMQ_ConnectionString: ${{ secrets.SERVICECONTROL_TRANSPORTTESTS_IBMMQ_CONNECTIONSTRING }} PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TestsFilter.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TestsFilter.cs new file mode 100644 index 0000000000..76ebf15442 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TestsFilter.cs @@ -0,0 +1 @@ +[assembly: IncludeInIBMMQTests()] \ No newline at end of file diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs index 2e293f7aa3..6d52918c91 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -24,7 +24,8 @@ class TransportTestsConfiguration public Task Configure() { TransportCustomization = new TestIBMMQTransportCustomization(); - ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey); + ConnectionString = Environment.GetEnvironmentVariable(ConnectionStringKey) + ?? Environment.GetEnvironmentVariable(ConnectionStringKey.ToUpperInvariant()); // Env keys are case sensitive, POSIX is all uppercase if (string.IsNullOrEmpty(ConnectionString)) { diff --git a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs index 1602cfd01f..573ac0e28d 100644 --- a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs +++ b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs @@ -13,7 +13,12 @@ static class TestConnectionDetails { // mq://admin:passw0rd@localhost:1414/QM1?appname=&sslkeyrepo=&cipherspec=&sslpeername=&topicprefix=DEV&channel=DEV.ADMIN.SVRCONN - static readonly Uri ConnectionUri = new(Environment.GetEnvironmentVariable("IBMMQ_CONNECTIONSTRING") ?? "mq://admin:passw0rd@localhost:1414"); + static readonly Uri ConnectionUri = new( + Environment.GetEnvironmentVariable("ServiceControl_TransportTests_IBMMQ_ConnectionString") + ?? Environment.GetEnvironmentVariable("SERVICECONTROL_TRANSPORTTESTS_IBMMQ_CONNECTIONSTRING") + ?? "mq://admin:passw0rd@localhost:1414" + ); + static readonly NameValueCollection Query = HttpUtility.ParseQueryString(ConnectionUri.Query); public static string Host => ConnectionUri.Host; diff --git a/src/TestHelper/IncludeInIBMMQTestsAttribute.cs b/src/TestHelper/IncludeInIBMMQTestsAttribute.cs new file mode 100644 index 0000000000..2bb18aeb93 --- /dev/null +++ b/src/TestHelper/IncludeInIBMMQTestsAttribute.cs @@ -0,0 +1,4 @@ +public class IncludeInIBMMQTestsAttribute : IncludeInTestsAttribute +{ + protected override string Filter => "IBMMQ"; +} From 7b687180b5482a149f5858a3fc944a5aee8246bc Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 10:38:49 +0100 Subject: [PATCH 10/25] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Use=20IBM=20MQ=20con?= =?UTF-8?q?tainer=20with=20health=20check=20instead=20of=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 326071fe45..3b60d29e15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -105,6 +105,18 @@ jobs: connection-string-name: ServiceControl_TransportTests_ASQ_ConnectionString azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} tag: ServiceControl + - name: Setup IBM MQ + if: matrix.test-category == 'IBMMQ' + run: | + docker run --name ibmmq -d -p 1414:1414 -p 9443:9443 ` + --health-cmd "dspmq" --health-interval 10s --health-timeout 5s --health-retries 10 --health-start-period 30s ` + -e LICENSE=accept -e MQ_QMGR_NAME=QM1 ` + icr.io/ibm-messaging/mq:latest + # Wait for container health check to pass + while ((docker inspect --format '{{.State.Health.Status}}' ibmmq) -ne 'healthy') { + Start-Sleep -Seconds 2 + } + echo "ServiceControl_TransportTests_IBMMQ_ConnectionString=mq://admin:passw0rd@localhost:1414/QM1?channel=DEV.ADMIN.SVRCONN&topicprefix=DEV" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append - name: Setup SQS environment variables if: matrix.test-category == 'SQS' run: | @@ -119,7 +131,6 @@ jobs: uses: Particular/run-tests-action@v1.7.0 env: ServiceControl_TESTS_FILTER: ${{ matrix.test-category }} - ServiceControl_TransportTests_IBMMQ_ConnectionString: ${{ secrets.SERVICECONTROL_TRANSPORTTESTS_IBMMQ_CONNECTIONSTRING }} PARTICULARSOFTWARE_LICENSE: ${{ secrets.LICENSETEXT }} AZURE_ACI_CREDENTIALS: ${{ secrets.AZURE_ACI_CREDENTIALS }} From bb3d94b685ebe84dbe17ba0eb5e15f6e39e416b5 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 10:51:52 +0100 Subject: [PATCH 11/25] =?UTF-8?q?=F0=9F=90=9B=20Set=20MQ=5FADMIN=5FPASSWOR?= =?UTF-8?q?D=20for=20IBM=20MQ=20CI=20container?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b60d29e15..748bedf831 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,7 +110,7 @@ jobs: run: | docker run --name ibmmq -d -p 1414:1414 -p 9443:9443 ` --health-cmd "dspmq" --health-interval 10s --health-timeout 5s --health-retries 10 --health-start-period 30s ` - -e LICENSE=accept -e MQ_QMGR_NAME=QM1 ` + -e LICENSE=accept -e MQ_QMGR_NAME=QM1 -e MQ_ADMIN_PASSWORD=passw0rd ` icr.io/ibm-messaging/mq:latest # Wait for container health check to pass while ((docker inspect --format '{{.State.Health.Status}}' ibmmq) -ne 'healthy') { From c74b291aab0e0faa2bebd98195dade1e7731b666 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 11:11:45 +0100 Subject: [PATCH 12/25] =?UTF-8?q?=F0=9F=90=9B=20Fix=20IBMMQ=20transport=20?= =?UTF-8?q?deploy=20folder=20and=20update=20approval=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix csproj artifact destination from Transports\MSMQ to Transports\IBMMQ - Remove Windows-only condition (was copy-pasted from MSMQ) - Add IBMMQ to TransportNames and packaging approval files --- .../ServiceControl.Transports.IBMMQ.csproj | 4 ++-- ...n_manifest_files_exist_in_specified_assembly.approved.txt | 1 + .../ApprovalFiles/APIApprovals.TransportNames.approved.txt | 5 +++++ .../DeploymentPackageTests.cs | 3 ++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj index 93bb097b76..1483e059b1 100644 --- a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj +++ b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj @@ -24,8 +24,8 @@ - - + + diff --git a/src/ServiceControl.Transports.Tests/ApprovalFiles/TransportManifestLibraryTests.All_types_defined_in_manifest_files_exist_in_specified_assembly.approved.txt b/src/ServiceControl.Transports.Tests/ApprovalFiles/TransportManifestLibraryTests.All_types_defined_in_manifest_files_exist_in_specified_assembly.approved.txt index 6d201c38cc..397809505d 100644 --- a/src/ServiceControl.Transports.Tests/ApprovalFiles/TransportManifestLibraryTests.All_types_defined_in_manifest_files_exist_in_specified_assembly.approved.txt +++ b/src/ServiceControl.Transports.Tests/ApprovalFiles/TransportManifestLibraryTests.All_types_defined_in_manifest_files_exist_in_specified_assembly.approved.txt @@ -1,6 +1,7 @@ [ "AmazonSQS", "AzureStorageQueue", + "IBMMQ", "LearningTransport", "LearningTransport", "MSMQ", diff --git a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/APIApprovals.TransportNames.approved.txt b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/APIApprovals.TransportNames.approved.txt index bb2e392a4e..58499293d7 100644 --- a/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/APIApprovals.TransportNames.approved.txt +++ b/src/ServiceControlInstaller.Engine.UnitTests/ApprovalFiles/APIApprovals.TransportNames.approved.txt @@ -35,6 +35,11 @@ "ServiceControl.Transports.AzureStorageQueues.ServiceControlAzureStorageQueueTransport, ServiceControl.Transports.AzureStorageQueues" ] }, + { + "Name": "IBMMQ", + "DisplayName": "IBM MQ", + "Aliases": [] + }, { "Name": "LearningTransport", "DisplayName": "Learning Transport (Non-Production)", diff --git a/src/ServiceControlInstaller.Packaging.UnitTests/DeploymentPackageTests.cs b/src/ServiceControlInstaller.Packaging.UnitTests/DeploymentPackageTests.cs index 2a9390c7a3..b4fb53fd8e 100644 --- a/src/ServiceControlInstaller.Packaging.UnitTests/DeploymentPackageTests.cs +++ b/src/ServiceControlInstaller.Packaging.UnitTests/DeploymentPackageTests.cs @@ -88,7 +88,8 @@ public void Should_package_all_transports() "MSMQ", "AmazonSQS", "LearningTransport", - "PostgreSql"}; + "PostgreSql", + "IBMMQ"}; var bundledTransports = deploymentPackage.DeploymentUnits .Where(u => u.Category == "Transports") From fdf1922d597f7cc62b8760b131063fe4c0b00d93 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 11:29:13 +0100 Subject: [PATCH 13/25] =?UTF-8?q?=F0=9F=90=9B=20Add=20IBMMQ=20to=20Develop?= =?UTF-8?q?mentTransportLocations=20for=20manifest=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ServiceControl.Transports/DevelopmentTransportLocations.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ServiceControl.Transports/DevelopmentTransportLocations.cs b/src/ServiceControl.Transports/DevelopmentTransportLocations.cs index b2b81e72b6..495b660609 100644 --- a/src/ServiceControl.Transports/DevelopmentTransportLocations.cs +++ b/src/ServiceControl.Transports/DevelopmentTransportLocations.cs @@ -25,6 +25,7 @@ static DevelopmentTransportLocations() ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.SqlServer")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.SQS")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.PostgreSql")); + ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.IBMMQ")); } } From 9f5ab4d358ad7ae4842763c6862892ef768ededa Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 9 Mar 2026 11:34:44 +0100 Subject: [PATCH 14/25] Sort transport manifest folder names --- src/ServiceControl.Transports/DevelopmentTransportLocations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceControl.Transports/DevelopmentTransportLocations.cs b/src/ServiceControl.Transports/DevelopmentTransportLocations.cs index 495b660609..95e7e8c021 100644 --- a/src/ServiceControl.Transports/DevelopmentTransportLocations.cs +++ b/src/ServiceControl.Transports/DevelopmentTransportLocations.cs @@ -19,13 +19,13 @@ static DevelopmentTransportLocations() { ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.ASBS")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.ASQ")); + ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.IBMMQ")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.Learning")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.Msmq", "net10.0-windows")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.RabbitMQ")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.SqlServer")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.SQS")); ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.PostgreSql")); - ManifestFiles.Add(BuildManifestPath(srcFolder, "ServiceControl.Transports.IBMMQ")); } } From 3f56faba104e8d9b62b4c0b3d2188cd77fa76daa Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 11 Mar 2026 09:21:38 +0100 Subject: [PATCH 15/25] Use TryGet instead of Get... could be that no override is registered --- .../IBMMQTransportCustomization.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs index 2596b3ffd6..a818034d14 100644 --- a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -25,10 +25,13 @@ protected override void AddTransportForMonitoringCore(IServiceCollection service protected override IBMMQTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - var overrides = transportSettings.Get>(); var transport = new IBMMQTransport(o => { - overrides(o); + if (transportSettings.TryGet>(out var overrides)) + { + overrides(o); + } + TestConnectionDetails.Apply(o); }); transport.TransportTransactionMode = transport.GetSupportedTransactionModes().Contains(preferredTransactionMode) From fc369e54326a66e3cf48db43c2d9d3d57cf6c491 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 11 Mar 2026 09:30:34 +0100 Subject: [PATCH 16/25] =?UTF-8?q?=E2=9C=A8=20Reuse=20MQQueueManager=20conn?= =?UTF-8?q?ection=20in=20QueueLengthProvider=20with=20broker=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueueLengthProvider.cs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs index 9d957d77f2..353e832ba5 100644 --- a/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs @@ -1,3 +1,4 @@ +#nullable enable namespace ServiceControl.Transports.IBMMQ; using System; @@ -99,8 +100,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) catch (Exception e) { logger.LogError(e, "Queue length query loop failure"); + CloseConnection(); + await Task.Delay(ReconnectDelayInterval, stoppingToken).ConfigureAwait(false); } } + + CloseConnection(); } void UpdateQueueLengthStore() @@ -131,16 +136,22 @@ void FetchQueueLengths() return; } - using var queueManager = new MQQueueManager(queueManagerName, connectionProperties); + var manager = EnsureConnected(); foreach (var endpointQueuePair in endpointQueues) { var queueName = endpointQueuePair.Value; try { - using var queue = queueManager.AccessQueue(queueName, MQC.MQOO_INQUIRE | MQC.MQOO_FAIL_IF_QUIESCING); + using var queue = manager.AccessQueue(queueName, MQC.MQOO_INQUIRE | MQC.MQOO_FAIL_IF_QUIESCING); sizes[queueName] = queue.CurrentDepth; } + catch (MQException e) when (IsConnectionError(e)) + { + logger.LogWarning(e, "Lost connection to queue manager while querying {QueueName}", queueName); + CloseConnection(); + throw; + } catch (Exception e) { logger.LogWarning(e, "Error querying queue length for {QueueName}", queueName); @@ -148,7 +159,50 @@ void FetchQueueLengths() } } + MQQueueManager EnsureConnected() + { + if (queueManager is not null) + { + return queueManager; + } + + queueManager = new MQQueueManager(queueManagerName, connectionProperties); + logger.LogInformation("Connected to queue manager '{QueueManagerName}'", queueManagerName); + return queueManager; + } + + void CloseConnection() + { + if (queueManager is null) + { + return; + } + + try + { + queueManager.Disconnect(); + } + catch (Exception e) + { + logger.LogDebug(e, "Error disconnecting from queue manager"); + } + + queueManager = null; + } + + static bool IsConnectionError(MQException e) => e.ReasonCode + is MQC.MQRC_CONNECTION_BROKEN + or MQC.MQRC_CONNECTION_ERROR + or MQC.MQRC_CONNECTION_STOPPED + or MQC.MQRC_CONNECTION_QUIESCING + or MQC.MQRC_CONNECTION_NOT_AVAILABLE + or MQC.MQRC_Q_MGR_NOT_AVAILABLE + or MQC.MQRC_Q_MGR_NOT_ACTIVE; + static readonly TimeSpan QueryDelayInterval = TimeSpan.FromMilliseconds(200); + static readonly TimeSpan ReconnectDelayInterval = TimeSpan.FromSeconds(10); + + MQQueueManager? queueManager; readonly ConcurrentDictionary endpointQueues = new(); readonly ConcurrentDictionary sizes = new(); From cba1bb9f253c5fdc7fa6e52cd143bb958fd82630 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 11 Mar 2026 09:33:17 +0100 Subject: [PATCH 17/25] =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20Remove=20duplicate?= =?UTF-8?q?=20CopyToOutputDirectory=20for=20transport.manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ServiceControl.Transports.IBMMQ.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj index 1483e059b1..94ceac0087 100644 --- a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj +++ b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj @@ -19,9 +19,6 @@ - - PreserveNewest - From 4e1bc003eec6c91a22c4c923fe76190d92969a1b Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 11 Mar 2026 10:12:19 +0100 Subject: [PATCH 18/25] =?UTF-8?q?=E2=9C=A8=20Add=20IBM=20MQ=20Dead=20Lette?= =?UTF-8?q?r=20Queue=20custom=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract shared connection URI parsing into ConnectionProperties helper and implement DLQ depth check using MQQueueManager.DeadLetterQueueName. Also remove unused MSMQ-era package references. --- .../ConnectionProperties.cs | 66 +++++++ .../DeadLetterQueueCheck.cs | 187 +++++++----------- .../QueueLengthProvider.cs | 54 +---- .../ServiceControl.Transports.IBMMQ.csproj | 2 - 4 files changed, 133 insertions(+), 176 deletions(-) create mode 100644 src/ServiceControl.Transports.IBMMQ/ConnectionProperties.cs diff --git a/src/ServiceControl.Transports.IBMMQ/ConnectionProperties.cs b/src/ServiceControl.Transports.IBMMQ/ConnectionProperties.cs new file mode 100644 index 0000000000..4c3606b5e6 --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/ConnectionProperties.cs @@ -0,0 +1,66 @@ +namespace ServiceControl.Transports.IBMMQ; + +using System; +using System.Collections; +using System.Web; +using IBM.WMQ; + +static class ConnectionProperties +{ + public static (string queueManagerName, Hashtable properties) Parse(string connectionString) + { + var connectionUri = new Uri(connectionString); + var query = HttpUtility.ParseQueryString(connectionUri.Query); + + var queueManagerName = connectionUri.AbsolutePath.Trim('/') is { Length: > 0 } path + ? Uri.UnescapeDataString(path) + : "QM1"; + + var properties = new Hashtable + { + [MQC.TRANSPORT_PROPERTY] = MQC.TRANSPORT_MQSERIES_MANAGED, + [MQC.HOST_NAME_PROPERTY] = connectionUri.Host, + [MQC.PORT_PROPERTY] = connectionUri.Port > 0 ? connectionUri.Port : 1414, + [MQC.CHANNEL_PROPERTY] = query["channel"] ?? "DEV.ADMIN.SVRCONN" + }; + + var userInfo = connectionUri.UserInfo; + if (!string.IsNullOrEmpty(userInfo)) + { + var parts = userInfo.Split(':'); + var user = Uri.UnescapeDataString(parts[0]); + + if (!string.IsNullOrWhiteSpace(user)) + { + properties[MQC.USE_MQCSP_AUTHENTICATION_PROPERTY] = true; + properties[MQC.USER_ID_PROPERTY] = user; + } + + if (parts.Length > 1) + { + var password = Uri.UnescapeDataString(parts[1]); + if (!string.IsNullOrWhiteSpace(password)) + { + properties[MQC.PASSWORD_PROPERTY] = password; + } + } + } + + if (query["sslkeyrepo"] is { } sslKeyRepo) + { + properties[MQC.SSL_CERT_STORE_PROPERTY] = sslKeyRepo; + } + + if (query["cipherspec"] is { } cipherSpec) + { + properties[MQC.SSL_CIPHER_SPEC_PROPERTY] = cipherSpec; + } + + if (query["sslpeername"] is { } sslPeerName) + { + properties[MQC.SSL_PEER_NAME_PROPERTY] = sslPeerName; + } + + return (queueManagerName, properties); + } +} diff --git a/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs b/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs index 6d65a5664a..5bc5041dd9 100644 --- a/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs +++ b/src/ServiceControl.Transports.IBMMQ/DeadLetterQueueCheck.cs @@ -1,121 +1,66 @@ -// namespace ServiceControl.Transports.IBMMQ; -// -// using System; -// using System.Configuration; -// using System.Diagnostics; -// using System.Threading; -// using System.Threading.Tasks; -// using Microsoft.Extensions.Logging; -// using NServiceBus.CustomChecks; -// using Transports; -// -// public class DeadLetterQueueCheck : CustomCheck -// { -// public DeadLetterQueueCheck(TransportSettings settings, ILogger logger) : -// base("Dead Letter Queue", "Transport", TimeSpan.FromHours(1)) -// { -// runCheck = settings.RunCustomChecks; -// if (!runCheck) -// { -// return; -// } -// -// logger.LogDebug("MSMQ Dead Letter Queue custom check starting"); -// -// categoryName = Read("Msmq/PerformanceCounterCategoryName", "MSMQ Queue"); -// counterName = Read("Msmq/PerformanceCounterName", "Messages in Queue"); -// counterInstanceName = Read("Msmq/PerformanceCounterInstanceName", "Computer Queues"); -// -// try -// { -// dlqPerformanceCounter = new PerformanceCounter(categoryName, counterName, counterInstanceName, readOnly: true); -// } -// catch (InvalidOperationException ex) -// { -// logger.LogError(ex, CounterMightBeLocalized("CategoryName", "CounterName", "CounterInstanceName"), categoryName, counterName, counterInstanceName); -// } -// -// this.logger = logger; -// } -// -// public override Task PerformCheck(CancellationToken cancellationToken = default) -// { -// if (!runCheck) -// { -// return CheckResult.Pass; -// } -// -// logger.LogDebug("Checking Dead Letter Queue length"); -// float currentValue; -// try -// { -// if (dlqPerformanceCounter == null) -// { -// throw new InvalidOperationException("Unable to create performance counter instance."); -// } -// -// currentValue = dlqPerformanceCounter.NextValue(); -// } -// catch (InvalidOperationException ex) -// { -// logger.LogWarning(ex, CounterMightBeLocalized("CategoryName", "CounterName", "CounterInstanceName"), categoryName, counterName, counterInstanceName); -// return CheckResult.Failed(CounterMightBeLocalized(categoryName, counterName, counterInstanceName)); -// } -// -// if (currentValue <= 0) -// { -// logger.LogDebug("No messages in Dead Letter Queue"); -// return CheckResult.Pass; -// } -// -// logger.LogWarning("{DeadLetterMessageCount} messages in the Dead Letter Queue on {MachineName}. This could indicate a problem with ServiceControl's retries. Please submit a support ticket to Particular if you would like help from our engineers to ensure no message loss while resolving these dead letter messages", currentValue, Environment.MachineName); -// return CheckResult.Failed($"{currentValue} messages in the Dead Letter Queue on {Environment.MachineName}. This could indicate a problem with ServiceControl's retries. Please submit a support ticket to Particular if you would like help from our engineers to ensure no message loss while resolving these dead letter messages."); -// } -// -// static string CounterMightBeLocalized(string categoryName, string counterName, string counterInstanceName) -// { -// return -// $"Unable to read the Dead Letter Queue length. The performance counter with category '{categoryName}' and name '{counterName}' and instance name '{counterInstanceName}' is not available. " -// + "It is possible that the counter category, name and instance name have been localized into different languages. " -// + @"Consider overriding the counter category, name and instance name in the application configuration file by adding: -// -// -// -// -// -// "; -// } -// -// // from ConfigFileSettingsReader since we cannot reference ServiceControl -// static string Read(string name, string defaultValue = default) -// { -// return Read("ServiceControl", name, defaultValue); -// } -// -// static string Read(string root, string name, string defaultValue = default) -// { -// return TryRead(root, name, out var value) ? value : defaultValue; -// } -// -// static bool TryRead(string root, string name, out string value) -// { -// var fullKey = $"{root}/{name}"; -// -// if (ConfigurationManager.AppSettings[fullKey] != null) -// { -// value = ConfigurationManager.AppSettings[fullKey]; -// return true; -// } -// -// value = default; -// return false; -// } -// -// PerformanceCounter dlqPerformanceCounter; -// string categoryName; -// string counterName; -// string counterInstanceName; -// bool runCheck; -// -// readonly ILogger logger; -// } +namespace ServiceControl.Transports.IBMMQ; + +using System; +using System.Threading; +using System.Threading.Tasks; +using IBM.WMQ; +using Microsoft.Extensions.Logging; +using NServiceBus.CustomChecks; +using ServiceControl.Infrastructure; + +public class DeadLetterQueueCheck : CustomCheck +{ + public DeadLetterQueueCheck(TransportSettings settings) : base(id: "Dead Letter Queue", category: "Transport", repeatAfter: TimeSpan.FromHours(1)) + { + Logger.LogDebug("IBM MQ Dead Letter Queue custom check starting"); + + (queueManagerName, connectionProperties) = ConnectionProperties.Parse(settings.ConnectionString); + runCheck = settings.RunCustomChecks; + } + + public override Task PerformCheck(CancellationToken cancellationToken = default) + { + if (!runCheck) + { + return Task.FromResult(CheckResult.Pass); + } + + Logger.LogDebug("Checking Dead Letter Queue length"); + + try + { + using var queueManager = new MQQueueManager(queueManagerName, connectionProperties); + + var dlqName = queueManager.DeadLetterQueueName?.Trim(); + if (string.IsNullOrEmpty(dlqName)) + { + return Task.FromResult(CheckResult.Pass); + } + + using var dlq = queueManager.AccessQueue(dlqName, MQC.MQOO_INQUIRE | MQC.MQOO_FAIL_IF_QUIESCING); + var depth = dlq.CurrentDepth; + + if (depth > 0) + { + var message = $"{depth} messages in the Dead Letter Queue '{dlqName}' on queue manager '{queueManagerName}'. This could indicate a problem with ServiceControl's retries. Please submit a support ticket to Particular if you would like help from our engineers to ensure no message loss while resolving these dead letter messages."; + Logger.LogWarning("{DeadLetterMessageCount} messages in the Dead Letter Queue '{DeadLetterQueueName}' on queue manager '{QueueManagerName}'", depth, dlqName, queueManagerName); + return Task.FromResult(CheckResult.Failed(message)); + } + + Logger.LogDebug("No messages in Dead Letter Queue"); + return Task.FromResult(CheckResult.Pass); + } + catch (MQException e) + { + var message = $"Unable to check Dead Letter Queue on queue manager '{queueManagerName}'. Reason: {e.Message} (RC={e.ReasonCode})"; + Logger.LogWarning(e, "Unable to check Dead Letter Queue on queue manager '{QueueManagerName}'", queueManagerName); + return Task.FromResult(CheckResult.Failed(message)); + } + } + + readonly string queueManagerName; + readonly System.Collections.Hashtable connectionProperties; + readonly bool runCheck; + + static readonly ILogger Logger = LoggerUtil.CreateStaticLogger(typeof(DeadLetterQueueCheck)); +} diff --git a/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs index 353e832ba5..7dcf53bcc2 100644 --- a/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs +++ b/src/ServiceControl.Transports.IBMMQ/QueueLengthProvider.cs @@ -6,7 +6,6 @@ namespace ServiceControl.Transports.IBMMQ; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using System.Web; using IBM.WMQ; using Microsoft.Extensions.Logging; @@ -16,58 +15,7 @@ public QueueLengthProvider(TransportSettings settings, Action 0 } path - ? Uri.UnescapeDataString(path) - : "QM1"; - - connectionProperties = new Hashtable - { - [MQC.TRANSPORT_PROPERTY] = MQC.TRANSPORT_MQSERIES_MANAGED, - [MQC.HOST_NAME_PROPERTY] = connectionUri.Host, - [MQC.PORT_PROPERTY] = connectionUri.Port > 0 ? connectionUri.Port : 1414, - [MQC.CHANNEL_PROPERTY] = query["channel"] ?? "DEV.ADMIN.SVRCONN" - }; - - var userInfo = connectionUri.UserInfo; - if (!string.IsNullOrEmpty(userInfo)) - { - var parts = userInfo.Split(':'); - var user = Uri.UnescapeDataString(parts[0]); - - if (!string.IsNullOrWhiteSpace(user)) - { - connectionProperties[MQC.USE_MQCSP_AUTHENTICATION_PROPERTY] = true; - connectionProperties[MQC.USER_ID_PROPERTY] = user; - } - - if (parts.Length > 1) - { - var password = Uri.UnescapeDataString(parts[1]); - if (!string.IsNullOrWhiteSpace(password)) - { - connectionProperties[MQC.PASSWORD_PROPERTY] = password; - } - } - } - - if (query["sslkeyrepo"] is { } sslKeyRepo) - { - connectionProperties[MQC.SSL_CERT_STORE_PROPERTY] = sslKeyRepo; - } - - if (query["cipherspec"] is { } cipherSpec) - { - connectionProperties[MQC.SSL_CIPHER_SPEC_PROPERTY] = cipherSpec; - } - - if (query["sslpeername"] is { } sslPeerName) - { - connectionProperties[MQC.SSL_PEER_NAME_PROPERTY] = sslPeerName; - } + (queueManagerName, connectionProperties) = ConnectionProperties.Parse(ConnectionString); } public override void TrackEndpointInputQueue(EndpointToQueueMapping queueToTrack) => diff --git a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj index 94ceac0087..2b2d9a9296 100644 --- a/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj +++ b/src/ServiceControl.Transports.IBMMQ/ServiceControl.Transports.IBMMQ.csproj @@ -13,8 +13,6 @@ - - From 0a96ba2367a6520c3ee895b7801afb164df2dcb7 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Wed, 11 Mar 2026 11:07:45 +0100 Subject: [PATCH 19/25] =?UTF-8?q?=E2=9C=A8=20Add=20tests=20for=20IBM=20MQ?= =?UTF-8?q?=20DeadLetterQueueCheck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DeadLetterQueueCheckTests.cs | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/ServiceControl.Transports.IBMMQ.Tests/DeadLetterQueueCheckTests.cs diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/DeadLetterQueueCheckTests.cs b/src/ServiceControl.Transports.IBMMQ.Tests/DeadLetterQueueCheckTests.cs new file mode 100644 index 0000000000..b05f681e9c --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ.Tests/DeadLetterQueueCheckTests.cs @@ -0,0 +1,148 @@ +namespace ServiceControl.Transport.Tests; + +using System; +using System.Collections; +using System.Threading.Tasks; +using System.Web; +using IBM.WMQ; +using NUnit.Framework; +using Transports; +using Transports.IBMMQ; + +[TestFixture] +class DeadLetterQueueCheckTests +{ + [Test] + public async Task Should_pass_when_custom_checks_disabled() + { + var settings = new TransportSettings + { + ConnectionString = ConnectionString, + RunCustomChecks = false + }; + + var check = new DeadLetterQueueCheck(settings); + var result = await check.PerformCheck().ConfigureAwait(false); + + Assert.That(result.HasFailed, Is.False); + } + + [Test] + public async Task Should_pass_when_dlq_is_empty() + { + DrainDeadLetterQueue(); + + var settings = new TransportSettings + { + ConnectionString = ConnectionString, + RunCustomChecks = true + }; + + var check = new DeadLetterQueueCheck(settings); + var result = await check.PerformCheck().ConfigureAwait(false); + + Assert.That(result.HasFailed, Is.False); + } + + [Test] + public async Task Should_fail_when_dlq_has_messages() + { + DrainDeadLetterQueue(); + PutMessageOnDeadLetterQueue(); + + try + { + var settings = new TransportSettings + { + ConnectionString = ConnectionString, + RunCustomChecks = true + }; + + var check = new DeadLetterQueueCheck(settings); + var result = await check.PerformCheck().ConfigureAwait(false); + + Assert.That(result.HasFailed, Is.True); + Assert.That(result.FailureReason, Does.Contain("messages in the Dead Letter Queue")); + } + finally + { + DrainDeadLetterQueue(); + } + } + + [Test] + public async Task Should_fail_when_connection_is_invalid() + { + var settings = new TransportSettings + { + ConnectionString = "mq://admin:passw0rd@localhost:19999/BOGUS", + RunCustomChecks = true + }; + + var check = new DeadLetterQueueCheck(settings); + var result = await check.PerformCheck().ConfigureAwait(false); + + Assert.That(result.HasFailed, Is.True); + Assert.That(result.FailureReason, Does.Contain("Unable to check Dead Letter Queue")); + Assert.That(result.FailureReason, Does.Contain("RC=")); + } + + static void PutMessageOnDeadLetterQueue() + { + var (qmName, props) = ParseConnectionString(); + using var qm = new MQQueueManager(qmName, props); + var dlqName = qm.DeadLetterQueueName.Trim(); + using var dlq = qm.AccessQueue(dlqName, MQC.MQOO_OUTPUT); + var msg = new MQMessage(); + msg.WriteString("DLQ test message"); + dlq.Put(msg); + } + + static void DrainDeadLetterQueue() + { + var (qmName, props) = ParseConnectionString(); + using var qm = new MQQueueManager(qmName, props); + var dlqName = qm.DeadLetterQueueName.Trim(); + using var dlq = qm.AccessQueue(dlqName, MQC.MQOO_INPUT_SHARED | MQC.MQOO_FAIL_IF_QUIESCING); + var gmo = new MQGetMessageOptions { WaitInterval = 0, Options = MQC.MQGMO_NO_WAIT }; + while (true) + { + try + { + dlq.Get(new MQMessage(), gmo); + } + catch (MQException e) when (e.ReasonCode == MQC.MQRC_NO_MSG_AVAILABLE) + { + break; + } + } + } + + static (string queueManagerName, Hashtable properties) ParseConnectionString() + { + var uri = new Uri(ConnectionString); + var query = HttpUtility.ParseQueryString(uri.Query); + + var qmName = uri.AbsolutePath.Trim('/') is { Length: > 0 } path + ? Uri.UnescapeDataString(path) + : "QM1"; + + var props = new Hashtable + { + [MQC.TRANSPORT_PROPERTY] = MQC.TRANSPORT_MQSERIES_MANAGED, + [MQC.HOST_NAME_PROPERTY] = uri.Host, + [MQC.PORT_PROPERTY] = uri.Port > 0 ? uri.Port : 1414, + [MQC.CHANNEL_PROPERTY] = query["channel"] ?? "DEV.ADMIN.SVRCONN", + [MQC.USE_MQCSP_AUTHENTICATION_PROPERTY] = true, + [MQC.USER_ID_PROPERTY] = Uri.UnescapeDataString(uri.UserInfo.Split(':')[0]), + [MQC.PASSWORD_PROPERTY] = Uri.UnescapeDataString(uri.UserInfo.Split(':')[1]) + }; + + return (qmName, props); + } + + static readonly string ConnectionString = + Environment.GetEnvironmentVariable("ServiceControl_TransportTests_IBMMQ_ConnectionString") + ?? Environment.GetEnvironmentVariable("SERVICECONTROL_TRANSPORTTESTS_IBMMQ_CONNECTIONSTRING") + ?? "mq://admin:passw0rd@localhost:1414"; +} From 842f5de1c9411a4fb90a69ff604719d084407d05 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 19 Mar 2026 14:58:31 +0100 Subject: [PATCH 20/25] Bump NServiceBus.Transport.IBMMQ to 1.0.0-alpha.2 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index f396518336..fe99b4a90e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + From 2584e444bab877c941e9e231e0534493899de99b Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 20 Mar 2026 10:38:37 +0100 Subject: [PATCH 21/25] =?UTF-8?q?=F0=9F=93=A6=20Bump=20NServiceBus.Transpo?= =?UTF-8?q?rt.IBMMQ=20to=201.0.0-alpha.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fe99b4a90e..344cc09f9a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + From 697f1bbe23eb5e5ab69f4a5ac294b841aae51ef3 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 23 Mar 2026 12:13:21 +0100 Subject: [PATCH 22/25] Bump NServiceBus.Transport.IBMMQ to 1.0.0-alpha.4 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 344cc09f9a..9dd4dbca55 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + From cd6d785976c3f0ea16306efcff66b2fbd241ae32 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 23 Mar 2026 16:47:50 +0100 Subject: [PATCH 23/25] =?UTF-8?q?=E2=9C=A8=20Include=20IBMMQ=20transport?= =?UTF-8?q?=20in=20container=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ProjectReferences.Transports.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ProjectReferences.Transports.props b/src/ProjectReferences.Transports.props index 78e264658b..ae9bd3bd3d 100644 --- a/src/ProjectReferences.Transports.props +++ b/src/ProjectReferences.Transports.props @@ -3,6 +3,7 @@ + From 1229d718a09ef472db55ace263717888c00ca778 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Tue, 24 Mar 2026 09:46:15 +0100 Subject: [PATCH 24/25] =?UTF-8?q?=F0=9F=93=A6=20Bump=20NServiceBus.Transpo?= =?UTF-8?q?rt.IBMMQ=20to=201.0.0-alpha.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IBMMQTransportOptions was removed; properties moved directly onto IBMMQTransport with a parameterless constructor. --- src/Directory.Packages.props | 2 +- .../TransportTestsConfiguration.cs | 2 +- .../IBMMQTransportCustomization.cs | 13 ++++++------- .../TestConnectionDetails.cs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9dd4dbca55..894aeeba66 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + diff --git a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs index 6d52918c91..da512c3ce5 100644 --- a/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs +++ b/src/ServiceControl.Transports.IBMMQ.Tests/TransportTestsConfiguration.cs @@ -44,7 +44,7 @@ sealed class TestIBMMQTransportCustomization : IBMMQTransportCustomization { protected override IBMMQTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - transportSettings.Set>(o => o.ResourceNameSanitizer = name => name + transportSettings.Set>(o => o.ResourceNameSanitizer = name => name .Replace("ServiceControlMonitoring", "SCM") // Mitigate max queue name length .Replace("-", ".") // dash is an illegal char ); diff --git a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs index a818034d14..ac98324f52 100644 --- a/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs +++ b/src/ServiceControl.Transports.IBMMQ/IBMMQTransportCustomization.cs @@ -25,15 +25,14 @@ protected override void AddTransportForMonitoringCore(IServiceCollection service protected override IBMMQTransport CreateTransport(TransportSettings transportSettings, TransportTransactionMode preferredTransactionMode = TransportTransactionMode.ReceiveOnly) { - var transport = new IBMMQTransport(o => + var transport = new IBMMQTransport(); + + if (transportSettings.TryGet>(out var overrides)) { - if (transportSettings.TryGet>(out var overrides)) - { - overrides(o); - } + overrides(transport); + } - TestConnectionDetails.Apply(o); - }); + TestConnectionDetails.Apply(transport); transport.TransportTransactionMode = transport.GetSupportedTransactionModes().Contains(preferredTransactionMode) ? preferredTransactionMode : TransportTransactionMode.ReceiveOnly; diff --git a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs index 573ac0e28d..dd4ea4f7c1 100644 --- a/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs +++ b/src/ServiceControl.Transports.IBMMQ/TestConnectionDetails.cs @@ -30,7 +30,7 @@ static class TestConnectionDetails public static string TopicPrefix => Query["topicprefix"] ?? "DEV"; - public static void Apply(IBMMQTransportOptions options) + public static void Apply(IBMMQTransport options) { options.Host = Host; options.Port = Port; From b5d76d01fb08de58636a8fd4e931d31183ecfecb Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Tue, 24 Mar 2026 14:59:13 +0100 Subject: [PATCH 25/25] =?UTF-8?q?=F0=9F=93=A6=20Bump=20NServiceBus.Transpo?= =?UTF-8?q?rt.IBMMQ=20to=201.0.0-alpha.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add local NuGet package source and ShortenedTopicNaming support. --- ...eBus.Transport.IBMMQ.1.0.0-alpha.3.2.nupkg | Bin 0 -> 78335 bytes nuget.config | 1 + src/Directory.Packages.props | 2 +- .../ShortenedTopicNaming.cs | 27 ++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 local-packages/NServiceBus.Transport.IBMMQ.1.0.0-alpha.3.2.nupkg create mode 100644 src/ServiceControl.Transports.IBMMQ/ShortenedTopicNaming.cs diff --git a/local-packages/NServiceBus.Transport.IBMMQ.1.0.0-alpha.3.2.nupkg b/local-packages/NServiceBus.Transport.IBMMQ.1.0.0-alpha.3.2.nupkg new file mode 100644 index 0000000000000000000000000000000000000000..bd394e52c4b47b03e1eac45db43b77eff58eb197 GIT binary patch literal 78335 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 Ij|qtX0y?fuasU7T literal 0 HcmV?d00001 diff --git a/nuget.config b/nuget.config index d72d1d3d28..892e0ea8b5 100644 --- a/nuget.config +++ b/nuget.config @@ -3,6 +3,7 @@ + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 894aeeba66..7895ad82d2 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -52,7 +52,7 @@ - + diff --git a/src/ServiceControl.Transports.IBMMQ/ShortenedTopicNaming.cs b/src/ServiceControl.Transports.IBMMQ/ShortenedTopicNaming.cs new file mode 100644 index 0000000000..f45c8a53cf --- /dev/null +++ b/src/ServiceControl.Transports.IBMMQ/ShortenedTopicNaming.cs @@ -0,0 +1,27 @@ +namespace ServiceControl.Transports.IBMMQ; + +using System; +using System.Security.Cryptography; +using System.Text; +using NServiceBus.Transport.IBMMQ; + +class ShortenedTopicNaming : TopicNaming +{ + public ShortenedTopicNaming(string topicPrefix = "DEV") : base(topicPrefix) => prefix = topicPrefix; + + readonly string prefix; + + public override string GenerateTopicName(Type eventType) + { + var fullName = (eventType.FullName ?? eventType.Name).Replace('+', '.').ToUpperInvariant(); + var name = $"{prefix.ToUpperInvariant()}.{fullName}"; + + if (name.Length <= 48) + { + return name; + } + + var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(name)))[..8]; + return $"{name[..(48 - 9)]}_{hash}"; + } +}