-
-
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) | |
| { | |
| } | |
| } |
@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
The attribute does not work for me.
I tried applying it to the job's class and to the job's execution method.
Neither seems to work.
@afelinczak
Please share your code changes