Dealing with failed jobs and dependencies in job filters


#1

I just started using Hangfire in this ASP.NET Core project at work and it’s working really well. I have a job which can be retried a bunch of times (currently 10 times) and after this I would like to notify third party teams that something is going wrong. E.g. sending a slack message for this I am already have a SlackService which can be injected using Autofac. Only I am struggling to get it injected into my custom JobFilter.

What’s the best practice to inject a service dependency with Hangfire? My current code checks the job parameter for retryCount and when it’s equal to 10 it should do somethings. This all seems to work fine just getting access to my service is difficult. Hangfire doesn’t seem to automatically inject properties when the Hangfire.Autofac extension is enabled. Am I doing something? Are there better approaches to solve this problem besides the code shown below?

using System;
using System.Linq;
using Autofac;
using Hangfire.Client;
using Hangfire.Common;
using Hangfire.Logging;
using Hangfire.Server;
using Hangfire.States;
using Hangfire.Storage;
using DataBlok.Common.Logging;
using DataBlok.Domain.Services;

namespace DataBlok.Domain.Tasks
{

    public interface INotifyWhenDrawdownFailedAttribute: IElectStateFilter, IApplyStateFilter {

    }

    public class NotifyWhenDrawdownFailedAttribute : JobFilterAttribute, INotifyWhenDrawdownFailedAttribute
    {
        private readonly ILoggingService _loggingService;
        private readonly ISlackService _slackService;

        private static readonly ILog Logger = LogProvider.GetCurrentClassLogger();

        public NotifyWhenDrawdownFailedAttribute(ILoggingService loggingService, ISlackService slackService, IContainer container = null)
        {
            _loggingService = container.Resolve<ILoggingService>();
            _slackService = container.Resolve<ISlackService>();
        }

        public void OnCreating(CreatingContext context)
        {
            _loggingService.Information("!!!! Creating a job based on method `{0}`...", context.Job.Method.Name);
        }

        public void OnCreated(CreatedContext context)
        {
            _loggingService.Information(
                "!!!! Job that is based on method `{0}` has been created with id `{1}`",
                context.Job.Method.Name,
                context.BackgroundJob?.Id
            );
        }

        public void OnPerforming(PerformingContext context)
        {
           _loggingService.Information("!!!!  Starting to perform job `{0}`", context.BackgroundJob.Id);
        }

        public void OnPerformed(PerformedContext context)
        {
           _loggingService.Information("!!!! Job `{0}` has been performed", context.BackgroundJob.Id);
        }

        public void OnStateElection(ElectStateContext context)
        {
            var failedState = context.CandidateState as FailedState;
            if (failedState != null)
            {
                var retryAttempt = context.GetJobParameter<int>("RetryCount") + 1;
                var maxAttemptsForDrawdowns = 10;
                if (retryAttempt == maxAttemptsForDrawdowns) {
                    _loggingService.Warning(
                        "!!!! Job `{0}` has been failed due to an exception `{1}` at retry attempt `{2}`",
                        context.BackgroundJob.Id,
                        failedState.Exception,
                        retryAttempt
                    );

                    try {
                        var jobId = "123456788"; // get the parameters passed to the job here
                        var initiatedByUserId = "1"; // TODO extract data from job
                        _slackService.HandleFailedCreation(jobId,initiatedByUserId);
                    } catch(Exception exc) {
                        _loggingService.Error(exc, "Failed to report a failing drawdown");
                    }
                }
            }
        }

        public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
        {
            _loggingService.Information(
                "!!!! Job `{0}` state was changed from `{1}` to `{2}`",
                context.BackgroundJob.Id,
                context.OldStateName,
                context.NewState.Name);
        }

        public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
        {
            _loggingService.Information(
                "!!!! Job `{0}` state `{1}` was unapplied.",
                context.BackgroundJob.Id,
                context.OldStateName);
        }
    }
}

#2

You could use ServiceProvider for this.

Build ServiceProvider at Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
ServiceProvider serviceProvider = services.BuildServiceProvider();

}

And in services.AddHangfire(op =>

  •          ...*
    
  •          op.Usefilter(new NotifyWhenDrawdownFailedAttribute(serviceProvider));*
    

In the attribute class, you access your service like this:

var slackService = (ISlackService)_serviceProvider.GetService(typeof(ISlackService))


#3

Thanks, this gave a good pointer to make it work. Now occasionally having the issue that null exception occurs in the method OnStateElection when I am trying to ignore any calls to this job filter for unrelated jobs