Retry Policies

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 pre-configured 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();
                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();
            }
            catch (Exception e) when (++tries < maxTries && filter(e))
            {
            }
        }
    }
}

snippet source | anchor

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 transient
c.RetryPolicy(ExceptionFilteringRetryPolicy.Twice(e => e is NpgsqlException ne && ne.IsTransient));

snippet source | anchor

Lastly, the filter is configured to retry failing operations twice, given they throw a NpgsqlException that is transient and thus might succeed later.

There's also a built-in DefaultRetryPolicy that has sane defaults for transient error handling. Like any custom policy, you can plug it into into the StoreOptions via the RetryPolicy method:

// Use DefaultRetryPolicy which handles Postgres's transient errors by default with sane defaults
// We retry operations twice if they yield and NpgsqlException that is transient
// Each error will cause sleep of N seconds where N is the current retry number
c.RetryPolicy(DefaultRetryPolicy.Twice());

snippet source | anchor

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