Error Handling Edit on GitHub


The sad truth is that JasperBus will not unfrequently hit exceptions as it processes messages. In all cases, JasperBus will first log the exception using the the IBusLogger sinks. After that, it walks through the configured error handling policies to determine what to do next with the message. In the absence of any confugured error handling policies, JasperBus will move any message that causes an exception into the error queue for the transport that the message arrived from on the first attempt.

Today, JasperBus has the ability to:

  • Enforce a maximum number of attempts to short circuit retries, with the default number being 1
  • Selectively apply remediation actions for a given type of Exception
  • Choose to re-execute the message immediately
  • Choose to re-execute the message later
  • Just bail out and move the message out to the error queues
  • Apply error handling policies globally, by configured policies, or explicitly by chain

Configuring Global Error Handling Rules

To establish global error handling policies that apply to all message types, use the JasperBusRegistry.ErrorHandling syntax as shown below:


public class GlobalRetryApp : JasperRegistry
{
    public GlobalRetryApp()
    {
        Messaging.ErrorHandling
            .OnException<TimeoutException>()
            .RetryLater(5.Seconds());

        Messaging.ErrorHandling
            .OnException<SecurityException>()
            .MoveToErrorQueue();
    }
}

In all cases, the global error handling is executed after any message type specific error handling.

Explicit Chain Configuration

To configure specific error handling polices for a certain message (or closely related messages), you can either use some in the box attributes on the message handler methods as shown below:


public class AttributeUsingHandler
{
    [RetryLaterOn(typeof(IOException), 5)]
    [RetryOn(typeof(SqlException))]
    [RequeueOn(typeof(InvalidOperationException))]
    [MoveToErrorQueueOn(typeof(DivideByZeroException))]
    [MaximumAttempts(2)]
    public void Handle(InvoiceCreated created)
    {
        // handle the invoice created message
    }
}

If you prefer -- or have a use case that isn't supported by the attributes, you can take advantage of JasperBus's Configure(HandlerChain) convention to do it programmatically. To opt into this, add a static method with the signature public static void Configure(HandlerChain) to your handler class as shown below:


public class MyErrorCausingHandler
{
    public static void Configure(HandlerChain chain)
    {
        chain.OnException<IOException>()
            .Requeue();

        chain.MaximumAttempts = 3;
    }


    public void Handle(InvoiceCreated created)
    {
        // handle the invoice created message
    }

    public void Handle(InvoiceApproved approved)
    {
        // handle the invoice approved message
    }
}

Do note that if a message handler class handles multiple message types, this method is applied to each message type chain separately.

Configuring through Policies

If you want to apply error handling to chains via some kind of policy, you can use an IHandlerPolicy like the one shown below:


public class ErrorHandlingPolicy : IHandlerPolicy
{
    public void Apply(HandlerGraph graph)
    {
        var matchingChains = graph.Chains.Where(x => x.MessageType.IsInNamespace("MyApp.Messages"));

        foreach (var chain in matchingChains)
        {
            chain.MaximumAttempts = 2;
            chain.OnException<SqlException>()
                .Requeue();
        }
    }
}

To apply this policy, use this syntax in your JasperBusRegistry:


public class MyApp : JasperRegistry
{
    public MyApp()
    {
        Messaging.Policies.Global<ErrorHandlingPolicy>();
    }
}

Filtering on Exceptions

To selectively respond to a certain exception type, you have these two built in methods to configure the error handling filtering:


public class FilteredApp : JasperRegistry
{
    public FilteredApp()
    {
        Messaging.ErrorHandling.OnException<SqlException>().Requeue();

        Messaging.ErrorHandling.OnException(typeof(InvalidOperationException)).Retry();
    }
}

Built in Error Handling Actions

Delayed message processing is not yet supported in Jasper.

The most common exception handling actions are shown below:


public class ContinuationTypes : JasperRegistry
{
    public ContinuationTypes()
    {
        // Try to execute the message again without going
        // back through the queue
        Messaging.ErrorHandling.OnException<SqlException>().Retry();

        // Retry the message again, but wait for the specified time
        Messaging.ErrorHandling.OnException<SqlException>().RetryLater(3.Seconds());

        // Put the message back into the queue where it will be
        // attempted again
        Messaging.ErrorHandling.OnException<SqlException>().Requeue();

        // Move the message into the error queue for this transport
        Messaging.ErrorHandling.OnException<SqlException>().MoveToErrorQueue();
    }
}

TODO -- what do we do to listen to the error queue? Syntactical sugar in the JasperBusRegistry to listen for error reports?

Raise Other Messages

You can also choose to send additional messages as a result of the exception. In this case, you can use the original message and the Envelope metadata plus the actual Exception to determine the message(s) to send out. Use this capability if you want to notify senders when a message fails.


public class RespondWithMessages : JasperRegistry
{
    public RespondWithMessages()
    {
        Messaging.ErrorHandling.OnException<SecurityException>()
            .RespondWithMessage((ex, envelope) =>
            {
                return new FailedOnSecurity(ex.Message);
            });
    }
}

** TODO -- talk about how to send it back to the original sender **

Custom Error Handlers

If the built in recipes don't cover your exception handling needs, all isn't lost. You can bypass the helpers and write your own class that implements the IErrorPolicy interface shown below:


public interface IErrorHandler
{
    IContinuation DetermineContinuation(Envelope envelope, Exception ex);
}

Your error handler needs to be able to look at an Exception and Envelope, then determine and return an IContinuation object that will be executed against the current message. That interface is shown below:


public interface IContinuation
{
    Task Execute(Envelope envelope, IEnvelopeContext context, DateTime utcNow);
}

Here's an example of a custom error handler:


public class CustomErrorHandler : IErrorHandler
{
    public IContinuation DetermineContinuation(Envelope envelope, Exception ex)
    {
        if (ex.Message.Contains("timed out"))
        {
            return new DelayedRetryContinuation(3.Seconds());
        }

        // If the handler doesn't apply to the exception,
        // return null to tell Jasper to try the next error handler (if any)
        return null;
    }
}

To register this custom error handler with your application, just add it to the ErrorHandlers collection:


public class CustomErrorHandlingApp : JasperRegistry
{
    public CustomErrorHandlingApp()
    {
        // TODO -- make a nicer syntax for this
        Messaging.ErrorHandling.ErrorHandlers.Add(new CustomErrorHandler());
    }
}

Note that you can apply custom error handlers either globally or by message type.

TODO -- link to the docs on using IEnvelopeContext & Envelope, when they actually exist;)