public class DisableMultipleQueuedItemsAttribute : JobFilterAttribute, IClientFilter, IServerFilter, IElectStateFilter
{
private static readonly TimeSpan FingerprintTimeout = TimeSpan.FromHours(1);
private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(10);
void IClientFilter.OnCreated(CreatedContext filterContext)
{
}
void IServerFilter.OnPerforming(PerformingContext filterContext)
{
}
public void OnCreating(CreatingContext filterContext)
{
if (!AddFingerprintIfNotExists(filterContext.Connection, filterContext.Job))
{
filterContext.Canceled = true;
}
}
public void OnPerformed(PerformedContext filterContext)
{
RemoveFingerprint(filterContext.Connection, filterContext.BackgroundJob);
}
public void OnStateElection(ElectStateContext context)
{
// If for any reason we delete a job from queue we release the lock.
if (context.CandidateState.Name == "Deleted")
{
RemoveFingerprint(context.Connection, context.BackgroundJob);
}
}
private static bool AddFingerprintIfNotExists(IStorageConnection connection, Job job)
{
using (connection.AcquireDistributedLock(GetFingerprintLockKey(job), LockTimeout))
{
var fingerprint = connection.GetAllEntriesFromHash(GetFingerprintKey(job));
DateTimeOffset timestamp;
if (fingerprint != null &&
fingerprint.ContainsKey("Timestamp") &&
DateTimeOffset.TryParse(fingerprint["Timestamp"], null, DateTimeStyles.RoundtripKind, out timestamp) &&
DateTimeOffset.UtcNow <= timestamp.Add(FingerprintTimeout))
{
// Actual fingerprint found, returning.
return false;
}
try
{
// Fingerprint does not exist, it is invalid (no `Timestamp` key),
// or it is not actual (timeout expired).
connection.SetRangeInHash(
GetFingerprintKey(job),
new Dictionary<string, string>
{
{ "Timestamp", DateTimeOffset.UtcNow.ToString("o") }
});
}
catch (Exception ex)
{
throw;
}
return true;
}
}
private static string GetFingerprint(Job job)
{
string parameters = string.Empty;
if (job.Args != null)
{
parameters = string.Join(".", job.Args);
}
if (job.Type == null || job.Method == null)
{
return string.Empty;
}
var fingerprint = $"{job.Type.FullName}.{job.Method.Name}.{parameters}";
return fingerprint;
}
private static string GetFingerprintKey(Job job)
{
return $"Fingerprint:{GetFingerprint(job)}";
}
private static string GetFingerprintLockKey(Job job)
{
return $"{GetFingerprintKey(job)}:lock";
}
private static void RemoveFingerprint(IStorageConnection connection, BackgroundJob job)
{
RemoveFingerprint(connection, job.Job);
}
private static void RemoveFingerprint(IStorageConnection connection, Job job)
{
using (connection.AcquireDistributedLock(GetFingerprintLockKey(job), LockTimeout))
{
using (var transaction = connection.CreateWriteTransaction())
{
transaction.RemoveHash(GetFingerprintKey(job));
transaction.Commit();
}
}
}
}
This is the code I found and am using to disable multiple queued jobs at the same time. What happens is that sometimes
connection.SetRangeInHash(
GetFingerprintKey(job),
new Dictionary<string, string>
{
{ "Timestamp", DateTimeOffset.UtcNow.ToString("o") }
});
this code fails with exception: “String or binary data would be truncated. The statement has been terminated.”
Call to queue job:
BackgroundJob.Enqueue<TestClass>(x => x.Run(model.From, model.To));
and
GetFingerprintKey(job)
returns
“Fingerprint:SomeNamespace.Jobs.Hangfire.Jobs.TestClass.Run.13.4.2016 00:00:00.13.4.2016 00:00:00”
and everything is ok. But if I add another string param to method Run() then the fingerprint looks like
“Fingerprint:SomeNamespace.Jobs.Hangfire.Jobs.TestClass.Run.13.4.2016 00:00:00.13.4.2016 00:00:00.x”
and I get the exception.
This is all on local configuration with SQL server. On production it’s even weirder, I can queue job with date 12.4.2016 but not with 13.4.2016. Seems like it’s using en-us culture but culture is set for whole app and even jobs have the right culture when I queue them so I have no idea what is causing it.
Is there a bug in SetRangeInHash?
Or am I missing an obvious bug?
Or is there a better way to disable multiple same queued jobs?
Or am I just crazy?