Skip to content

[Discussion] Implementing a FusionCache Backplane with Azure Service Bus #370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cyrildurand opened this issue Jan 30, 2025 · 13 comments
Open

Comments

@cyrildurand
Copy link

cyrildurand commented Jan 30, 2025

My app is hosted on Azure, and I already use Azure Service Bus and Cosmos DB. Since Azure Redis is costly and my app doesn’t require its level of performance, I’m exploring Cosmos DB for distributed caching and Azure Service Bus as a backplane.

From my research, it seems that Redis is the only officially supported backplane implementation, and I couldn't find an existing implementation using Azure Service Bus.

Questions:

  1. Does using Azure Service Bus as a backplane make sense?
  2. Would there be any drawbacks compared to Redis?

My Implementation

I started working on a Service Bus-based backplane, but I have some concerns—particularly with the Subscribe method.

Here’s my current (dummy/basic/work-in-progress) implementation:

using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane;

namespace ZiggyCreatures.FusionCache.Backplane.AzureServiceBus
{
	public class ServiceBusBackplane : IFusionCacheBackplane
	{
		private const String topicName = "fusionCache-dev";

		public ServiceBusBackplane(ServiceBusAdministrationClient serviceBusAdministrationClient, ServiceBusClient serviceBusClient)
		{
			this._serviceBusAdministrationClient = serviceBusAdministrationClient;
			this._serviceBusClient = serviceBusClient;
			this._serviceBusSender = new Lazy<ServiceBusSender>(() =>
			{
				return this._serviceBusClient.CreateSender(topicName);
			});
		}

		private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient;
		private readonly ServiceBusClient _serviceBusClient;
		private readonly Lazy<ServiceBusSender> _serviceBusSender;
		private ServiceBusProcessor _serviceBusProcessor;

		public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
		{
			this.PublishAsync(message, options, token).GetAwaiter().GetResult();
		}

		public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
		{
			await this._serviceBusSender.Value.SendMessageAsync(new ServiceBusMessage() { Body = new BinaryData(BackplaneMessage.ToByteArray(message)) }, token);
		}

		public void Subscribe(BackplaneSubscriptionOptions options)
		{
			this.SubscribeAsync(options).GetAwaiter().GetResult();
		}

		public void Unsubscribe()
		{
			this.UnsubscribeAsync().GetAwaiter().GetResult();
		}

		private async Task SubscribeAsync(BackplaneSubscriptionOptions options)
		{
			if (!await this._serviceBusAdministrationClient.TopicExistsAsync(topicName))
			{
				await this._serviceBusAdministrationClient.CreateTopicAsync(topicName);
			}
			var subcriptionName = $"fusion-{options.CacheInstanceId}";
			if (!await this._serviceBusAdministrationClient.SubscriptionExistsAsync(topicName, subcriptionName))
			{
				await this._serviceBusAdministrationClient.CreateSubscriptionAsync(topicName, subcriptionName);
			}
			this._serviceBusProcessor = this._serviceBusClient.CreateProcessor(topicName, subcriptionName);
			this._serviceBusProcessor.ProcessErrorAsync += async (args) =>
			{
                             // TODO
			};
			this._serviceBusProcessor.ProcessMessageAsync += async (args) =>
			{
				var data = args.Message.Body.ToArray();
				var msg = BackplaneMessage.FromByteArray(data);
				if (options.IncomingMessageHandlerAsync != null)
				{
					await options.IncomingMessageHandlerAsync(msg);
				}
			};
			await this._serviceBusProcessor.StartProcessingAsync();
		}

		private async Task UnsubscribeAsync()
		{
			await this._serviceBusProcessor.StopProcessingAsync();
			await this._serviceBusProcessor.DisposeAsync();
			// TODO delete subscription 
		}
	}
}

Concerns

Subscribe Method and Sync-over-Async Pattern

The Subscribe method is synchronous, which forces me to use a sync-over-async pattern (.GetAwaiter().GetResult()). I’d like to avoid this because it can lead to deadlocks or thread pool exhaustion in certain scenarios.

  • Is there a specific reason why this method is sync-only?
  • Would it make sense to introduce an async version of this method?
  • I checked the Redis-based backplane and noticed similar concerns — was there a design decision behind this?

Unsubscribe Method Behavior

I noticed that in the FusionCache Simulator App, the Unsubscribe method is never triggered.

  • Is this expected behavior?
  • If so, how does FusionCache typically handle backplane unsubscription?

Next Steps

If this approach makes sense, I’m happy to open a Pull Request (PR) to contribute this as an alternative backplane provider for FusionCache.

Would love to hear your thoughts! 🚀

@cyrildurand cyrildurand changed the title Discussion: Implementing a FusionCache Backplane with Azure Service Bus [Discussion] Implementing a FusionCache Backplane with Azure Service Bus Jan 30, 2025
@jodydonetti
Copy link
Collaborator

Hi @cyrildurand
I'll answer about the first part later, but for now:

Concerns

Subscribe Method and Sync-over-Async Pattern

The Subscribe method is synchronous, which forces me to use a sync-over-async pattern (.GetAwaiter().GetResult()). I’d like to avoid this because it can lead to deadlocks or thread pool exhaustion in certain scenarios.
[...]

You're right about this, and in fact since last week I've been working exactly on this.
Mind you, this never cause any issue that I'm aware of, but still it would be better to go full-async on all the code paths.

The next version (coming out very soon) will have this change in it.

@jodydonetti
Copy link
Collaborator

Hi @cyrildurand , v2.1 is out 🥳

@cyrildurand
Copy link
Author

Hi,

I updated my code sample with the new version, everything great.

I also found why the Unsubscibe method is not called, I used the simulator app and dispose is not called on this app. I will change the app and implement the unsubscribe method.

Would you be interested in having an azure service bus backplane ? If so, I would be glad to implement it and make a PR for this feature, otherwise I will take shortcut and just implement for my project.

@jodydonetti
Copy link
Collaborator

Hi!

I updated my code sample with the new version, everything great.

Great 👍

I also found why the Unsubscibe method is not called, I used the simulator app and dispose is not called on this app.

Ah you're right, my bad.

I just pushed a small change that adds explicit disposal of the clusters, see here.

Thanks for spotting this.

Would you be interested in having an azure service bus backplane ? If so, I would be glad to implement it and make a PR for this feature, otherwise I will take shortcut and just implement for my project.

This is definitely interesting, so let me tell you what I'm thinking about: instead of creating a specific backplane for each specific technology, I'm trying to understand if there's a way to create a "universal" one.

In the past I tried playing with Rebus, see here, but after talking with its creator it did went well, see here.

Recently I'm looking at Wolverine,which also has a provider for Azure Service Bus.

On one hand the universal one would reduce the number of specific implementations, while on the other it will require staying in line with the Wolverine (or others) implementations, roadmap, changes, etc.

What do you think about the plan? Any knowledge about Wolverine maybe?

Let me know, thanks!

@cyrildurand
Copy link
Author

Hi,

I did something similar for the dispose, I will update my local repository with your latest changes.

I try to limit the number of dependency, I also work with react app and my project now relies on many dead deep dependencies on the project I work.

I prefer having a backplane for each provider, the implementation for azure is not that complex and it make sense for me to have an implementation for azure service bus, one aws SQS and even one for Wolverine. I understand that from your point of view it's easier to relies on universal implementation and it avoid the burden to test and maintain each implementation.

I will continue working on the implementation for azure and deploy it live in February and will let you know. I will be happy to share and contribute the code.

In the past I tried playing with Rebus, see #111 (comment), but after talking with its creator it did went well, see #111 (comment).

I read that before starting the implementation and I was not sure to understand why. Is that because service bus concept is not designed for this ? For my application, performance of the backplane is not critical and I don't have that many key in cache.

I also consider using signalr to do the backplane, but it limits to only 20 nodes for the free version otherwise it's ~2$/day and my app may scale up to 30 nodes (https://azure.microsoft.com/en-us/pricing/details/signalr-service/#pricing).

@bbehrens
Copy link

The author of Wolverine is one of the sharpest guys I know. I'd also ping him about your nuget version issue. 2 decade + OSS guy

@jodydonetti
Copy link
Collaborator

I try to limit the number of dependency, I also work with react app and my project now relies on many dead deep dependencies on the project I work.

I prefer having a backplane for each provider, the implementation for azure is not that complex and it make sense for me to have an implementation for azure service bus, one aws SQS and even one for Wolverine.

Ok, understandable. I also like lightweight implementations, it's just that there are so many possible ones that it would be hard to keep track of all of them (regarding updates, new versions, etc). It's the same reason why I like the use of IDistributedCache so much (although its api surface area is quite limited), because it means that as long as the interface is respected, I don't even need to know an implementation exists, it all just works (I have community members using their own custom IDistributedCache impl, and I don't even know the details of them but everything works).

I will continue working on the implementation for azure and deploy it live in February and will let you know. I will be happy to share and contribute the code.

Ok let's do this, if you agree: after you deployed it your production environment and see that it works well, if you'd like I can take a look at it, then we can decide together the next move.

I see 2 options here:

  • you contribute that implementation to the FusionCache repo (or pass me the code, however you prefer) and in case of needed changes in the future I maintain it
  • you create your own repo with the 3rd party package, and I link to that in the docs but you maintain it. Of course I'm also open to other options that I may be missing, whatever works best for both of us 🙂

What do you think?

@cyrildurand
Copy link
Author

Hi Jody,

I worked on an implementation here : https://github.com/cyrildurand/FusionCache/blob/main/ZiggyCreatures.FusionCache.Backplane.AzureServiceBus/ServiceBusBackplane.cs

I still have to review constructor to provide IOptions support and provide some XML comment.

Once improvements is done, I will deploy it on a production project and see how it goes. So far test looks great.

For the contribution I'm open in all options. I think it would be more valuable for the community if I contribute by doing a PR to this project, this way, if you change interfaces, you can easily update the implementation.

@jodydonetti
Copy link
Collaborator

Hi @cyrildurand

I worked on an implementation here : https://github.com/cyrildurand/FusionCache/blob/main/ZiggyCreatures.FusionCache.Backplane.AzureServiceBus/ServiceBusBackplane.cs

Cool I'm looking at it right now, will let you know.

Once improvements is done, I will deploy it on a production project and see how it goes. So far test looks great.

Awesome, let me know how it goes in production.

For the contribution I'm open in all options. I think it would be more valuable for the community if I contribute by doing a PR to this project, this way, if you change interfaces, you can easily update the implementation.

Yeah it makes sense, I'm just thinking about what happens if everybody will do this, meaning 6 different implementations for 6 different technologies (eg: Azure Service Bus, Redis, MongoDB, etc) and having to maintain and tests all of them. But yeah, it's probably the safest route.

Let's update when the your run n production run will be over, and see how to move forward ok?

Thanks again!

@MiguelGuedelha
Copy link

MiguelGuedelha commented Feb 15, 2025

Couldn't this be handled in a way that doesn't involve needing more work from your side @jodydonetti

Essentially, you could release FusionCache related abstraction only packages, such as:

  • ZiggyCreatures.FusionCache.Backplane.Abstractions

I haven't had a too in-depth look at the current structure of the project so not 100% sure if a restructure/refactor of namespaces would be implied to be able to create and release such a package(s)

The community can go forth and create and maintain said extra provider packages themselves, for example?

So just like Microsoft provides the IDistributedCache abstractions for any of us to create community implementations, you'd provide the abstractions package containing only the IFusionCacheBackplane, and any other required interfaces to implement it end to end 🙂

Idk, food for thought?

EDIT: Or like aspire, have a CummunityToolkit repository that basically distances it from the main Aspire core resource implementations, but still allows it to be overseen by you but in a more "let other people contribute to it and provide updates, etc" (would be a bit more work from your side to manage said second repo compared to just releasing an abstractions package to the wild but 🤷‍♂ )

@cyrildurand
Copy link
Author

Hello Miguel, Jody,

For the contribution side, I'm open to everything and I'm not that experienced with open source contribution but I will be happy to help the way you prefer.

As a software editor and dependency Consumer, I always thinks about long term. If the azure service bus backplane is maintained by the same project than the core FusionCache project I will consider it as the same dependency group and won't think much about it.
If the service bus backplane is from another project I will think twice about it. I understand that it requires more work for this open source project.


BTW, I'm still converting my App and I had some complex test case I'm not sure about.

Let's consider the following code

            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
            {
                await doSomething("key")); 

                await cache.RemoveAsync("key");
                // or worse await cache.SetAsync("key", "newValue"); 

                await doSomethingElse(); // throws exception 


                scope.Complete(); 
            }

In that case, the service bus backplane will be part of the transaction and if a rollback occurs the service bus message will be unsent.

There is no error and everything goes well but I'm not sure if I should suppress the transaction scope in the backplane or if I just ignore and let it like this.

Any thought ?

@jodydonetti
Copy link
Collaborator

jodydonetti commented Feb 23, 2025

Hi @MiguelGuedelha

Essentially, you could release FusionCache related abstraction only packages, such as:

  • ZiggyCreatures.FusionCache.Backplane.Abstractions

I haven't had a too in-depth look at the current structure of the project so not 100% sure if a restructure/refactor of namespaces would be implied to be able to create and release such a package(s)

I'd need to check what that imples, what other references would be needed and so on, but yes it's something that I can look into.

So just like Microsoft provides the IDistributedCache abstractions for any of us to create community implementations, you'd provide the abstractions package containing only the IFusionCacheBackplane, and any other required interfaces to implement it end to end 🙂

Question: what would be the advantage of referencing the abstraction package instead of the main one?
Would it be just to reference a smaller package or is there something else I'm missing?

Idk, food for thought?

Fore sure, thanks!

EDIT: Or like aspire, have a CummunityToolkit repository that basically distances it from the main Aspire core resource implementations, but still allows it to be overseen by you but in a more "let other people contribute to it and provide updates, etc" (would be a bit more work from your side to manage said second repo compared to just releasing an abstractions package to the wild but 🤷‍♂ )

A community toolkit repo, this is an interesting idea! Will need to see how to handle the package prefix/namespace, contributions rules, updates propagation but it's definitely an interesting one, let me think about it.

Thanks!

@MiguelGuedelha
Copy link

Hey @jodydonetti

Question: what would be the advantage of referencing the abstraction package instead of the main one?
Would it be just to reference a smaller package or is there something else I'm missing?

That is one factor, yes, importing only the abstractions means that the implementation doesn't come bundled when there is probably no need for it, for the package/library developer. It would also mean that, even if the core library of Fusion Cache gets updated with implementation level updates/fixes, etc, as long as the interfaces stay the same, theoretically, the versions of the abstraction packages wouldn't be bumped/no new versions of the abstractions would need to be released unless a breaking change/new addition is made, etc.

Again, probably easier said than done, it would all depend on the structure of the project, and if the abstractions were to be segregated enough from the rest or not, etc

A community toolkit repo, this is an interesting idea! Will need to see how to handle the package prefix/namespace, contributions rules, updates propagation but it's definitely an interesting one, let me think about it.

Yeah, I thought this might be an interesting approach too! Came across it randomly when searching for certain aspire packages, and saw a comment from David Fowl stating that certain packages certainly deserve to have a place in a, let's say, "trusted"/reputable location that allows people to basically still have confidence that the package adheres to the standards of the core library, but not actively mantained by the core aspire team, etc and instead if more comunnity driven.

Namespace wise, they seem to make it a separate repo, but still be within the same namespace as the regular/core aspire library, see the Deno resource for example: https://github.com/CommunityToolkit/Aspire/blob/main/src/CommunityToolkit.Aspire.Hosting.Deno/DenoAppResource.cs

Main repo of the toolkit: https://github.com/CommunityToolkit/Aspire

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants