Fork me on GitHub

Retry Policies Edit on GitHub


Marten can be configured to retry failing database operations by implementing an IRetryPolicy. Such policy is plugged into the StoreOptions when the DocumentStore is configured and bootstrapped.

The sample below demonstrates an IRetryPolicy implementation that retries any failing operation preconfigured number of times with an optional predicate on the thrown exception(s).


// Implement IRetryPolicy interface
public sealed class ExceptionFilteringRetryPolicy: IRetryPolicy
{
    private readonly int maxTries;
    private readonly Func<Exception, bool> filter;

    private ExceptionFilteringRetryPolicy(int maxTries, Func<Exception, bool> filter)
    {
        this.maxTries = maxTries;
        this.filter = filter;
    }

    public static IRetryPolicy Once(Func<Exception, bool> filter = null)
    {
        return new ExceptionFilteringRetryPolicy(2, filter ?? (_ => true));
    }

    public static IRetryPolicy Twice(Func<Exception, bool> filter = null)
    {
        return new ExceptionFilteringRetryPolicy(3, filter ?? (_ => true));
    }

    public static IRetryPolicy NTimes(int times, Func<Exception, bool> filter = null)
    {
        return new ExceptionFilteringRetryPolicy(times + 1, filter ?? (_ => true));
    }

    public void Execute(Action operation)
    {
        Try(() => { operation(); return Task.CompletedTask; }, CancellationToken.None).GetAwaiter().GetResult();
    }

    public TResult Execute<TResult>(Func<TResult> operation)
    {
        return Try(() => Task.FromResult(operation()), CancellationToken.None).GetAwaiter().GetResult();
    }

    public Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken)
    {
        return Try(operation, cancellationToken);
    }

    public Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken)
    {
        return Try(operation, cancellationToken);
    }

    private async Task Try(Func<Task> operation, CancellationToken token)
    {
        for (var tries = 0; ; token.ThrowIfCancellationRequested())
        {
            try
            {
                await operation().ConfigureAwait(false);
                return;
            }
            catch (Exception e) when (++tries < maxTries && filter(e))
            {
            }
        }
    }

    private async Task<T> Try<T>(Func<Task<T>> operation, CancellationToken token)
    {
        for (var tries = 0; ; token.ThrowIfCancellationRequested())
        {
            try
            {
                return await operation().ConfigureAwait(false);
            }
            catch (Exception e) when (++tries < maxTries && filter(e))
            {
            }
        }
    }
}


The policy is then plugged into the StoreOptions via the RetryPolicy method:


// Plug in our custom retry policy via StoreOptions
// We retry operations twice if they yield and NpgsqlException that is not transient (for the sake of easier demonstrability)
c.RetryPolicy(ExceptionFilteringRetryPolicy.Twice(e => e is NpgsqlException ne && !ne.IsTransient));

Lastly, the filter is configured to retry failing operations twice, given they throw a NpgsqlException that is non-transient (for the sake of demonstrability).

Also you could use the fantastic Polly library to easily build more resilient and expressive retry policies by implementing IRetryPolicy.