Rufus Scheduler - 帮助在模型中启动/停止作业

时间:2016-11-15 17:28:02

标签: ruby ruby-on-rails-5 rufus-scheduler

在阅读Rufus Scheduler Docs大约100次后,我可以找到每个StackOverflow问题,查看源代码,最后阅读how to report bugs effectively两次。我已经到了@jmettraux我需要你帮助的地步:

应用程序

我正在编写一个内部应用程序,允许我的团队创建指标,定期运行将值保存到数据库的代码。我有以下设置。

schema.rb

create_table "metrics", force: :cascade do |t|
    t.string   "frequency"
    t.string   "name"
    t.text     "data"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.enter code hereboolean  "active"
  end

初始化/ scheduler.rb

require 'rufus-scheduler'

class Rufus::Scheduler::Job
  def report
    logger = Logger.new(STDOUT)
    logger.info "Job: #{@id} Tags: #{@tags} Frequency: #{@frequency}"
  end
end

SCHEDULER = Rufus::Scheduler.new(:lockfile => ".rufus-scheduler.lock")

unless defined?(Rails::Console) || File.split($0).last == 'rake'
  #Launch All Matrics Jobs that are active
  @metrics = Metric.active

  unless SCHEDULER.down?
    @metrics.each_with_index do | metric, index |
      SCHEDULER.every metric.frequency, :tags => metric.id, :overlap => false, :timeout => metric.try(:timeout) || '3m' do | job |
        metric.add_value
        job.report
      end
    end
  end
end

什么有用

上面的代码完美无瑕,Rufus正确初始化,所有工作都按照应有的方式开始。

问题

我遇到的问题是,一旦应用程序运行,我需要能够取消计划作业并在保存指标时重新安排它们。我使用Rufus标签在创建时为其分配带有度量标准ID的标记,并且我使用它来加载它们以进行非排序。 这是我当前的度量模型及相关代码。

模型/ metric.rb

class Metric < ApplicationRecord

  ...

  after_save :update_job

  private
  def update_job
    if self.changed.present?
      job = SCHEDULER.jobs(tag: self.id).first
      if job.present?
        logger.info "Job #{job.id}, for Metric #{self.id} unscheduled."
        SCHEDULER.unschedule(job.id)
      end

      if self.active
        metric = self
        new_job = SCHEDULER.every self.frequency, :tags => self.id, :overlap => false, :timeout => self.try(:timeout) || '3m' do | job |
          metric.add_value
        end
        logger.info "Job #{new_job} for Metric #{self.id} scheduled."
      end
    end
  end

end

实际问题!

  

情景1:

     
      
  • 应用程序启动
  •   
  • scheduler initilizer加载所有活动指标
  •   
  • scheduler initilizer将所有活动指标作为“每个”作业启动
  •   
  • 用户编辑有效指标,将其设置为无效
  •   
  • 在保存指标后运行update_job方法
  •   
  • 在update_job方法中,成功找到当前指标的作业
  •   
  • 在作业上调用unscheudle方法
  •   
  • 问题在这里:即使在不定期之后,工作也会再次运行
  •   
     

情景2:

     
      
  • 应用程序启动
  •   
  • scheduler initilizer加载所有活动指标
  •   
  • scheduler initilizer将所有活动指标作为“每个”作业启动
  •   
  • 用户编辑非活动指标,将其设置为活动
  •   
  • 在保存指标后运行update_job方法
  •   
  • 在update_job方法中,为度量标准
  • 安排了新的“每个”作业   
  • 问题在这里:工作永远不会开始
  •   

我尝试了什么

  • 我尝试将以下代码添加到初始化程序的底部,以确保取消计划方法在我的应用程序中正常运行。

    sleep 50
    job = SCHEDULER.jobs(tag: 2).first
    SCHEDULER.unschedule(job)
    

    ID为2的指标的频率为20秒。它运行两次然后正确调度,所以我知道它在模型代码之外正常工作。

系统/应用程序信息

  • Ruby 2.3.0
  • Rails 5.0.0.1
  • Puma 3.6.0
  • OSX 10.12.1 Sierra
  • rufus-scheduler 3.2.2

第2阶段日志 - 2016年12月20日

情景1:

  xxx SCHEDULER started: 70168058449780
  xxx trigger (scheduled in initializer) Job: Rufus::Scheduler::EveryJob every_1482273018.01852_1684784125738549751 (70168045492220) Tags: ["2"] Frequency: 20.0
  xxx Metric 2 #update_job active: false
  xxx Metric 2 #update_job 0 SCHEDULER Rufus::Scheduler 3.2.2 70168058449780 down? false at_jobs: 0 in_jobs: 0 every_jobs: 2 interval_jobs: 0 cron_jobs: 0
  xxx Metric 2 found Job Job: Rufus::Scheduler::EveryJob every_1482273018.01852_1684784125738549751 (70168045492220) Tags: ["2"] Frequency: 20.0
  xxx Metric 2 Job every_1482273018.01852_1684784125738549751 unscheduled
  xxx Metric 2 #update_job 1 SCHEDULER Rufus::Scheduler 3.2.2 70168058449780 down? false at_jobs: 0 in_jobs: 0 every_jobs: 1 interval_jobs: 0 cron_jobs: 0
  xxx Metric 2 #update_job 2 SCHEDULER Rufus::Scheduler 3.2.2 70168058449780 down? false at_jobs: 0 in_jobs: 0 every_jobs: 1 interval_jobs: 0 cron_jobs: 0
  xxx trigger (scheduled in initializer) Job: Rufus::Scheduler::EveryJob every_1482273018.01852_1684784125738549751 (70168045492220) Tags: ["2"] Frequency: 20.0
  xxx trigger (scheduled in initializer) Job: Rufus::Scheduler::EveryJob every_1482273018.01852_1684784125738549751 (70168045492220) Tags: ["2"] Frequency: 20.0

情景2:

  xxx SCHEDULER started: 70272158805840
  xxx Metric 2 #update_job active: true
  xxx Metric 2 #update_job 0 SCHEDULER Rufus::Scheduler 3.2.2 70272158805840 down? false at_jobs: 0 in_jobs: 0 every_jobs: 1 interval_jobs: 0 cron_jobs: 0
  xxx Metric 2 #update_job 1 SCHEDULER Rufus::Scheduler 3.2.2 70272158805840 down? false at_jobs: 0 in_jobs: 0 every_jobs: 1 interval_jobs: 0 cron_jobs: 0
  xxx Metric 2 Job every_1482273159.799382_1489585858424637616 scheduled
  xxx Metric 2 #update_job 2 SCHEDULER Rufus::Scheduler 3.2.2 70272158805840 down? false at_jobs: 0 in_jobs: 0 every_jobs: 2 interval_jobs: 0 cron_jobs: 0

第3阶段日志 - 2016年12月20日

情景1:

xxx SCHEDULER started: 70321240613760
xxx sc Rufus::Scheduler 3.2.2 70321240613760
xxx sc down? false
xxx sc Process.pid 39975
xxx sc jobs:
xxx sc   0: j: Rufus::Scheduler::EveryJob i: "every_1482335616.453129_4198855636205920237" oi: 70321198587560 ts: ["2"] frq: 20.0 ua: nil c: 0 nt: 2016-12-21 09:53:56 -0600
xxx initializer over.
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 scheduled in initializer for Metric 2
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 scheduled in initializer for Metric 2
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 scheduled in initializer for Metric 2
xxx Metric 2 #update_job changed: ["active", "updated_at"] active: false
xxx Metric 2 sc #update_job 0 Rufus::Scheduler 3.2.2 70321240613760
xxx Metric 2 sc #update_job 0 down? false
xxx Metric 2 sc #update_job 0 Process.pid 40037
xxx Metric 2 sc #update_job 0 jobs:
xxx Metric 2 sc #update_job 0   0: j: Rufus::Scheduler::EveryJob i: "every_1482335616.453129_4198855636205920237" oi: 70321198587560 ts: ["2"] frq: 20.0 ua: nil c: 0 nt: 2016-12-21 09:53:56 -0600
xxx Metric 2 found Job j: Rufus::Scheduler::EveryJob i: "every_1482335616.453129_4198855636205920237" oi: 70321198587560 ts: ["2"] frq: 20.0 ua: nil c: 0 nt: 2016-12-21 09:53:56 -0600 first
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 unscheduled
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 scheduled? false
xxx Metric 2 sc #update_job 1 Rufus::Scheduler 3.2.2 70321240613760
xxx Metric 2 sc #update_job 1 down? false
xxx Metric 2 sc #update_job 1 Process.pid 40037
xxx Metric 2 sc #update_job 1 jobs:
xxx Metric 2 Job every_1482335616.453129_4198855636205920237 scheduled in initializer for Metric 2

情景2:

xxx SCHEDULER started: 70152969118240
xxx sc Rufus::Scheduler 3.2.2 70152969118240
xxx sc down? false
xxx sc Process.pid 40811
xxx sc jobs:
xxx initializer over.
xxx Metric 2 #update_job changed: ["active", "updated_at"] active: true
xxx Metric 2 sc #update_job 0 Rufus::Scheduler 3.2.2 70152969118240
xxx Metric 2 sc #update_job 0 down? false
xxx Metric 2 sc #update_job 0 Process.pid 40872
xxx Metric 2 sc #update_job 0 jobs:
xxx Metric 2 sc #update_job 1 Rufus::Scheduler 3.2.2 70152969118240
xxx Metric 2 sc #update_job 1 down? false
xxx Metric 2 sc #update_job 1 Process.pid 40872
xxx Metric 2 sc #update_job 1 jobs:
xxx Metric 2 Job every_1482336051.9823449_3069051954259909338 scheduled in #update_job
xxx Metric 2 sc #update_job 2 Rufus::Scheduler 3.2.2 70152969118240
xxx Metric 2 sc #update_job 2 down? false
xxx Metric 2 sc #update_job 2 Process.pid 40872
xxx Metric 2 sc #update_job 2 jobs:
xxx Metric 2 sc #update_job 2   0: j: Rufus::Scheduler::EveryJob i: "every_1482336051.9823449_3069051954259909338" oi: 70152971745840 ts: ["2"] frq: 20.0 ua: nil c: 0 nt: 2016-12-21 10:01:11 -0600

1 个答案:

答案 0 :(得分:1)

  

当上述代码运行以使活动指标变为非活动状态时,我成功获取了Logger消息并加载了正确的作业。同样,当我使用更好的错误来检查该区域的代码时,作业本身会正确设置@unscheduled_at属性,但是Job会不断重新安排再次运行。

     

另一方面,当update_job方法运行并尝试重新安排作业时,它永远不会启动。

那么你是否想说约伯被重新安排,但新的,重新安排的约伯永远不会触发?

如果我天真地看着你的代码(为了更好的理解而重新编写代码):

def update_job

  if self.changed.present?
    job = SCHEDULER.jobs(tag: self.id).first
    if job.present?
      logger.info "Job #{job.id}, for Metric #{self.id} unscheduled."
      job.unschedule
    end

    if self.active
      metric = self
      new_job_id =
        SCHEDULER.every(
          self.frequency,
          :tags => self.id,
          :overlap => false,
          :timeout => self.try(:timeout) || '3m'
        ) { |job| metric.add_value }
      logger.info "Job #{new_job_id} for Metric #{self.id} scheduled."
    end
  end
end

您描述的症状是否可以恢复为“应用程序永远不会进入update_job方法的重新安排块”?

  

ID为2的指标的频率为20秒。它运行两次然后正确计划,所以我知道它在模型代码之外正常工作。

所以这是你的代码模型中的东西。与rufus-scheduler相关的可能性很小。

请仔细改写您的“实际问题”。在目前的版本中,我很困惑。

第2阶段 - 2016-12-21

您能否尝试使用以下代码?

警告:它未经过测试,可能包含您必须发现并修复以使其可用的错误。

然后尝试你的两个scenarii并报告,这个代码正在发出“xxx”的日志grep。它可能会告诉我们出了什么问题。

提前致谢。

# initializers/scheduler.rb

require 'rufus-scheduler'

class Rufus::Scheduler
  def to_report_s
    a = []
    a << self.class
    a << Rufus::Scheduler::VERSION
    a << self.object_id
    a << "down? #{self.down?}"
    %w[ at in every internal cron ].each do |flav|
      m = "#{flav}_jobs".to_sym
      a << "#{m}: #{self.send(m).size}"
    end
    a.collect(&:to_s).join(' ')
  end
end
class Rufus::Scheduler::Job
  def to_report_s
    "Job: #{self.class} #{@id} (#{self.object_id}) " +
    "Tags: #{@tags.inspect} Frequency: #{@frequency}"
  end
end

#SCHEDULER = Rufus::Scheduler.new(:lockfile => ".rufus-scheduler.lock")
SCHEDULER = Rufus::Scheduler.new

# Add "global" error handler to the rufus-scheduler instance
#
def SCHEDULER.on_error(job, error)

  Rails.logger.error(
    "xxx err#{error.object_id} rufus-scheduler intercepted #{error.inspect}" +
    " in job #{job.to_report_s}")
  error.backtrace.each_with_index do |line, i|
    Rails.logger.error(
      "xxx err#{error.object_id} #{i}: #{line}")
  end
end

logger.info("xxx SCHEDULER started: #{SCHEDULER.object_id}")

unless (
  defined?(Rails::Console) || File.split($0).last == 'rake' ||
  SCHEDULER.down?
)
  # launch All Matrics Jobs that are active

  Metric.active.each do |metric|

    SCHEDULER.every(
      metric.frequency,
      :tags => metric.id,
      :overlap => false,
      :timeout => metric.try(:timeout) || '3m'
    ) do |job|
      logger.info "xxx trigger (scheduled in initializer) #{job.to_report_s}"
      metric.add_value
    end
  end
end

# models/metric.rb

class Metric < ApplicationRecord

  after_save :update_job

  private

  def update_job

    lip = "Metric #{id}" # logger info prefix
    logger.info("xxx #{lip} #update_job active: #{self.active.inspect}")

    if self.changed.present?

      logger.info("xxx #{lip} #update_job 0 SCHEDULER #{SCHEDULER.to_report_s}")

      #job = SCHEDULER.jobs(tag: self.id).first
      #if job.present?
      if job = SCHEDULER.jobs(tag: self.id).first
        logger.info "xxx #{lip} found Job #{job.to_report_s}"
        #SCHEDULER.unschedule(job.id)
        job.unschedule
        logger.info "xxx #{lip} Job #{job.id} unscheduled"
      end

      logger.info("xxx #{lip} #update_job 1 SCHEDULER #{SCHEDULER.to_report_s}")

      if self.active
        metric = self
        job_id = SCHEDULER.every(
          self.frequency,
          :tags => self.id,
          :overlap => false,
          :timeout => self.try(:timeout) || '3m'
        ) do |job|
          logger.info "xxx trigger (scheduled in #updateJob) #{job.to_report_s}"
          metric.add_value
        end
        logger.info "xxx #{lip} Job #{job_id} scheduled"
      end

      logger.info("xxx #{lip} #update_job 2 SCHEDULER #{SCHEDULER.to_report_s}")
    end
  end
end

第3阶段 - 2016-12-21

请您重试以下代码:

# initializers/scheduler.rb

require 'rufus-scheduler'

class Rufus::Scheduler
  def to_report_a
    a = []
    a << "#{self.class} #{Rufus::Scheduler::VERSION} #{object_id}"
    a << "down? #{self.down?}"
    a << "Process.pid #{Process.pid}"
    a << "jobs:"
    jobs.each_with_index { |job, i| a << "  #{i}: #{job.to_report_s}" }
    a.collect(&:to_s)
  end
end
class Rufus::Scheduler::Job
  def to_report_s
    {
      j: self.class, i: @id, oi: object_id, ts: @tags,
      frq: @frequency, ua: @unscheduled_at, c: count, nt: next_time
    }
      .collect { |k, v| "#{k}: #{v.inspect}" }
      .join(' ')
  end
end

#SCHEDULER = Rufus::Scheduler.new(:lockfile => ".rufus-scheduler.lock")
SCHEDULER = Rufus::Scheduler.new

# Add "global" error handler to the rufus-scheduler instance
#
def SCHEDULER.on_error(job, error)
  lep = "xxx err#{error.object_id}"
  Rails.logger.error(
    "xxx #{lep} rufus-scheduler intercepted #{error.inspect}" +
    " in job #{job.to_report_s}")
  error.backtrace.each_with_index do |line, i|
    Rails.logger.error(
      "xxx #{lep} #{i}: #{line}")
  end
  Rails.logger.error("xxx #{lep} scheduler:")
  SCHEDULER.to_report_a.each { |l| Rails.logger.error("xxx #{lep} #{l}") }
end

logger.info("xxx SCHEDULER started: #{SCHEDULER.object_id}")

unless (
  defined?(Rails::Console) || File.split($0).last == 'rake' ||
  SCHEDULER.down?
)
  # launch All Matrics Jobs that are active

  Metric.active.each do |metric|

    lip = "Metric #{metric.id}" # logger info prefix

    job_id =
      SCHEDULER.every(
        metric.frequency,
        :tags => metric.id,
        :overlap => false,
        :timeout => metric.try(:timeout) || '3m'
      ) do |job|
        logger.info(
          "xxx #{lip} trigger (scheduled in initializer) " +
          "#{job.to_report_s}")
        metric.add_value
      end
    logger.info(
      "xxx #{lip} Job #{job_id} scheduled in initializer " +
      "for Metric #{metric.id}")
  end

  SCHEDULER.to_report_a.each { |l| logger.info("xxx sc #{l}") }
  logger.info("xxx initializer over.")
end

# models/metric.rb

class Metric < ApplicationRecord

  after_save :update_job

  private

  def update_job

    lip = "Metric #{id}" # logger info prefix
    logger.info(
      "xxx #{lip} #update_job " +
      "changed: #{self.changed.inspect} active: #{self.active.inspect}")

    return unless self.changed.present?

    SCHEDULER.to_report_a
      .each { |l| logger.info("xxx #{lip} sc #update_job 0 #{l}") }

    jobs = SCHEDULER.jobs(tag: self.id)

    jobs.each_with_index do |job, i|
      logger.info(
        "xxx #{lip} found Job #{job.to_report_s} #{i == 0 ? 'first' : ''}")
    end

    if job = jobs.first
      #SCHEDULER.unschedule(job.id)
      job.unschedule
      logger.info "xxx #{lip} Job #{job.id} unscheduled"
      logger.info "xxx #{lip} Job #{job.id} scheduled? #{SCHEDULER.scheduled?(job.id)}"
    end

    SCHEDULER.to_report_a
      .each { |l| logger.info("xxx #{lip} sc #update_job 1 #{l}") }

    return unless self.active

    metric = self
    job_id = SCHEDULER.every(
      self.frequency,
      :tags => self.id,
      :overlap => false,
      :timeout => self.try(:timeout) || '3m'
    ) do |job|
      logger.info "xxx trigger (scheduled in #update_job) #{job.to_report_s}"
      metric.add_value
    end
    logger.info "xxx #{lip} Job #{job_id} scheduled in #update_job"

    SCHEDULER.to_report_a
      .each { |l| logger.info("xxx #{lip} sc #update_job 2 #{l}") }
  end
end

第4阶段 - 2016-12-22

  

添加第3阶段日志以提问。看起来有些新的调度程序进程随后会在模型代码中创建然后销毁。再次感谢您对此的勤奋!

这是否真的发生在型号代码中?您的日志告诉我们它发生在另一个进程中。您的初始Ruby进程实例化rufus-scheduler,然后您的HTTP请求在工作进程中提供,这些进程是您的初始进程的分支(没有线程,换句话说是非活动的调度程序)。

您在群集模式下使用Puma。我应该立即问你关于你的配置。

https://github.com/puma/puma#configuration

仔细阅读其文档

一个简单的解决方法是不使用集群模式,因此只涉及一个Ruby进程,为所有HTTP请求提供服务。

另一方面,如果您需要群集模式,则必须改变您的思维方式。您可能不希望每个工作线程有1个rufus-scheduler实例。您可以专注于在主进程中使用核心(实时)rufus调度程序。它可能有一个“管理”工作,可以检查最近更新的指标和计划/计划工作。

SCHEDULER.every '10s', overlap: false do
  Metric.recently_updated.each do |metric|
    SCHEDULER.jobs(tags: metric.id).each(&:unschedule)
    SCHEDULER.every(metric.frequency, tags: self.id) { metric.add_value }
  end
end
  # or something like that...

玩得开心!