Skip to content

Instantly share code, notes, and snippets.

@hidegh
Created September 22, 2017 16:13
Show Gist options
  • Save hidegh/36bbae8868e5cf5b37aea70cfa37ed5c to your computer and use it in GitHub Desktop.
Save hidegh/36bbae8868e5cf5b37aea70cfa37ed5c to your computer and use it in GitHub Desktop.
/// <summary>
/// Depends on domain events...
/// Reason: wanted to de-couple it from the next job and so I do have DI objects accessible...
///
/// NOTE:
/// This is by default a PERMANENT failure notification filter, since the filter Order is so high, it get's surely executed after the AutomaticRetry filter.
/// Also if AutomaticRetry filter does delete the job on permanent fail, the PERMANENT failure notification won't occure.
///
/// If the HangfirePermanentFailureNotificationEvent.Order is less than the AutomaticRetry.Order, we will get a notification on each failure, not just the last one!
/// </summary>
public class HangfirePermanentFailureNofitifactionAttribute : JobFilterAttribute, IElectStateFilter
{
public HangfirePermanentFailureNofitifactionAttribute()
{
Order = int.MaxValue;
}
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;
DomainEvents.Raise(new HangfirePermanentFailureNotificationEvent(
jobId: context.BackgroundJob?.Id,
jobCreatedAt: context.BackgroundJob?.CreatedAt,
jobMethod: context.BackgroundJob?.Job?.Method,
jobArgs: context.BackgroundJob?.Job?.Args,
failedAt: failedState.FailedAt,
stateName: failedState.Name,
retryAttempt: retryAttempt,
isFinal: failedState.IsFinal,
reason: failedState.Reason,
exception: failedState.Exception
));
}
}
@hidegh
Copy link
Author

hidegh commented Nov 14, 2021

IApplyStateFilter should be used for final failure!
see: https://docs.hangfire.io/en/latest/tutorials/send-email.html

@hidegh
Copy link
Author

hidegh commented Sep 15, 2024

using ...
using ...
using Hangfire.Common;
using Hangfire.States;
using Hangfire.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ...

/// <summary>
/// This filter is applied to Hangfire jobs to execute custom actions when a job permanently fails.
/// Based on: https://docs.hangfire.io/en/latest/tutorials/send-email.html#id5
/// 
/// Example Usage:
/// 
///   In your Hangfire configuration or startup class:
///   
///   services.AddTransient<IHangfirePermanentFailureAction, HangfirePermanentFailureEmailAction>();
///   services.AddSingleton<HangfirePermanentFailureActionFilter>();
/// 
///   GlobalJobFilters.Filters.Add(services.BuildServiceProvider().GetRequiredService<HangfirePermanentFailureActionFilter>());
///   
/// Or:
/// 
///   services
///     .AddHangfire((provider, configuration) => 
///       configuration
///         .UseConsole()
///         .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
///         .UseSimpleAssemblyNameTypeSerializer()
///         .UseRecommendedSerializerSettings()
///         /* .UseDefaultActivator() */
///         .UseFilter(new HangfireStateChangeActionFilter(provider))
///         .UseSqlServerStorage(
///           connectionString,
///           new SqlServerStorageOptions
///           {
///             CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
///             SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
///             QueuePollInterval = TimeSpan.Zero,
///             UseRecommendedIsolationLevel = true,
///             DisableGlobalLocks = true
///           }
///         )
///        )
///      ;
/// </summary>
public class HangfireStateChangeActionFilter : JobFilterAttribute, IApplyStateFilter
{
    private readonly IServiceProvider serviceProvider;
    private readonly ILogger<HangfireStateChangeActionFilter> logger;

    public HangfireStateChangeActionFilter(IServiceProvider serviceProvider)
    {
        var logger = serviceProvider.GetService<ILogger<HangfireStateChangeActionFilter>>();
        if (logger == null)
            throw new ExceptionEx($"Could not get {nameof(ILogger<HangfireStateChangeActionFilter>)} instance!");

        this.serviceProvider = serviceProvider;
        this.logger = logger;
    }

    public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
    {
        // check edge case...
        var job = (Job?)context.BackgroundJob?.Job;
        if (job == null)
        {
            return;
        }

        // handle usual route...
        var dto = new HangfireStateChangeDetails
        {
            Id = context.BackgroundJob!.Id,

            Type = context.BackgroundJob.Job.Type,
            Method = context.BackgroundJob.Job.Method,
            Args = context.BackgroundJob.Job.Args,
            Queue = context.BackgroundJob.Job.Queue,

            CreatedAt = context.BackgroundJob.CreatedAt,
            FailedAt = null,
            ExceptionMessage = null
        };

        var errors = new List<(IHangfireStateChangeHandler Handler, Exception Exception)>();

        var handlers = serviceProvider.GetServices<IHangfireStateChangeHandler>();

        foreach (var handler in handlers)
        {

            try
            {

                // check if we should handle it!
                if (handler.CanHandle(dto) == false)
                    continue;

                // check state and call the corresponding method...
                if (context.NewState is ProcessingState)
                {
                    handler.OnProcessing(dto);
                }
                else if (context.NewState is SucceededState)
                {
                    handler.OnSuccess(dto);
                }
                else if (context.NewState is ScheduledState || context.NewState is EnqueuedState)
                {
                    var retryCount = context.GetJobParameter<int>("RetryCount");
                    var previouslyFailed = retryCount > 0;

                    if (previouslyFailed)
                    {
                        handler.OnRetryingFailure(dto);
                    }
                    else
                    {
                        // hangfire is just preparing 1st run...
                    }
                }
                else if (context.NewState is FailedState || context.NewState is DeletedState)
                {
                    var ds = context.NewState as DeletedState;
                    var fs = context.NewState as FailedState;

                    dto.FailedAt = ds?.DeletedAt ?? fs?.FailedAt;
                    dto.ExceptionMessage = ds?.ExceptionInfo?.Message ?? fs?.Exception?.Message;

                    handler.OnPermanentFailure(dto);
                }

            }
            catch (Exception ex)
            {
                errors.Add((handler, ex));   
            }

        }

        if (errors.Count > 0)
        {
            errors.ForEach(e =>
            {
                var handler = e.Handler;
                var ex = e.Exception;
                var message = $"Exception occurred when executing one of '{handler.GetType().Name}' methods inside '{nameof(HangfireStateChangeActionFilter)}'.";
                logger.LogCritical(ex, message);
            });            
        }

    }

    public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
    {
        // Intentionally left empty.
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment