【问题标题】:Hangfire - Prevent multiples of the same job being enqueuedHangfire - 防止同一作业的多个入队
【发布时间】:2017-07-18 10:37:28
【问题描述】:

场景:

作业 1 计划每 5 分钟运行一次,大约需要 1 分钟才能完成。

大量工作堆积起来,作业 1 需要 15 分钟才能运行。

现在同时处理三个 Job 1 - 我不希望这样。


如果 Job 1 已经存在,如何防止它再次添加到队列中?

是否有 Hangfire 设置,还是我需要手动轮询作业状态?

【问题讨论】:

    标签: c# asp.net hangfire


    【解决方案1】:

    您可以使用DisableConcurrentExecution 属性来防止同时执行多个方法。只需将此属性放在您的方法之上 -

    [DisableConcurrentExecution(timeoutInSeconds: 10 * 60)]
    public void Job1()
    {
        // Metohd body
    }
    

    【讨论】:

    • 有人在 2016 年 11 月 10 日报告了关于 DisableConcurrentExecution 属性的错误。可能有问题。
    • 如果Job1()方法中有自定义输入参数怎么办?
    • 注意,Hangfire 可能会通过这种方式导致数据库连接池泄漏:codewithstyle.info/real-bug-story-long-running-jobs-hangfire
    【解决方案2】:

    有点晚了,但我正在使用这个类来防止重复的作业同时运行

    public class SkipConcurrentExecutionAttribute : JobFilterAttribute, IServerFilter, IElectStateFilter
    {
        private readonly int _timeoutSeconds;
        private const string DistributedLock = "DistributedLock";
    
        public SkipConcurrentExecutionAttribute(int timeOutSeconds)
        {
            if (timeOutSeconds < 0) throw new ArgumentException("Timeout argument value should be greater that zero.");
            this._timeoutSeconds = timeOutSeconds;
        }
    
        public void OnPerformed(PerformedContext filterContext)
        {
            if (!filterContext.Items.ContainsKey(DistributedLock))
                throw new InvalidOperationException("Can not release a distributed lock: it was not acquired.");
    
            var distributedLock = (IDisposable)filterContext.Items[DistributedLock];
            distributedLock?.Dispose();
        }
    
    
    
        public void OnPerforming(PerformingContext filterContext)
        {
            filterContext.WriteLine("Job Started");
    
            var resource = String.Format(
                               "{0}.{1}",
                              filterContext.BackgroundJob.Job.Type.FullName,
                              filterContext.BackgroundJob.Job.Method.Name);
    
            var timeOut = TimeSpan.FromSeconds(_timeoutSeconds);
    
            filterContext.WriteLine($"Waiting for running jobs to complete. (timeout: { _timeoutSeconds })");
    
            try
            {
                var distributedLock = filterContext.Connection.AcquireDistributedLock(resource, timeOut);
                filterContext.Items[DistributedLock] = distributedLock;
            }
            catch (Exception ex)
            {
                filterContext.WriteLine(ex);
                filterContext.WriteLine("Another job is already running, aborted.");
                filterContext.Canceled = true; 
            }
    
        }
    
        public void OnStateElection(ElectStateContext context)
        {
            //if (context.CandidateState as FailedState != null)
            //{
    
            //}
        }
    }
    

    希望对您有所帮助,谢谢!

    【讨论】:

    • 取消工作,是的。您还可以将方法参数添加到锁定名称,并能够运行相同的方法但使用不同的参数 - 每个只需一次
    • 另外,如果您使用作业继续,取消作业将触发依赖它的作业,因为父作业成功。记住这一点
    • 为了避免这种情况,而不是 filterContext.Canceled = true;抛出新的 JobAbortedException();这将告诉工人跳过重复的作业运行
    【解决方案3】:

    您可能对以下内容感兴趣: https://discuss.hangfire.io/t/job-reentrancy-avoidance-proposal/607/8

    讨论是关于跳过将同时执行的作业到已经运行的作业。

    【讨论】:

      【解决方案4】:

      有一个名为 DisableConcurrentExecution 的属性,可以防止 2 个相同类型的作业同时运行。

      不过,在您的情况下,最好检查任务是否运行并相应地跳过。

      【讨论】:

      • 据我了解,这将阻止它们同时执行,但它们仍将排队运行。这是朝着正确方向迈出的一步,但理想情况下,他们根本不会排队。感谢您的回答!
      • 这就是我第二句话的意思。我认为不将作业排队不是正确的方法,而是让它们启动并检查其他作业当前是否正在运行,然后在此处跳过它...
      【解决方案5】:

      是的。可能如下:

                  RecurringJob.AddOrUpdate(Environment.MachineName, () => MyJob(Environment.MachineName), Cron.HourInterval(2));
      

      MyJob 应该这样定义:

          public void MyJob(string taskId)
          {
              if (!taskId.Equals(Environment.MachineName))
              {
                  return;
              }
              //Do whatever you job should do.
          }
      

      【讨论】:

      • 这并不能解决 OP 的问题。您的回答只会阻止在除已排队的机器之外的机器上执行作业,但不会阻止重入。在您的示例中,如果您的作业需要 2 个多小时才能完成,那么 Hangfire 会将新作业排入同一台机器中,并且您将有两个相同类型的作业同时运行。
      【解决方案6】:

      如果你想放弃已经运行两次的尝试,你总是可以这样做(注意没有应用任何属性):

          private static bool _isRunningUpdateOrders;
          public void UpdateOrders()
          {
              try
              {
                  if (_isRunningUpdateOrders)
                  {
                      return; 
                  }
      
                  _isRunningUpdateOrders = true;
      
                  // Logic...
      
              }
              finally 
              {
                  _ isRunningUpdateOrders = false;
              }
         }
      

      编辑:请仅使用类似的方法作为快速修复,例如,如果您刚刚发现问题并且仍在评估更好的解决方案 :-) 或者如果您很懒,只想“解决问题;-)

      【讨论】:

      • 错误答案。因为你认为没有人可以调用UpdateOrders,没有锁?行。您可以使用锁定,但由于您无法处理 semafor,因此会再次出现问题。
      • 当有多个服务器时这会失败,因为静态将是服务器本地的。如果你只有一个 hangfire 实例,它可以工作。
      • 为了避免竞争条件,我将在 try 语句之前使用 if (Interlocked.Exchange(ref _isRunningUpdateOrders, 1) == 1) return; 并在 finally 部分内使用 Interlocked.Exchange(ref _isRunningUpdateOrders, 0);。顺便说一句,Hangfire 文档还提到,有时人们可能更喜欢自己的锁定机制——在单个服务器处理作业的情况下。
      • 这个解决方案甚至可能是唯一可能的解决方案,因为 Hangfire 可能会导致数据库连接池泄漏,而使用 DisableConcurrentExecution 属性的替代解决方案:codewithstyle.info/real-bug-story-long-running-jobs-hangfire
      • 这个答案假设只有 1 个服务器处理作业。此解决方案不适用于多台服务器。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多