-
Notifications
You must be signed in to change notification settings - Fork 0
Commands
Commands are the heart of Mjolnir and operate with a fail-fast mentality. They apply timeouts, track and react to errors using Circuit Breakers, and avoid resource exhaustion by using Bulkheads.
You wrap your potentially-dangerous code by extending one of two base classes:
-
SyncCommandfor synchronous code -
AsyncCommandfor asynchronous code
Here's a (somewhat contrived) example of a synchronous Command that makes an HTTP call to Amazon Web Services' S3 API to see if a file exists.
class S3FileExistsCommand : SyncCommand<bool>
{
private readonly IS3Client _client;
private readonly string _bucketName;
private readonly string _fileName;
public S3FileExistsCommand(IS3Client client, string bucketName, string fileName)
: base("s3", "s3-read", TimeSpan.FromSeconds(5))
{
_client = client;
// other validation
_bucketName = bucketName;
_fileName = fileName;
}
public override bool Execute(CancellationToken cancellationToken)
{
return _client.FileExists(_bucketName, _fileName);
}
}Here's the equivalent asynchronous implementation.
class S3FileExistsAsyncCommand : AsyncCommand<bool>
{
private readonly IS3AsyncClient _client;
private readonly string _bucketName;
private readonly string _fileName;
public S3FileExistsAsyncCommand(IS3AsyncClient client, string bucketName, string fileName)
: base("s3", "s3-read", TimeSpan.FromSeconds(5))
{
if (client == null) throw new ArgumentNullException("client");
// other validation
_bucketName = bucketName;
_fileName = fileName;
}
public override Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
return _client.FileExistsAsync(_bucketName, _fileName, cancellationToken);
}
}Let's dig into those a bit.
Constructor
The base classes provide two constructors:
base(string group, string isolationKey, TimeSpan defaultTimeout)
// or
base(string group, string breakerKey, string bulkheadKey, TimeSpan defaultTimeout)-
groupis a logical grouping, and doesn't have much bearing on functionality. It's used in Configuration and logging, and mainly helps namespace Commands as an identifier. Most of the time, all Commands in a package or domain will share the same group (in this case,"s3"). -
breakerKeyis how you group sets of Commands together under the same breaker. Commands using the same breaker will use the same error counters and state, and will essentially "fail together" when the Circuit Breaker trips. It makes sense to group Commands that share the same dependencies together. -
bulkheadKeyis similar tobreakerKey, but for Bulkheads. Commands in the same Bulkhead will "fail together" when the Bulkhead is full. -
isolationKeylets you use the same key for both thebreakerKeyandbulkheadKey. This is fairly common. -
defaultTimeoutis a catch-all timeout that's used if a more specific timeout isn't found when the Command is invoked. It's normally superseded by a dynamically-configured timeout or a value passed in at invocation time.
Execute / ExecuteAsync Methods
The Execute method is where your critical code goes. A CancellationToken is provided for cooperative cancellation - it's your responsibility to pass the token down into methods that support it (like HTTP client calls), or to check for cancellation periodically if you're looping.
Commands are run by passing them to an instance of ICommandInvoker. Since the default implementation of CommandInvoker in the Mjolnir library holds a lot of state information, you should create and use it as a singleton throughout an application. Using a dependency injection framework is an ideal way of achieving that.
Callers who need static access should consider creating a static singleton wrapper around a single instance of CommandInvoker.
The Invoke*() methods on the invoker have two variants targeted at how exceptions should be handled: Throw and Return.
- The
Throwvariants will re-throw exceptions that occur during invocation. - The
Returnvariants will catch and wrap Exceptions, preferring to always return a result to the caller (even when errors occur). Since Mjolnir's intent is to introduce fast failure to avoid failure cascading in unpredictable ways, it's useful for callers to handle that failure in ways beyond just re-throwing the exception upward. This could be handled by the caller usingtry/catch, but baking it into the Mjolnir API helps callers realize that they need to think about failure and make conscious decisions about how to handle it.
Examples
Synchronous Command, errors re-thrown.
var invoker = new CommandInvoker()
var command = new S3FileExistsCommand(_client, "static-content", "foo.txt");
var exists = invoker.InvokeThrow(command);Synchronous Command, errors wrapped.
var invoker = new CommandInvoker()
var command = new S3FileExistsCommand(_client, "static-content", "foo.txt");
var result = invoker.InvokeReturn(command);
if (result.WasSuccess)
{
var exists = result.Value;
}
else
{
// Handle error gracefully.
}Asynchronous Command, errors re-thrown.
var invoker = new CommandInvoker()
var command = new S3FileExistsAsyncCommand(_client, "static-content", "foo.txt");
var exists = await invoker.InvokeThrowAsync(command);Asynchronous Command, errors wrapped.
var invoker = new CommandInvoker()
var command = new S3FileExistsAsyncCommand(_client, "static-content", "foo.txt");
var result = await invoker.InvokeReturnAsync(command);
if (result.WasSuccess)
{
var exists = result.Value;
}
else
{
// Handle error gracefully.
}