Skip to content

Instantly share code, notes, and snippets.

@odinserj
Last active May 3, 2024 17:03
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save odinserj/a6ad7ba6686076c9b9b2e03fcf6bf74e to your computer and use it in GitHub Desktop.
Save odinserj/a6ad7ba6686076c9b9b2e03fcf6bf74e to your computer and use it in GitHub Desktop.
SkipWhenPreviousJobIsRunningAttribute.cs
// Zero-Clause BSD (more permissive than MIT, doesn't require copyright notice)
//
// Permission to use, copy, modify, and/or distribute this software for any purpose
// with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
// THIS SOFTWARE.
using System;
using System.Collections.Generic;
using Hangfire.Client;
using Hangfire.Common;
using Hangfire.States;
using Hangfire.Storage;
namespace ConsoleApp28
{
public class SkipWhenPreviousJobIsRunningAttribute : JobFilterAttribute, IClientFilter, IApplyStateFilter
{
public void OnCreating(CreatingContext context)
{
var connection = context.Connection as JobStorageConnection;
// We can't handle old storages
if (connection == null) return;
// We should run this filter only for background jobs based on
// recurring ones
if (!context.Parameters.ContainsKey("RecurringJobId")) return;
var recurringJobId = context.Parameters["RecurringJobId"] as string;
// RecurringJobId is malformed. This should not happen, but anyway.
if (String.IsNullOrWhiteSpace(recurringJobId)) return;
var running = connection.GetValueFromHash($"recurring-job:{recurringJobId}", "Running");
if ("yes".Equals(running, StringComparison.OrdinalIgnoreCase))
{
context.Canceled = true;
}
}
public void OnCreated(CreatedContext filterContext)
{
}
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
if (context.NewState is EnqueuedState)
{
var recurringJobId = JobHelper.FromJson<string>(context.Connection.GetJobParameter(context.BackgroundJob.Id, "RecurringJobId"));
if (String.IsNullOrWhiteSpace(recurringJobId)) return;
transaction.SetRangeInHash(
$"recurring-job:{recurringJobId}",
new[] { new KeyValuePair<string, string>("Running", "yes") });
}
else if ((context.NewState.IsFinal && !FailedState.StateName.Equals(context.OldStateName, StringComparison.OrdinalIgnoreCase)) ||
(context.NewState is FailedState))
{
var recurringJobId = JobHelper.FromJson<string>(context.Connection.GetJobParameter(context.BackgroundJob.Id, "RecurringJobId"));
if (String.IsNullOrWhiteSpace(recurringJobId)) return;
transaction.SetRangeInHash(
$"recurring-job:{recurringJobId}",
new []{ new KeyValuePair<string, string>("Running", "no") });
}
}
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction)
{
}
}
}
@candidodmv
Copy link

Thanks for sharing!
Please update lines with deprecated methods:
https://gist.github.com/odinserj/a6ad7ba6686076c9b9b2e03fcf6bf74e#file-skipwhenpreviousjobisrunningattribute-cs-L52
https://gist.github.com/odinserj/a6ad7ba6686076c9b9b2e03fcf6bf74e#file-skipwhenpreviousjobisrunningattribute-cs-L43

To new ones:
var recurringJobId = SerializationHelper.Deserialize(context.Connection.GetJobParameter(context.BackgroundJob.Id, "RecurringJobId"));

@kashyapus
Copy link

https://gist.github.com/odinserj/a6ad7ba6686076c9b9b2e03fcf6bf74e#file-skipwhenpreviousjobisrunningattribute-cs-L50 is also invalid now that IsFinal is false for FailedState unless there is bug in the way it is coded.

@fairking
Copy link

fairking commented Oct 1, 2021

I have found a bug. When my job failed, it never runs again. The bug in line 28: var running = connection.GetValueFromHash($"recurring-job:{recurringJobId}", "Running"); it always returns yes. If I go to the queue, there are no running job but failed.

P.S. My recurringJobId is always the same for every new job.

Does anyone know how to fix it?

@fairking
Copy link

fairking commented Oct 1, 2021

Ok, I found a solution.
The statement in line 50 needs to be uncommented:

else if (context.NewState.IsFinal || context.NewState is FailedState)

@Narian-Naidoo
Copy link

How can I use this on background enqueued jobs? This SkipWhenPreviousJobIsRunningAttribute seems specific to recurring jobs, I would like the current event driven job to complete before the next one can start even though it has been triggered.

@alexandis
Copy link

I would like to hear the solution for a background job too! Now I have bumped into the issue that in 30 mins, while background job is still running, the same job is being run again! And I cannot see any setting with this "magic" 30 minutes...

@csrowell
Copy link

It looks like you have to buy a subscription to get the Hangfire.Ace extensibility set to use the Hangfire.Throttling package. See Concurrency & Rate Limiting.

@simeyla
Copy link

simeyla commented Jun 26, 2023

How can I use this on background enqueued jobs? This SkipWhenPreviousJobIsRunningAttribute seems specific to recurring jobs, I would like the current event driven job to complete before the next one can start even though it has been triggered.

You could make a named queue with a size of 1.

app.UseHangfireServer(new BackgroundJobServerOptions
{
    WorkerCount = 1, 
    Queues = new[] { "queueName" }
});

Then apply [Queue("queueName")] to your job.

CAUTION: If you have two servers running each will start a queue can process a maximum of one job. So if you just want one per server then this will work. If you really cannot have two at a time for some business reason then this won't work with multiple servers.

@co-dax
Copy link

co-dax commented Sep 18, 2023

@simeyla good idea. I am just not clear on what you intended to say with you "CAUTION" note from above.I have tested and your approch seems to be actually working fine when there are multiple servers where one of them is linked to a queue that is also used to enqueue a background job. I have tested by enqueuing several jobs and all of them are consistently being executed one by one by the server that has been linked to the queue used to enqueue each of the background jobs.
Am I missing your something?

Thanks!

@frozzen10
Copy link

hello there, I have a problem with this code. After some time (cannot really say what is some) some of my recurring jobs which are decorated with this attribute are stopping execution. I mean they are immediately canceled after new execution and when I placed logger there here is what it produces:

2023-11-28 16:05:33.7064|INFO|Namespace.Jobs.Hangfire.Attributes.SkipWhenPreviousJobIsRunningAttribute|running: yes
2023-11-28 16:05:33.7064|INFO|Namespace.Jobs.Hangfire.Attributes.SkipWhenPreviousJobIsRunningAttribute|Job will be canceled

and here is code in C#

var logger = new NLogFactory().Create(this);
string running = connection.GetValueFromHash($"recurring-job:{recurringJobId}", "Running");
logger.Info($"running: {running}");
if ("yes".Equals(running, StringComparison.OrdinalIgnoreCase))
{
    logger.Info($"Job will be canceled");
    context.Canceled = true;
}

I don't know why is it happening, but it won't trigger from this moment... I am using SQLite file as DB.

@novacema
Copy link

novacema commented Apr 9, 2024

@frozzen10 The issue you're experiencing might be because the "Running" status of the job is not being reset properly when the job fails or when it's in a final state. This could cause the job to be immediately canceled on the next execution because the system thinks it's still running. To fix this, you should ensure that the "Running" status is reset in all cases when the job is in a final state, not just when it's not in a FailedState.

in OnStateApplied
`
var recurringJobId = SerializationHelper.Deserialize(
context.Connection.GetJobParameter(context.BackgroundJob.Id, "RecurringJobId"));

if (string.IsNullOrWhiteSpace(recurringJobId)) return;

if (context.NewState is EnqueuedState)
{
transaction.SetRangeInHash(
$"recurring-job:{recurringJobId}",
new[] {new KeyValuePair<string, string>("Running", "yes")});
}
else if (context.NewState.IsFinal)
{
transaction.SetRangeInHash(
$"recurring-job:{recurringJobId}",
new[] {new KeyValuePair<string, string>("Running", "no")});
}
`

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