Introduction
What is Hangfire?
“An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.”
https://www.hangfire.io/
Basically, we needed to replace our “good” old Microsoft WorkFlow’s by something else. And the decision has been made; we will use Hangfire!
What is the problem with Hangfire?
It is not possible to create recurring tasks under a minute with Hangfire. We tried to create a work around, even if it’s not perfect. The basic idea is to use a FOR loop. Every x seconds a task will be executed then wait until the next execution.
For example: we will say that during a minute, we want to run a task
every 5 seconds. So we will execute 12 times the job during this minute.
So now let’s say that a task takes 2 seconds to execute. We will have to wait 3 seconds before the next execution. And if the next task takes 4 seconds to execute, we only need to wait 1 second before the next execution.
Now we need to check if the global job doesn’t take more than 1 minute. So, a second check is needed. If the total time spent in the job is more than 1 minute, exit the job.
It is recommended to use some attributes on your job methods. It will be explained at the end of this post.
CronInfo
First things first! It’s possible that we have tasks that have to run every x seconds. But should keep in mind that it’s possible to have tasks that will have to run every x minutes. So we need to create a CronInfo model which contains the “cron expression”. If the recurring job is under a minute it contains also the seconds.
In summary :
- < 1 minute : cron expression + seconds.
- > 1 minute : only cron expression
The seconds will help us to distinguish later the special case.
public class CronInfo
{
public string CronExpression { get; set; }
public int? Seconds { get; set; }
public CronInfo(string cronExpression, int? seconds)
{
CronExpression = cronExpression;
Seconds = seconds;
}
}
Code language: C# (cs)
Manage recurring tasks under a minute
The Execute method will receive our model and the task to execute.
[AutomaticRetry(Attempts = 0, LogEvents = true, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
[DisableConcurrentExecution(timeoutInSeconds: 60)]
private async Task ExecuteAsync(CronInfo cronInfo, string jobName, Task jobTask)
{
if (cronInfo.Seconds.HasValue)
{
int interval = 60 / cronInfo.Seconds.Value;
var total = Stopwatch.StartNew();
for (int i = 0; i < interval; i++)
{
var current = Stopwatch.StartNew();
await jobTask;
if (total.Elapsed.Seconds >= 60) break;
var msElapsed = (cronInfo.Seconds.Value * 1000) - current.Elapsed.Milliseconds;
if (msElapsed > 0)
await Task.Delay(msElapsed);
}
}
else
await jobTask;
}
Code language: C# (cs)
Hangfire Attributes
DisableConcurrentExecution
As explain in Hangfire’s documentation, this attribute creates a distributed lock. It will avoid concurrency issues in our less than a minute recurring tasks.
AutomaticRetry
The AutomaticRetry attribute allows us to retry in case of failure. But it’s not a behavior we want in our example. So it that case we can use “Attempts = 0”. We don’t want a stack of job in our queue. “OnAttempsExceeded = Delete” because we don’t need to keep the job, a new one will start just after.
Have a look at the Cron Converter post.
Happy coding! 🙂