Hangfire get last execution time
Asked Answered
G

5

7

I'm using hangfire 1.5.3. In my recurring job I want to call a service that uses the time since the last run. Unfortunately the LastExecution is set to the current time, because the job data was updated before executing the job.

Job

public abstract class RecurringJobBase
{
    protected RecurringJobDto GetJob(string jobId)
    {
        using (var connection = JobStorage.Current.GetConnection())
        {
            return connection.GetRecurringJobs().FirstOrDefault(p => p.Id == jobId);
        }
    }

    protected DateTime GetLastRun(string jobId)
    {
        var job = GetJob(jobId);

        if (job != null && job.LastExecution.HasValue)
        {

            return job.LastExecution.Value.ToLocalTime();
        }

        return DateTime.Today;
    }
}

public class NotifyQueryFilterSubscribersJob : RecurringJobBase
{
    public const string JobId = "NotifyQueryFilterSubscribersJob";
    private readonly IEntityFilterChangeNotificationService _notificationService;

    public NotifyQueryFilterSubscribersJob(IEntityFilterChangeNotificationService notificationService)
    {
        _notificationService = notificationService;
    }

    public void Run()
    {
        var lastRun = GetLastRun(JobId);
        _notificationService.CheckChangesAndSendNotifications(DateTime.Now - lastRun);
    }
}

Register

RecurringJob.AddOrUpdate<NotifyQueryFilterSubscribersJob>(NotifyQueryFilterSubscribersJob.JobId, job => job.Run(), Cron.Minutely, TimeZoneInfo.Local);

I know, that it is configured as minutely, so I could calculate the time roughly. But I'd like to have a configuration independent implementation. So my Question is: How can I implement RecurringJobBase.GetLastRun to return the time of the previous run?

Gittel answered 4/2, 2016 at 15:4 Comment(0)
K
2

To address my comment above, where you might have more than one type of recurring job running but want to check previous states, you can check that the previous job info actually relates to this type of job by the following (although this feels a bit hacky/convoluted).

If you're passing the PerformContext into the job method than you can use this:

var jobName = performContext.BackgroundJob.Job.ToString();
var currentJobId = int.Parse(performContext.BackgroundJob.Id);
JobData jobFoundInfo = null;

using (var connection = JobStorage.Current.GetConnection()) {
    var decrementId = currentJobId;
    while (decrementId > currentJobId - 50 && decrementId > 1) { // try up to 50 jobs previously
        decrementId--;
        var jobInfo = connection.GetJobData(decrementId.ToString());
        if (jobInfo.Job.ToString().Equals(jobName)) { // **THIS IS THE CHECK**
            jobFoundInfo = jobInfo;
            break;
        }
    }
    if (jobFoundInfo == null) {
       throw new Exception($"Could not find the previous run for job with name {jobName}");
    }
    return jobFoundInfo;
}
Koser answered 24/5, 2019 at 21:18 Comment(0)
S
1

You could take advantage of the fact you already stated - "Unfortunately the LastExecution is set to the current time, because the job data was updated before executing the job".

The job includes the "LastJobId" property which seems to be an incremented Id. Hence, you should be able to get the "real" previous job by decrement LastJobId and querying the job data for that Id.

var currentJob = connection.GetRecurringJobs().FirstOrDefault(p => p.Id == CheckForExpiredPasswordsId);

if (currentJob == null)
{
    return null; // Or whatever suits you
}

var previousJob = connection.GetJobData((Convert.ToInt32(currentJob.LastJobId) - 1).ToString());

return previousJob.CreatedAt;

Note that this is the time of creation, not execution. But it might be accurate enough for you. Bear in mind the edge case when it is your first run, hence there will be no previous job.

Sentimentalize answered 27/7, 2017 at 12:51 Comment(1)
This is also a problem if you have multiple different recurring jobs. So the decremented job id is actually the id of a different type of job entirely.Koser
R
0

After digging around, I came up with the following solution.

var lastSucceded = JobStorage.Current.GetMonitoringApi().SucceededJobs(0, 1000).OrderByDescending(j => j.Value.SucceededAt).FirstOrDefault(j => j.Value.Job.Method.Name == "MethodName" && j.Value.Job.Type.FullName == "NameSpace.To.Class.Containing.The.Method").Value;
var lastExec = lastSucceded.SucceededAt?.AddMilliseconds(Convert.ToDouble(-lastSucceded.TotalDuration));

It's not perfect but i think a little cleaner than the other solutions. Hopefully they will implement an official way soon.

Roulette answered 6/10, 2021 at 8:1 Comment(0)
K
0

The answer by @Marius Steinbach is often good enough but if you have thousands of job executions (my case) loading all of them from DB doesn't seem that great. So finally I decided to write a simple SQL query and use it directly (this is for PostgreSQL storage though changing it to SqlServer should be straightforward):

private async Task<DateTime?> GetLastSuccessfulExecutionTime(string jobType)
{
    await using var conn = new NpgsqlConnection(_connectionString);
    if (conn.State == ConnectionState.Closed)
        conn.Open();

    await using var cmd = new NpgsqlCommand(@"
        SELECT s.data FROM hangfire.job j
        LEFT JOIN hangfire.state s ON j.stateid = s.id 
        WHERE j.invocationdata LIKE $1 AND j.statename = $2
        ORDER BY s.createdat DESC
        LIMIT 1", conn)
    {
        Parameters =
        {
            new() { Value = $"%{jobType}%" } ,
            new() { Value = SucceededState.StateName }
        }
    };

    var result = await cmd.ExecuteScalarAsync();
    if (result is not string data)
        return null;

    var stateData = JsonSerializer.Deserialize<Dictionary<string, string>>(data);

    return JobHelper.DeserializeNullableDateTime(stateData?.GetValueOrDefault("SucceededAt"));
}
Kendy answered 19/1, 2022 at 15:17 Comment(0)
D
0

Use this method that return Last exucution time and Next execution time of one job. this method return last and next execution time of one job.

public static (DateTime?, DateTime?) GetExecutionDateTimes(string jobName)
{
    DateTime? lastExecutionDateTime = null;
    DateTime? nextExecutionDateTime = null;
    using (var connection = JobStorage.Current.GetConnection())
    {
        var job = connection.GetRecurringJobs().FirstOrDefault(p => p.Id == jobName);
        if (job != null && job.LastExecution.HasValue)
            lastExecutionDateTime = job.LastExecution;
        if (job != null && job.NextExecution.HasValue)
            nextExecutionDateTime = job.NextExecution;
    }
    return (lastExecutionDateTime, nextExecutionDateTime);
}
Dix answered 31/12, 2022 at 9:57 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Ramsdell

© 2022 - 2024 — McMap. All rights reserved.