Skip to content
Damian Turczynski edited this page Oct 24, 2017 · 1 revision

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:

Extending SyncCommand or AsyncCommand

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)
  • group is 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").
  • breakerKey is 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.
  • bulkheadKey is similar to breakerKey, but for Bulkheads. Commands in the same Bulkhead will "fail together" when the Bulkhead is full.
  • isolationKey lets you use the same key for both the breakerKey and bulkheadKey. This is fairly common.
  • defaultTimeout is 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.

Invoking a Command

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 Throw variants will re-throw exceptions that occur during invocation.
  • The Return variants 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 using try/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.
}

« Installing and ConfiguringTimeouts and Cancellation »

Clone this wiki locally