Introduction
"High availability, is a characteristic of a system which aims to ensure an agreed level of operational performance, usually uptime, for a higher than normal period." - Wikipedia
Regardless of the software architecture, high levels of availability is the ultimate goal of moving to the cloud. The idea is to make your product available to your customers at any time.
Servers have a limited amount of resources under disposal. Storage resources are quite cheap these days, but compute resources are precious, and we should aim to squeeze every bit of power from them.
When you offer a SaaS solution, you do that in the form of a web service, hosted somewhere in the cloud. If the traffic to your application is low, you will probably not have much problems to serve all your clients, as these modern web applications can handle a substantial amount of concurrent user connections. Each request that hits your server is handle from a thread picked up from the thread pool.
Once the traffic grows and the server has to handle all those requests, the thread pool shrinks in the sense that, more threads are in use, and there are less of them under disposal to handle new requests. We want to avoid this!
When the thread pool gets exhausted, it is reflected to the outside world as an application that is unreachable from clients. The faster we can handle a request and return the appropriate response, the better for our application's availability.
Even though we like to think that using async/await
makes our application asynchronous, it is actually fake asynchronous behaviour, in the sense that the application is not in a blocking state but the response can not return to the caller, until everything has finished.
Messaging makes true asynchronous behaviour possible. Although messaging is fundamentally a pragmatic reaction to the problems of distributed systems. As a technique, it is not restricted to distributed systems. We can leverage messaging in monolithic applications too.
Messaging used in a Monolith
The idea is to leverage messaging to reduce the response time of HTTP requests. This has direct impact on a monolithic application's availability.
When HTTP requests hits the server, messages get published to the message broker. The responses are returned "immediately". The monolith afterwards consumes these messages, and actually does the work that was supposed to be done, triggered by the respective request.
We can see here that the monolith is acting as the publisher & subscriber.
Use case
Imagine you are building an appliction that offers a way to store files in some kind of hierarchical structure of folders. It also supports folder nesting. Files can get large, and we do not want to store those in our RDBMS. But instead store only the path to the actual file, which in-turn live somewhere else. Maybe in our file system, blob storage, or even a NoSQL database. The point is, it is stored somewhere cheaper 😉.
What happens if a user deletes one of the folders?
Well, we need to make sure that we:
- Delete that folder in our database.
- Delete all file paths that belong in that folder.
- Delete all the actual files that correspond to the above step.
So far so good, but imagine if that folder had hundreds or thousands of files. Even worse what if the folder had many other folders within it, and those folders had other folders, each with hundreds or thousands of files. This would require a lot of IO calls, wether it be file system calls or network calls.
You could apply (if applicable) some batching techniques to take care of the number of calls. But nonetheless, the bigger the batch is, the longer it takes to complete that request.
With async/await
you can make sure that the UI does not hang, but the HTTP response won't get started untill everything has completed. This impacts availability of our application!
Messaging to the rescue
With messaging, once the HTTP request hits our server, we can publish a message (a command to be more precise) to the message broker, which would contain only the ID of the folder that we want to delete.
I should premise that the publishing of the message to the broker, should be done with at-least-once delivery kept in-mind.
The moment the message gets stored in the Outbox table and the transaction has been commited, we can start the HTTP response. Here the monolith is the publisher, but we already mentioned that the monolith is also the subscriber of that message. That means, somewhere in our codebase we have a handler for that message, which triggers the actual work that previously was supposed to be done, while the HTTP request was running.
Needless to say that we do this work in a background thread.
Example
Below we can see a JSON representation of a nested folder hierarchy with files and their corresponding paths.
The files are housed in our local file system. The hierachy has been flattend in the file system. This is actually quite common in a lot of blob storage service offerings from various cloud providers.
Let's have a look at the DeleteFolderCommand
.
public class DeleteFolderCommand : IRequest<Unit>
{
public Guid FolderId { get; set; }
}
public class DeleteFolderCommandHandler :
IRequestHandler<DeleteFolderCommand, Unit>
{
private readonly FileStore fileStore;
private readonly SqlConnectionFactory factory;
private readonly ApplicationDbContext context;
public DeleteFolderCommandHandler(
FileStore fileStore,
SqlConnectionFactory factory,
ApplicationDbContext context)
{
this.fileStore = fileStore;
this.factory = factory;
this.context = context;
}
public async Task<Unit> Handle(DeleteFolderCommand request,
CancellationToken cancellationToken)
{
var folderDtos = await LoadFlattenHierarchy(request.FolderId);
var paths = folderDtos
.SelectMany(ff => ff.Files
.Select(f => f.FileName));
await fileStore.Remove(paths);
var folders = context.Folders
.Where(f => folderDtos
.Select(f => f.Id).Contains(f.Id));
context.Folders.RemoveRange(folders);
await context.SaveChangesAsync();
return Unit.Value;
}
}
- We get the folder id that the user wants to delete.
- Inject dependencies like
FileStore
,SqlConnectionFactory
, andApplicationDbContext
. - Load a flattened representation of the folder hierarchy.
- Select all the paths of the files.
- Delete those files from the file store.
- Remove all the folders from our instance of
ApplicationDbContext
. - Save changes to the context.
DeleteFolderCommand
is the command that does the actual work. But we do not want this to be callable from the API endpoint. Instead we want the InitDeleteFolderCommand
to be called.
public class InitDeleteFolderCommand : IRequest<Unit>
{
public Guid FolderId { get; set; }
}
public class InitDeleteFolderCommandHandler :
IRequestHandler<InitDeleteFolderCommand, Unit>
{
private readonly ICapPublisher capPublisher;
public InitDeleteFolderCommandHandler(
ICapPublisher capPublisher)
{
this.capPublisher = capPublisher;
}
public async Task<Unit> Handle(InitDeleteFolderCommand request,
CancellationToken cancellationToken)
{
await capPublisher.PublishAsync("delete.folder", request.FolderId);
return Unit.Value;
}
}
We are using CAP as our library that implements the Outbox Pattern, and the neccessary communications to our message broker (RabbitMQ). But don't let that distract you! At the end, it is just an abstraction of a publishing mechanism to a message bus.
- We get the folder id that the user wants to delete.
- Inject our message bus publisher abstraction
ICapPublisher
. - Publish a message named "
delete.folder
" with the folder id as the payload.
After the command execution has finished, the HTTP reponse gets send to the caller. Once the message is received by the monolith, we internally kick-off the DeleteFolderCommand
to do the actual work.
public class InitDeleteFolderHandler : ICapSubscribe
{
private readonly ISender sender;
public InitDeleteFolderHandler(ISender sender)
{
this.sender = sender;
}
[CapSubscribe("delete.folder")]
public async Task Handle(Guid folderId)
{
await sender.Send(new DeleteFolderCommand()
{
FolderId = folderId
});
}
}
Notice ⚠️
We could have also written the code in DeleteFolderCommand
within InitDeleteFolderHandler
. But if you have a look at the source code you can see that, starting a command also starts a database transaction from the transaction pipeline.
Comparing execution times
Below we can see the total execution time for InitDeleteFolderCommand
. It only took 79 [ms] to complete the command, as opposed to the 12,805 [ms] that it took to complete DeleteFolderCommand
.
I should put emphasis that the HTTP response has been sent to the caller, after InitDeleteFolderCommand
has finished. We can see that DeleteFolderCommand
has started on 16:41:45
which is 8 [s] after the first command has finished on 16:41:37
. During that time, the message "delete.folder
" has been stored in the Outbox
table, and published from an outbox processor to RabbitMQ.
Which one of the commands would you like the client to invoke, and which one to run in the background?
Obviously we want the client to call InitDeleteFolderCommand
and run DeleteFolderCommand
in the background for reasons we elaborated on.
Summary
In this article, we have gone through the process of increasing a monolithic application's availability by leveraging messaging techniques, which are commonly used in distributed systems.
If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available on GitHub.