Thanks for raising this interesting question. Based on my understanding, we are looking at two constraints:
- Fairness between tenants
- Ordering of messages within a tenant
Let’s look at those individually for a moment.
When looking at fairness, it becomes apparent when using a single queue with scaled out consumers it is difficult to achieve fairness because a busy tenant might produce more messages into the queue that then lead to the result of other messages having to wait. With that, the critical time of a message for a tenant might fluctuate tremendously depending on how many noisy neighbors are connected to the same queue.
When you look at the message session, it looks like it might be a good fit for achieving fairness, but I wonder if that is really the case based on my experience with sessions. A session processor has to be configured with an appropriate session idle timeout, maximum number of concurrent session and a maximum concurrency per session. The idle timeout sets the maximum amount of time to wait for a message to be received for a currently active session and after that is elapsed, the processor will close the session and attempt to process another session. This means a busy session id/group can end up being continuously processed, and you would need to fine tune a lot in alignment with your tenants to achieve “fairness” even with sessions.
The message session feature in Azure Service Bus in my opinion, is designed to achieve ordering within a session and not “fairness”. So you’d try to use a feature that might not be “fit for purpose” which can lead to other issues down the line.
When it comes to strict ordering, we have found many ordering cases don’t actually need ordering when looking at those cases closer. Dennis and David wrote an excellent blog post about this with the title “You don’t need ordered delivery”. Usually, when looking at “those ordering cases” closer and modeling those like you mentioned with sagas we get better tradeoffs than relying on technical solutions like message sessions. It is true though that not all use cases are the same and the message session has its value when ordering is really required. As such, it might be nice to provide an API for our users to make it supported. Since with Azure Service Bus when ordering is required, message sessions are the recommended approach.
Sessions in general
When you look at the session feature, it becomes clear that enabling sessions has consequences for the producer of the messages as well as the consumer.
When sessions are enabled on a queue or a subscription, the client applications can no longer send/receive regular messages. All messages must be sent as part of a session (by setting the session id) and received by accepting the session.
This means there is tighter coupling between the producer and the consumer. Which for me would mean I would only ever want to apply this pattern for producer and consumers within the same service boundary (which for me would also indicate only for commands). Since commands only involve a certain coupling by definition (by knowing where to send the intent to) I do not think it is necessarily more involved to send the command to the tenant specific queue/endpoint once you ask yourself the question of multi-tenancy vs multi-single tenancy.
Multi tenancy vs multi-single tenancy
When we look at the problem of fairness, I wonder whether this is a question of choosing the “right” tenancy structure. Gregor Hohpe wrote an excellent blog post about this on LinkedIn. In there, he talks about Multi-single-tenancy (“efficient single family homes”). With such an approach, you would have tenant-specific queues. With such an approach, you gain numerous benefits like:
- Dedicated resources per tenant that can be right sized depending on the “tier” of the customer. For example, compute and memory, as well as concurrency, can be controlled by the tenant tier.
- Metrics, telemetry etc. are clearly separated by tenant, which gives you greater insights of what is going on within a single tenant than what you can get with sessions.
- It is possible to do tenant specific scaling of consumers. With a single queue, you can only do competing consumer on the resources of that queue.
- Quotas of the queue infrastructure can be aligned with the allowed quotas of the tier and closely monitored per tenant.
- Onboarding and offboarding tenants can be fully automated with the infrastructure deployment. When a tenant is offboarded, all the tenant-specific data can either be removed or it is clear how long the infrastructure needs to be kept around until all the tenant-specific data is processed because you get the necessary queue statistics to even answer this question.
I’m aware that there are operational overheads in terms of compute, storage and memory compared to multi-tenancy, but these are tradeoffs that might be worthwhile exploring further given the benefits you get with multi-single-tenancy.