Forked from odinserj/DisableMultipleQueuedItemsFilter.cs
Last active
March 3, 2023 02:29
-
-
Save paulczy/f45bed1feeae623e81af406f361673ba to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. | |
// Source from: https://gist.github.com/odinserj/a8332a3f486773baa009?permalink_comment_id=4051053#gistcomment-4051053 | |
using Hangfire.Client; | |
using Hangfire.Common; | |
using Hangfire.States; | |
using Hangfire.Storage; | |
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
using System.Security.Cryptography; | |
namespace Hangfire.Filters | |
{ | |
public class DisableMultipleQueuedItemsFilter : JobFilterAttribute, IClientFilter, IApplyStateFilter | |
{ | |
private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(5); | |
private static readonly CultureInfo EnUs = new CultureInfo("en-US"); | |
/// <summary> | |
/// Convert arguments into a culture-aware string | |
/// </summary> | |
///<see cref="https://gist.github.com/odinserj/a8332a3f486773baa009?permalink_comment_id=4048344#gistcomment-4048344"/><see> | |
private static string ConvertArgument(object obj) | |
{ | |
switch (obj) | |
{ | |
case null: | |
return string.Empty; | |
case string s: | |
return s; | |
case DateTime dt: | |
return dt.ToString("o"); // ISO8601 date format | |
default: | |
return (string)Convert.ChangeType(obj, typeof(string), EnUs); // And force the rest to US English | |
} | |
} | |
private static bool AddFingerprintIfNotExists(IStorageConnection connection, Job job) | |
{ | |
var fingerprintKey = GetFingerprintKey(job); | |
var finterprintLockKey = GetFingerprintLockKey(fingerprintKey); | |
var distributedLock = connection.AcquireDistributedLock(finterprintLockKey, LockTimeout); | |
using (distributedLock) | |
{ | |
var fingerprint = connection.GetAllEntriesFromHash(fingerprintKey); | |
if (fingerprint != null) | |
{ | |
// 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(fingerprintKey, new Dictionary<string, string> | |
{ | |
{ "Timestamp", DateTimeOffset.UtcNow.ToString("o") } | |
}); | |
return true; | |
} | |
} | |
private static void RemoveFingerprint(IStorageConnection connection, Job job) | |
{ | |
var fingerprintKey = GetFingerprintKey(job); | |
var finterprintLockKey = GetFingerprintLockKey(fingerprintKey); | |
using (connection.AcquireDistributedLock(finterprintLockKey, LockTimeout)) | |
using (var transaction = connection.CreateWriteTransaction()) | |
{ | |
transaction.RemoveHash(fingerprintKey); | |
transaction.Commit(); | |
} | |
} | |
private static string GetFingerprintLockKey(string fingerprintKey) | |
{ | |
return string.Format("{0}:lock", fingerprintKey); | |
} | |
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?.Args != null) | |
{ | |
parameters = string.Join(".", job.Args.Select(ConvertArgument)); | |
} | |
if (job?.Type == null || job.Method == null) | |
{ | |
return string.Empty; | |
} | |
//https://gist.github.com/odinserj/a8332a3f486773baa009#gistcomment-1898401 | |
var payload = $"{job.Type.FullName}.{job.Method.Name}.{parameters}"; | |
var hash = SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload)); | |
var fingerprint = Convert.ToBase64String(hash); | |
return fingerprint; | |
} | |
public void OnCreating(CreatingContext filterContext) | |
{ | |
if (!AddFingerprintIfNotExists(filterContext.Connection, filterContext.Job)) | |
{ | |
filterContext.Canceled = true; | |
} | |
} | |
public void OnCreated(CreatedContext filterContext) | |
{ | |
//do nothing | |
} | |
public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) | |
{ | |
if (context.NewState.Name.Equals(Hangfire.States.SucceededState.StateName) | |
|| context.NewState.Name.Equals(Hangfire.States.FailedState.StateName)) | |
{ | |
RemoveFingerprint(context.Connection, context.BackgroundJob.Job); | |
} | |
} | |
public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) | |
{ | |
// do nothing | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment