Skip to content

Commit 5d46354

Browse files
authored
[Messaging] Minor updates for April 2025 release (Azure#49169)
* [Messaging] Minor updates for April 2025 release The focus of these changes is to make a small set of minor updates to the Azure Messaging libraries. Included are: - Clarifying the load balancing behavior of the Event Hubs processor's load balancing strategies. - Enhancing the retry policies for better support in web socket scenarios. It was observed that the relevant failure information in many scenarios was embedded in an inner `WebSocketException` and wrapped by `WebSocketException`. Going forward, retry decisions will be made based on the inner exception, if present. - Added an option for a remote certificate validation callback to the `ServiceBusClientOptions` in order to allow applications to control validation behavior and to align with the existing option in Event Hubs. * Reverting copy/paste error * Restoring accidental deletion * Fixing test mocks * Regenerating Event Hubs API * Fixing missed Clone for the callback.
1 parent d721c3b commit 5d46354

File tree

18 files changed

+318
-56
lines changed

18 files changed

+318
-56
lines changed

sdk/eventhub/Azure.Messaging.EventHubs.Processor/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
### Bugs Fixed
1010

11-
- UpdateCheckpointAsync in the 'EventProcessorClient` was not awaiting the Checkpoint Stores 'UpdateCheckpointAsync' method.
12-
By not awaiting we were starting the OTel span and closing it almost immediately. The catch would've been ignored as well.
11+
- Fixed behavior in the `UpdateCheckpointAsync` method of `EventProcessorClient` to properly await the call to the checkpoint store. Previously, this was not awaited, causing the OTel span to open and close almost immediately and skip reporting any errors observed.
1312

1413
### Other Changes
1514

15+
- Enhanced retry logic to consider additional cases for web socket-based failures. In many cases, a `WebSocketException` is triggered which wraps a `SocketException` with the details for the specific network conditions. Retry decisions are now based on the internal exception, if present, to ensure retries are correctly applied.
16+
1617
## 5.12.0-beta.2 (2025-02-11)
1718

1819
### Acknowledgments

sdk/eventhub/Azure.Messaging.EventHubs.Shared/src/Core/BasicRetryPolicy.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Globalization;
66
using System.IO;
77
using System.Net.Sockets;
8+
using System.Net.WebSockets;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Azure.Core;
@@ -140,14 +141,16 @@ public BasicRetryPolicy(EventHubsRetryOptions retryOptions)
140141
///
141142
private static bool ShouldRetryException(Exception exception)
142143
{
143-
if ((exception is TaskCanceledException) || (exception is OperationCanceledException))
144-
{
145-
exception = exception?.InnerException;
146-
}
147-
else if (exception is AggregateException aggregateEx)
144+
// Transform the exception into a more relevant type, if possible.
145+
146+
exception = exception switch
148147
{
149-
exception = aggregateEx?.Flatten().InnerException;
150-
}
148+
TaskCanceledException => exception?.InnerException,
149+
OperationCanceledException => exception?.InnerException,
150+
WebSocketException => exception?.InnerException ?? exception,
151+
AggregateException aggregateEx => aggregateEx?.Flatten().InnerException,
152+
_ => exception
153+
};
151154

152155
switch (exception)
153156
{

sdk/eventhub/Azure.Messaging.EventHubs.Shared/tests/RetryPolicies/BasicRetryPolicyTests.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Net.Sockets;
8+
using System.Net.WebSockets;
89
using System.Threading.Tasks;
910
using Azure.Messaging.EventHubs.Core;
1011
using Moq;
@@ -31,6 +32,9 @@ public static IEnumerable<object[]> RetriableExceptionTestCases()
3132
yield return new object[] { new IOException() };
3233
yield return new object[] { new UnauthorizedAccessException() };
3334

35+
// WebSocketException should use the inner exception as the decision point.
36+
yield return new object[] { new WebSocketException("dummy", new EventHubsException(true, null)) };
37+
3438
// Task/Operation Canceled should use the inner exception as the decision point.
3539

3640
yield return new object[] { new TaskCanceledException("dummy", new EventHubsException(true, null)) };
@@ -63,6 +67,9 @@ public static IEnumerable<object[]> NonRetriableExceptionTestCases()
6367
yield return new object[] { new SocketException((int)SocketError.HostNotFound) };
6468
yield return new object[] { new SocketException((int)SocketError.HostUnreachable) };
6569

70+
// WebSocketException should use the inner exception as the decision point.
71+
yield return new object[] { new WebSocketException("dummy", new EventHubsException(false, null)) };
72+
6673
// Task/Operation Canceled should use the inner exception as the decision point.
6774

6875
yield return new object[] { new TaskCanceledException("dummy", new EventHubsException(false, null)) };
@@ -96,7 +103,7 @@ public static IEnumerable<object[]> NonRetriableExceptionTestCases()
96103
[TestCase(2)]
97104
[TestCase(10)]
98105
[TestCase(100)]
99-
public void CalulateTryTimeoutRespectsOptions(int attemptCount)
106+
public void CalculateTryTimeoutRespectsOptions(int attemptCount)
100107
{
101108
var timeout = TimeSpan.FromSeconds(5);
102109
var options = new EventHubsRetryOptions { TryTimeout = timeout };
@@ -255,7 +262,7 @@ public void CalculateRetryDelayRespectsThrottleInterval()
255262
/// </summary>
256263
///
257264
[Test]
258-
public void CalculateRetryDelayDoesNotTrottleGeneralExceptions()
265+
public void CalculateRetryDelayDoesNotThrottleGeneralExceptions()
259266
{
260267
var delaySeconds = 1;
261268
var exception = new EventHubsException(true, "dummy", EventHubsException.FailureReason.GeneralError);

sdk/eventhub/Azure.Messaging.EventHubs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
### Other Changes
1212

13+
- Enhanced retry logic to consider additional cases for web socket-based failures. In many cases, a `WebSocketException` is triggered which wraps a `SocketException` with the details for the specific network conditions. Retry decisions are now based on the internal exception, if present, to ensure retries are correctly applied.
14+
1315
## 5.12.0-beta.2 (2025-02-11)
1416

1517
### Acknowledgments

sdk/eventhub/Azure.Messaging.EventHubs/api/Azure.Messaging.EventHubs.net8.0.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,13 +818,11 @@ public static partial class EventHubClientBuilderExtensions
818818
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Consumer.EventHubConsumerClient, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions> AddEventHubConsumerClient<TBuilder>(this TBuilder builder, string consumerGroup, string connectionString) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilder { throw null; }
819819
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Consumer.EventHubConsumerClient, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions> AddEventHubConsumerClient<TBuilder>(this TBuilder builder, string consumerGroup, string connectionString, string eventHubName) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilder { throw null; }
820820
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Binding strongly typed objects to configuration values requires generating dynamic code at runtime, for example instantiating generic types. Use the Configuration Binder Source Generator (EnableConfigurationBindingGenerator=true) instead.")]
821-
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Binding strongly typed objects to configuration values is not supported with trimming. Use the Configuration Binder Source Generator (EnableConfigurationBindingGenerator=true) instead.")]
822821
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Consumer.EventHubConsumerClient, Azure.Messaging.EventHubs.Consumer.EventHubConsumerClientOptions> AddEventHubConsumerClient<TBuilder, TConfiguration>(this TBuilder builder, TConfiguration configuration) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilderWithConfiguration<TConfiguration> { throw null; }
823822
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Producer.EventHubProducerClient, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions> AddEventHubProducerClientWithNamespace<TBuilder>(this TBuilder builder, string fullyQualifiedNamespace, string eventHubName) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilderWithCredential { throw null; }
824823
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Producer.EventHubProducerClient, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions> AddEventHubProducerClient<TBuilder>(this TBuilder builder, string connectionString) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilder { throw null; }
825824
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Producer.EventHubProducerClient, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions> AddEventHubProducerClient<TBuilder>(this TBuilder builder, string connectionString, string eventHubName) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilder { throw null; }
826825
[System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Binding strongly typed objects to configuration values requires generating dynamic code at runtime, for example instantiating generic types. Use the Configuration Binder Source Generator (EnableConfigurationBindingGenerator=true) instead.")]
827-
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Binding strongly typed objects to configuration values is not supported with trimming. Use the Configuration Binder Source Generator (EnableConfigurationBindingGenerator=true) instead.")]
828826
public static Azure.Core.Extensions.IAzureClientBuilder<Azure.Messaging.EventHubs.Producer.EventHubProducerClient, Azure.Messaging.EventHubs.Producer.EventHubProducerClientOptions> AddEventHubProducerClient<TBuilder, TConfiguration>(this TBuilder builder, TConfiguration configuration) where TBuilder : Azure.Core.Extensions.IAzureClientFactoryBuilderWithConfiguration<TConfiguration> { throw null; }
829827
}
830828
}

sdk/eventhub/Azure.Messaging.EventHubs/src/Processor/LoadBalancingStrategy.cs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,36 @@ namespace Azure.Messaging.EventHubs.Processor
1212
public enum LoadBalancingStrategy
1313
{
1414
/// <summary>
15-
/// An event processor will take a measured approach to requesting
16-
/// partition ownership when balancing work with other processors, slowly
17-
/// claiming partitions until a stabilized distribution is achieved.
15+
/// An event processor will take a slow approach to requesting
16+
/// partition ownership when balancing work with other processors, waiting
17+
/// until a load balancing cycle is schedule to run, claiming 1 partition
18+
/// per cycle until a stabilized distribution is achieved.
1819
///
19-
/// <para>When using this strategy, it may take longer for all partitions of
20+
/// <para>When using this strategy, it will be considerably longer for all partitions of
2021
/// an Event Hub to be owned by a processor when processing first starts, the
21-
/// number of active processors changes, or when partitions are scaled. The
22-
/// Balanced strategy will reduce contention for a partition, ensuring that once
23-
/// it is claimed, processing will be more likely to be steady and consistent.</para>
22+
/// number of active processors changes, or when partitions are scaled.</para>
23+
///
24+
/// <para>The Balanced strategy is generally not recommended, as it does not provide
25+
/// any tangible benefits unless the load balancing interval is set below 10 seconds,
26+
/// which is strongly discouraged. The Balanced strategy mainly exists to ensure backwards
27+
/// compatibility with earlier library versions.</para>
2428
/// </summary>
2529
///
2630
Balanced,
2731

2832
/// <summary>
2933
/// An event processor will attempt to claim ownership of its fair share of
30-
/// partitions aggressively when balancing work with other processors.
34+
/// partitions consistently, claiming 1 partition at a time until work is balanced
35+
/// between all active processors.
36+
///
37+
/// <para>When using this strategy, load balancing cycles run without delay until
38+
/// ownership is evenly distributed, ensuring that partitions are processed more quickly
39+
/// when processing first starts, the number of active processors changes, or
40+
/// when partitions are scaled.</para>
3141
///
32-
/// <para>When using this strategy, all partitions of an Event Hub will be claimed
33-
/// quickly when processing first starts, the number of active processors changes, or
34-
/// when partitions are scaled. The Greedy strategy is likely to cause competition for
35-
/// ownership of a given partition, causing it to see sporadic processing and some amount of
36-
/// duplicate events until balance has been reached and work is distributed equally among the
37-
/// active processors.</para>
42+
/// <para>The Greedy strategy will not cause additional competition for township of a given
43+
/// when compared to other strategies, as it allows claims from other processors to safely
44+
/// interleave. Greedy is the recommended default strategy for processors.</para>
3845
/// </summary>
3946
///
4047
Greedy

sdk/servicebus/Azure.Messaging.ServiceBus/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- `ServiceBusClientOptions` now supports registering a callback delegate for participating in the validation of SSL certificates when connections are established. This delegate may override the built-in validation and allow or deny certificates based on application-specific logic.
8+
79
### Breaking Changes
810

911
### Bugs Fixed
@@ -12,6 +14,8 @@
1214

1315
- Added jitter to the lock renewal timer to reduce the likelihood of lock renewal collisions when using the `ServiceBusProcessor` or the `ServiceBusSessionProcessor`.
1416

17+
- Enhanced retry logic to consider additional cases for web socket-based failures. In many cases, a `WebSocketException` is triggered which wraps a `SocketException` with the details for the specific network conditions. Retry decisions are now based on the internal exception, if present, to ensure retries are correctly applied.
18+
1519
## 7.18.4 (2025-02-11)
1620

1721
### Bugs Fixed

sdk/servicebus/Azure.Messaging.ServiceBus/api/Azure.Messaging.ServiceBus.net8.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public ServiceBusClient(string connectionString, Azure.Messaging.ServiceBus.Serv
134134
public partial class ServiceBusClientOptions
135135
{
136136
public ServiceBusClientOptions() { }
137+
public System.Net.Security.RemoteCertificateValidationCallback CertificateValidationCallback { get { throw null; } set { } }
137138
public System.TimeSpan ConnectionIdleTimeout { get { throw null; } set { } }
138139
public System.Uri CustomEndpointAddress { get { throw null; } set { } }
139140
public bool EnableCrossEntityTransactions { get { throw null; } set { } }

sdk/servicebus/Azure.Messaging.ServiceBus/api/Azure.Messaging.ServiceBus.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public ServiceBusClient(string connectionString, Azure.Messaging.ServiceBus.Serv
134134
public partial class ServiceBusClientOptions
135135
{
136136
public ServiceBusClientOptions() { }
137+
public System.Net.Security.RemoteCertificateValidationCallback CertificateValidationCallback { get { throw null; } set { } }
137138
public System.TimeSpan ConnectionIdleTimeout { get { throw null; } set { } }
138139
public System.Uri CustomEndpointAddress { get { throw null; } set { } }
139140
public bool EnableCrossEntityTransactions { get { throw null; } set { } }

sdk/servicebus/Azure.Messaging.ServiceBus/samples/Sample13_AdvancedConfiguration.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,39 @@ var options = new ServiceBusClientOptions
3535
ServiceBusClient client = new(fullyQualifiedNamespace, new DefaultAzureCredential(), options);
3636
```
3737

38+
### Influencing SSL certificate validation
39+
40+
For some environments using a proxy or custom gateway for routing traffic to Service Bus, a certificate not trusted by the root certificate authorities may be issued. This can often be a self-signed certificate from the gateway or one issued by a company's internal certificate authority.
41+
42+
By default, these certificates are not trusted by the Service Bus client library and the connection will be refused. To enable these scenarios, a [RemoteCertificateValidationCallback](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback) can be registered to provide custom validation logic for remote certificates. This allows an application to override the default trust decision and assert responsibility for accepting or rejecting the certificate.
43+
44+
```C# Snippet:ServiceBusConfigureRemoteCertificateValidationCallback
45+
string fullyQualifiedNamespace = "<fully_qualified_namespace>";
46+
DefaultAzureCredential credential = new();
47+
48+
static bool ValidateServerCertificate(
49+
object sender,
50+
X509Certificate certificate,
51+
X509Chain chain,
52+
SslPolicyErrors sslPolicyErrors)
53+
{
54+
if ((sslPolicyErrors == SslPolicyErrors.None)
55+
|| (certificate.Issuer == "My Company CA"))
56+
{
57+
return true;
58+
}
59+
60+
// Do not allow communication with unauthorized servers.
61+
62+
return false;
63+
}
64+
65+
ServiceBusClient client = new(fullyQualifiedNamespace, credential, new ServiceBusClientOptions
66+
{
67+
CertificateValidationCallback = ValidateServerCertificate
68+
});
69+
```
70+
3871
## Customizing the retry options
3972

4073
The retry options are used to configure the retry policy for all operations when communicating with the service. The default values are shown below, but they can be customized to fit your scenario.

0 commit comments

Comments
 (0)