Automatic retries - how to manually configure the time between retries

Requirement
I want a custom retry period between job retries

Implementation
This is how I accomplished it, Hangfire comes with a default retry filter. I remove the default filter on startup and replace it with my own custom class.

Here is the startup code to remove the default filter.

public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            // create properties
            NameValueCollection properties = new NameValueCollection();

            properties["level"] = "ALL";

            // set Adapter
            Common.Logging.LogManager.Adapter = new Common.Logging.Simple.TraceLoggerFactoryAdapter(properties);

            System.Diagnostics.Trace.TraceError("Application started");

            app.UseHangfire(config =>
            {
                config.UseAuthorizationFilters();
                config.UseServer();
            });


            #region Hangfire custom retry

            /*
             *Hangfire comes with a retry policy that is automatically set to 10 retry and backs off over several mins
             *We in the following remove this attribute and add our own custom one which adds significant backoff time
             *custom logic to determine how much to back off and what to to in the case of fails
             *
             */

            // The trick here is we can't just remove the filter as you'd expect using remove
            // we first have to find it then save the Instance then remove it 
            object automaticRetryAttribute = null;

            System.Diagnostics.Trace.TraceError("Search hangfire automatic retry");
            foreach (var filter in GlobalJobFilters.Filters)
            {
                if (filter.Instance is Hangfire.AutomaticRetryAttribute)
                {
                    // found it
                    automaticRetryAttribute = filter.Instance;
                    System.Diagnostics.Trace.TraceError("Found hangfire automatic retry");
                    
                }
                
            }
            System.Diagnostics.Trace.TraceError("Not found hangfire unless previous log says found");
            // ok now let's remove it
            if (automaticRetryAttribute == null)
            {
                throw new System.Exception("Didn't find hangfire automaticRetryAttribute something very wrong");

            }

            System.Diagnostics.Trace.TraceError("remove hangefire automaticRetryAttribute");
            GlobalJobFilters.Filters.Remove(automaticRetryAttribute);

            GlobalJobFilters.Filters.Add(new HangFireCustomAutoRetryJobFilterAttribute());


            #endregion
        }

Here is the custom filter class. Ive left a hook in so you can put custom logic there.
HangFireCustomAutoRetryJobFilterAttribute

 public class HangFireCustomAutoRetryJobFilterAttribute : JobFilterAttribute, IElectStateFilter
    {
        private static readonly ILog Logger = LogManager.GetCurrentClassLogger();
        private const int DefaultRetryAttempts = 10;
        private int _attempts;
        public HangFireCustomAutoRetryJobFilterAttribute()
        {
            Attempts = DefaultRetryAttempts;
            LogEvents = true;
            OnAttemptsExceeded = AttemptsExceededAction.Fail;
        }
        public int Attempts
        {
            get { return _attempts; }
            set
            {
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException("value", "Attempts value must be equal or greater than zero.");
                }
                _attempts = value;
            }
        }
        public AttemptsExceededAction OnAttemptsExceeded { get; set; }
        public bool LogEvents { get; set; }
        public void OnStateElection(ElectStateContext context)
        {
            var failedState = context.CandidateState as FailedState;
            if (failedState == null)
            {
                // This filter accepts only failed job state.
                return;
            }
            var retryAttempt = context.GetJobParameter<int>("RetryCount") + 1;
            

            if (retryAttempt <= Attempts)
            {
                switch (context.Job.Method.Name)
                {
                    case <Use your method name or an Interface to do you custom logic here>:
                        break;
                }               
                ScheduleAgainLater(context, retryAttempt, failedState);
            }
            else if (retryAttempt > Attempts && OnAttemptsExceeded == AttemptsExceededAction.Delete)
            {
                TransitionToDeleted(context, failedState);
            }
            else
            {
                if (LogEvents)
                {
                    Logger.ErrorFormat(
                    "Failed to process the job '{0}': an exception occurred.",
                    failedState.Exception,
                    context.JobId);
                }
            }
        }
        /// <summary>
        /// Schedules the job to run again later. See <see cref="SecondsToDelay"/>.
        /// </summary>
        /// <param name="context">The state context.</param>
        /// <param name="retryAttempt">The count of retry attempts made so far.</param>
        /// <param name="failedState">Object which contains details about the current failed state.</param>
        private void ScheduleAgainLater(ElectStateContext context, int retryAttempt, FailedState failedState)
        {
            var delay = TimeSpan.FromSeconds(SecondsToDelay(retryAttempt));
            context.SetJobParameter("RetryCount", retryAttempt);
            // If attempt number is less than max attempts, we should
            // schedule the job to run again later.
            context.CandidateState = new ScheduledState(delay)
            {
                Reason = String.Format("Retry attempt {0} of {1}", retryAttempt, Attempts)
            };
            if (LogEvents)
            {
                Logger.ErrorFormat(
                "Failed to process the job '{0}': an exception occurred. Retry attempt {1} of {2} will be performed in {3}.",
                failedState.Exception,
                context.JobId,
                retryAttempt,
                Attempts,
                delay);
            }
        }
        /// <summary>
        /// Transition the candidate state to the deleted state.
        /// </summary>
        /// <param name="context">The state context.</param>
        /// <param name="failedState">Object which contains details about the current failed state.</param>
        private void TransitionToDeleted(ElectStateContext context, FailedState failedState)
        {
            context.CandidateState = new DeletedState
            {
                Reason = string.Format("Automatic deletion after retry count exceeded {0}", Attempts)
            };
            if (LogEvents)
            {
                Logger.ErrorFormat(
                "Failed to process the job '{0}': an exception occured. Job was automatically deleted because the retry attempt count exceeded {1}",
                failedState.Exception,
                context.JobId,
                Attempts);
            }
        }
        // delayed_job uses the same basic formula
        private static int SecondsToDelay(long retryCount)
        {
            var random = new Random();
            double pow = 4;
            if (retryCount > 5)
            {
                switch (retryCount)
                {
                    case 6:
                        retryCount = 8;
                        break;
                    case 7:
                        retryCount = 9;
                        break;
                    case 8:
                        retryCount = 11;
                        break;
                    case 9:
                        retryCount = 12; // 
                        break;
                    case 10:
                        retryCount = 19; // 36 hrs
                        break;


                }
                retryCount = retryCount + retryCount;
            }


            return (int)Math.Round(
            Math.Pow(retryCount - 1, pow) + 15 + (random.Next(30) * (retryCount)));
        }

@Ashley, can you format the code using so-called fences blocks – the interpreter in my brain can’t parse it :frowning:

```csharp
var a = new B();
```

Any plans on adding something like this to the AutomaticRetry attribute shipped with Hangfire?
It would be great to be able to specify a fixed time interval between retries and/or a starting time interval on which to base the incrementing time between retries.

+1 this would realy be nice.

I’ve implemented a very small change that adds the ability to provide a TimeSpan retry interval with the AutomaticRetryAttribute.

I’d like to share that as a pull request but first I want to make sure I’m not breaking anything else. @odinserj can you guide me on how to run all the tests? Thanks.

I believe Build.bat in the root of the project runs all of the tests.

+1 for this feature. Would be quite nice to have a greater control of how the retry interval is calculated given the retry count.

+1 for this feature!

Guys, was this implemented?

So will this feature be implemented?

+1! This will be very useful

Looks like custom delay logic was added in PR 931.