Created
September 22, 2017 16:13
-
-
Save hidegh/36bbae8868e5cf5b37aea70cfa37ed5c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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 | |
)); | |
} | |
} |
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
IApplyStateFilter should be used for final failure!
see: https://docs.hangfire.io/en/latest/tutorials/send-email.html