-
-
Save odinserj/a8332a3f486773baa009 to your computer and use it in GitHub Desktop.
// 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. | |
public class DisableMultipleQueuedItemsFilter : JobFilterAttribute, IClientFilter, IServerFilter | |
{ | |
private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5); | |
private static readonly TimeSpan FingerprintTimeout = TimeSpan.FromHours(1); | |
public void OnCreating(CreatingContext filterContext) | |
{ | |
if (!AddFingerprintIfNotExists(filterContext.Connection, filterContext.Job)) | |
{ | |
filterContext.Canceled = true; | |
} | |
} | |
public void OnPerformed(PerformedContext filterContext) | |
{ | |
RemoveFingerprint(filterContext.Connection, filterContext.Job); | |
} | |
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; | |
} | |
// 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") } | |
}); | |
return true; | |
} | |
} | |
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(); | |
} | |
} | |
private static string GetFingerprintLockKey(Job job) | |
{ | |
return String.Format("{0}:lock", GetFingerprintKey(job)); | |
} | |
private static string GetFingerprintKey(Job job) | |
{ | |
return String.Format("fingerprint:{0}", GetFingerprint(job)); | |
} | |
private static string GetFingerprint(Job job) | |
{ | |
string parameters = string.Empty; | |
if (job.Arguments != null) | |
{ | |
parameters = string.Join(".", job.Arguments); | |
} | |
if (job.Type == null || job.Method == null) | |
{ | |
return string.Empty; | |
} | |
var fingerprint = String.Format( | |
"{0}.{1}.{2}", | |
job.Type.FullName, | |
job.Method.Name, parameters); | |
return fingerprint; | |
} | |
void IClientFilter.OnCreated(CreatedContext filterContext) | |
{ | |
} | |
void IServerFilter.OnPerforming(PerformingContext filterContext) | |
{ | |
} | |
} |
To fix it we changed ConvertArgument method to ignore token.
Please share your code changes
@HenrikHoyer Did you ever figure out the code @afelinczak was talking about
Hello, I missed the comment - sorry.
This is the fix we are using.
private static string ConvertArgument(object obj) => obj switch { CancellationToken => String.Empty, _ => JsonConvert.SerializeObject(obj) };
Hangfire replaced it with null in Parameters
Hello @afelinczak. What do you mean? CancellationToken is a struct, it can't be null. We must pass it as the default
but it'll be replaced with real token in runtime. So it shouldn't be null anyway.
Btw. There is at least one more "special" parameter: PerformContext
. It is not well documented but is is used by popular extension Hangfire.Console
Hello,
We experienced problem when passing CancellationToken into a job.
https://docs.hangfire.io/en/latest/background-methods/using-cancellation-tokens.html
In our case, when job was completed calculated hash was incorrect as Hangfire replaced it with null in Parameters.
To fix it we changed ConvertArgument method to ignore token.