Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Ev.ServiceBus.sln
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{15791C07-7
docs\Ev.ServiceBus.HealthChecks.md = docs\Ev.ServiceBus.HealthChecks.md
docs\Ev.ServiceBus.Mvc.md = docs\Ev.ServiceBus.Mvc.md
docs\Instrumentation.md = docs\Instrumentation.md
docs\Isolation.md = docs\Isolation.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8EC286EB-A71F-4431-ACA4-E92C79975817}"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ across the whole system (name of the property : `PayloadTypeId`).
* [Initial Set up](./docs/SetUp.md)
* [How to send messages](./docs/SendMessages.md)
* [How to receive messages](./docs/ReceiveMessages.md)
* [Isolation Feature](./docs/Isolation.md)
* [Advanced scenarios](./docs/AdvancedScenarios.md)
* [Ev.ServiceBus.HealthChecks](./docs/Ev.ServiceBus.HealthChecks.md)
* [Instrumentation](./docs/Instrumentation.md)
Expand Down
18 changes: 0 additions & 18 deletions docs/AdvancedScenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,3 @@ public class MyMessageSender
}
}
```

## Running service instance in isolation

By "running in isolation" we mean ability to run an instance of microservice (MS) on local environment, when MS is using real queues and topics to communicate with other MSs
Simple case : App1 sending message to App2, which responds back with another message.
Desired isolation is in the following: If message comes from App1.Local, then the response to it should be processed also only by App1.Local. Same holds true from App1.Cloud - it should process responses that are only coming from cloud instances.

This is achived by simply proper condifuration (see sample project)
```csharp
services.AddServiceBus(
settings =>
{
// Provide a connection string here !
settings.WithConnection("Endpoint=sb://yourconnection.servicebus.windows.net/;SharedAccessKeyName=yourkeyh;SharedAccessKey=ae6pTuOBAFDH2y7xJJf9BFubZGxXMToN6B9NiVgLnbQ=", new ServiceBusClientOptions());
settings.UseIsolation = true;
settings.IsolationKey = "your-isolation-key";
})
```
88 changes: 88 additions & 0 deletions docs/Isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Isolation feature

## Concept

In a microservice ecosystem where those services communicate through messages via Service bus queues or topics, some scenarios become difficult to debug.
For example, sometimes you want to manually send a message to a queue and debug in local how your microservice behaves. That would require you to recreate a service bus namespace with the proper queue configured and link app running in local to it.
Another example is, sometimes you want to send a message to one microservice to see and debug how another microservice behaves down the line.

The isolation feature allows your local application to connect to the service bus namespace of an up and running environment without disturbing it's stability.
It then allows you to manually send messages with some metadata to that queue and you local application will receive it instead of the app running in the environment.

## Configuration

### Default configuration
By default, the isolation feature is disabled. your application will treat every incoming messages.
```csharp
services.AddServiceBus(
settings =>
{
// Provide a connection string here !
settings.WithConnection("Endpoint=sb://yourconnection.servicebus.windows.net/;SharedAccessKeyName=yourkeyh;SharedAccessKey=ae6pTuOBAFDH2y7xJJf9BFubZGxXMToN6B9NiVgLnbQ=", new ServiceBusClientOptions());
settings.WithIsolation(IsolationBehavior.HandleAllMessages, null, null);
})
```

### Environment configuration
To use the feature, you need to activate isolation both on your local app and on your environment.

Applications running in your environment must use the "HandleNonIsolatedMessages" behavior.
You also need to provide a name for your microservice.

```csharp
services.AddServiceBus(
settings =>
{
// Provide a connection string here !
settings.WithConnection("Endpoint=sb://yourconnection.servicebus.windows.net/;SharedAccessKeyName=yourkeyh;SharedAccessKey=ae6pTuOBAFDH2y7xJJf9BFubZGxXMToN6B9NiVgLnbQ=", new ServiceBusClientOptions());
settings.WithIsolation(IsolationBehavior.HandleNonIsolatedMessages, null, "My.Application");
})
```

### Local configuration
To use the feature, you need to activate isolation both on your local app and on your environment.

The Application running in local must use the "HandleIsolatedMessage" behavior.
You also need to provide a name for your microservice and an isolation key.
Your local application will only process messages that have the same isolation key as the one you provide.

```csharp
services.AddServiceBus(
settings =>
{
// Provide a connection string here !
settings.WithConnection("Endpoint=sb://yourconnection.servicebus.windows.net/;SharedAccessKeyName=yourkeyh;SharedAccessKey=ae6pTuOBAFDH2y7xJJf9BFubZGxXMToN6B9NiVgLnbQ=", new ServiceBusClientOptions());
settings.WithIsolation(IsolationBehavior.HandleIsolatedMessage, "My.IsolationKey", "My.Application");
})
```

## Usage

### Simple case
Now to use the feature, you will need to send a message with the proper metadata to the queue/topic of your environment.

Let's say you have a microservice `App1` and you want to run that application on local for debugging purposes.
You will need to send a message to the queue/topic of `App1` with the following metadata:
```json
{
"IsolationKey": "My.IsolationKey",
"ApplicationName": "App1"
}
```
Your local app will receive the message and process it.

### Complex case
Now let's say that in your environment you have the following microservices: `App1`, `App2`, `App3`, `App4`, `App5`.
They communicate with each other through messages.
The entry point of your ecosystem is `App1`, but you want to debug `App2` and `App3` locally.

You will need to send a message to the queue/topic of `App1` with the following metadata:
```json
{
"IsolationKey": "My.IsolationKey",
"ApplicationName": "App2,App3"
}
```
The isolation metadata will be transferred to any message sent during the processing of the isolated message.

Meaning that your local apps will be able to receive subsequent isolated messages allowing you to debug them.
7 changes: 7 additions & 0 deletions samples/Ev.ServiceBus.Samples.Receiver/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Azure.Messaging.ServiceBus;
using Ev.ServiceBus.Abstractions;
using Ev.ServiceBus.AsyncApi;
using Ev.ServiceBus.Prometheus;
using Ev.ServiceBus.Sample.Contracts;
Expand Down Expand Up @@ -73,6 +74,12 @@ private static WebApplicationBuilder CreateHostBuilder(string[] args)
settings.WithConnection(
"Endpoint=sb://yourconnection.servicebus.windows.net/;SharedAccessKeyName=yourkeyh;SharedAccessKey=ae6pTuOBAFDH2y7xJJf9BFubZGxXMToN6B9NiVgLnbQ=",
new ServiceBusClientOptions());

//
settings.WithIsolation(
IsolationBehavior.HandleIsolatedMessages,
"MyIsolationKey",
"Company.ReceiverApp");
})
// Enables you to execute code whenever execution of a message starts, succeeded or failed
.RegisterEventListener<ServiceBusEventListener>()
Expand Down
35 changes: 27 additions & 8 deletions src/Ev.ServiceBus.Abstractions/Configuration/ServiceBusSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,7 @@ public sealed class ServiceBusSettings
/// </summary>
public ConnectionSettings? ConnectionSettings { get; private set; }

/// <summary>
/// When true, The application will subscribe to getting messages from topic in isolation mode, using IsolationKey as differentiator
/// </summary>
public bool UseIsolation { get; set; } = false;
/// <summary>
/// Key that is used to determine if this running instance should receive and complete or abandon a message
/// </summary>
public string? IsolationKey { get; set; } = null;
public IsolationSettings IsolationSettings { get; internal set; } = new (IsolationBehavior.HandleAllMessages, null, null);

/// <summary>
/// Sets the default Connection to use for every resource. (this can be overriden on each and every resource you want)
Expand All @@ -41,4 +34,30 @@ public void WithConnection(string connectionString, ServiceBusClientOptions opti
{
ConnectionSettings = new ConnectionSettings(connectionString, options);
}

public void WithIsolation(IsolationBehavior behavior, string? isolationKey = null, string? applicationName = null)
{
IsolationSettings = new IsolationSettings(behavior, isolationKey, applicationName);
}
}

public class IsolationSettings
{
public IsolationSettings(IsolationBehavior behavior, string? isolationKey, string? applicationName)
{
IsolationBehavior = behavior;
IsolationKey = isolationKey;
ApplicationName = applicationName;
}

public IsolationBehavior IsolationBehavior { get; private set; }
public string? IsolationKey { get; private set; }
public string? ApplicationName { get; private set; }
}

public enum IsolationBehavior
{
HandleAllMessages = 1,
HandleIsolatedMessages,
HandleNonIsolatedMessages
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace Ev.ServiceBus.Abstractions;

public class ConfigurationException : Exception
{
public ConfigurationException(string message) : base(message)
{
}
}
25 changes: 25 additions & 0 deletions src/Ev.ServiceBus.Abstractions/MessageHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Azure.Messaging.ServiceBus;
using System.Collections.Generic;
using System.Linq;

namespace Ev.ServiceBus.Abstractions;

Expand Down Expand Up @@ -38,6 +39,22 @@ public static ServiceBusMessage CreateMessage(string contentType, byte[] body, s
return message;
}

public static string[] GetIsolationApps(this ServiceBusReceivedMessage message)
{
var appsString = TryGetValue(message, UserProperties.IsolationApps);
if (string.IsNullOrEmpty(appsString))
return [];
return appsString.Split(',');
}

public static string[] GetIsolationApps(this IReadOnlyDictionary<string, object> applicationProperties)
{
if (applicationProperties == null)
return [];
applicationProperties.TryGetValue(UserProperties.IsolationApps, out var value);
return value == null ? [] : ((string)value).Split(',');
}

public static string? GetIsolationKey(this ServiceBusReceivedMessage message)
{
return TryGetValue(message, UserProperties.IsolationKey);
Expand All @@ -64,6 +81,14 @@ public static ServiceBusMessage SetIsolationKey(this ServiceBusMessage message,
return message;
}

public static ServiceBusMessage SetIsolationApps(this ServiceBusMessage message, string[] isolationApps)
{
if (isolationApps.Length == 0)
return message;
message.ApplicationProperties[UserProperties.IsolationApps] = string.Join(',', isolationApps);
return message;
}

public static void SetIsolationKey(this IDictionary<string, object> applicationProperties, string? isolationKey)
{
if (string.IsNullOrEmpty(isolationKey))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public MessageContext(ProcessSessionMessageEventArgs args, ClientType clientType
CancellationToken = args.CancellationToken;
PayloadTypeId = Message.GetPayloadTypeId();
IsolationKey = Message.GetIsolationKey();
IsolationApps = Message.GetIsolationApps();
}

public MessageContext(ProcessMessageEventArgs args, ClientType clientType, string resourceId)
Expand All @@ -28,6 +29,7 @@ public MessageContext(ProcessMessageEventArgs args, ClientType clientType, strin
CancellationToken = args.CancellationToken;
PayloadTypeId = Message.GetPayloadTypeId();
IsolationKey = Message.GetIsolationKey();
IsolationApps = Message.GetIsolationApps();
}

public ServiceBusReceivedMessage Message { get; }
Expand All @@ -40,6 +42,7 @@ public MessageContext(ProcessMessageEventArgs args, ClientType clientType, strin
public string? PayloadTypeId { get; internal set; }
public MessageReceptionRegistration? ReceptionRegistration { get; internal set; }
public string? IsolationKey { get; internal set; }
public string[] IsolationApps { get; internal set; }

public MessageExecutionContext ReadExecutionContext()
{
Expand Down
1 change: 1 addition & 0 deletions src/Ev.ServiceBus.Abstractions/UserProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public static class UserProperties
public const string PayloadTypeIdProperty = "PayloadTypeId";
public const string MessageTypeProperty = "MessageType";
public const string IsolationKey = "IsolationKey";
public const string IsolationApps = "IsolationApps";
}
14 changes: 11 additions & 3 deletions src/Ev.ServiceBus/Dispatch/DispatchSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,10 @@

private class MessagesPerResource
{
public MessageToSend[] Messages { get; set; }

Check warning on line 192 in src/Ev.ServiceBus/Dispatch/DispatchSender.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Messages' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 192 in src/Ev.ServiceBus/Dispatch/DispatchSender.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Messages' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public ClientType ClientType { get; set; }
public string ResourceId { get; set; }

Check warning on line 194 in src/Ev.ServiceBus/Dispatch/DispatchSender.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'ResourceId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 194 in src/Ev.ServiceBus/Dispatch/DispatchSender.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'ResourceId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public IMessageSender Sender { get; set; }

Check warning on line 195 in src/Ev.ServiceBus/Dispatch/DispatchSender.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'Sender' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}

private class MessageToSend
Expand Down Expand Up @@ -239,8 +239,6 @@
MessageDispatchRegistration registration,
Abstractions.Dispatch dispatch)
{
var originalCorrelationId = _messageMetadataAccessor.Metadata?.CorrelationId ?? Guid.NewGuid().ToString();
var originalIsolationKey = _messageMetadataAccessor.Metadata?.ApplicationProperties.GetIsolationKey();
var result = _messagePayloadSerializer.SerializeBody(dispatch.Payload);
var message = MessageHelper.CreateMessage(result.ContentType, result.Body, registration.PayloadTypeId);

Expand All @@ -251,10 +249,20 @@
}

message.SessionId = dispatch.SessionId;

var originalCorrelationId = _messageMetadataAccessor.Metadata?.CorrelationId ?? Guid.NewGuid().ToString();
message.CorrelationId = dispatch.CorrelationId ?? originalCorrelationId;
message.SetIsolationKey(originalIsolationKey ?? _serviceBusOptions.Settings.IsolationKey);

var originalIsolationKey = _messageMetadataAccessor.Metadata?.ApplicationProperties.GetIsolationKey();
message.SetIsolationKey(originalIsolationKey ?? _serviceBusOptions.Settings.IsolationSettings.IsolationKey);

var originalIsolationApps = _messageMetadataAccessor.Metadata?.ApplicationProperties.GetIsolationApps() ?? [];
message.SetIsolationApps(originalIsolationApps);

if (dispatch.DiagnosticId != null)
{
message.SetDiagnosticIdIfIsNot(dispatch.DiagnosticId);
}
if (!string.IsNullOrWhiteSpace(dispatch.MessageId))
{
message.MessageId = dispatch.MessageId;
Expand Down
Loading
Loading