【问题标题】:How to correctly implement recurring tasks with time/frequency set by user如何正确执行用户设置的时间/频率的重复任务
【发布时间】:2013-02-04 13:15:00
【问题描述】:

用户订阅了包含最新视频的电子邮件,但他们还设置了何时收到这些电子邮件。

Subscription(user_id, frequency, day, time, time_zone)

user_id  |  frequency  |  day    |  time   |  time_zone
1        |  daily      |  null   |  16:00  |  GMT
2        |  weekly     |  friday |  11:00  |  UTC
3        |  weekly     |  monday |  18:00  |  EST

我们如何才能在用户在他们的时区选择的确切时间和频率发送电子邮件而不会搞砸(例如发送双重电子邮件或错过时间)

唯一的频率是每天和每周,如果每天,则该天为空。

我使用 redis 作为数据库,让我知道如何正确地做到这一点!

【问题讨论】:

  • 如果您使用延迟作业 - github.com/collectiveidea/delayed_job - 您可以将每个作业的 run_at 设置为您想要的任何内容(匹配您的订阅时间,每个计划一个作业),然后在作业完成时将其排入队列自己根据您的调度程序设置再次运行
  • 您必须精确到什么程度?如果是一分钟,可以接受吗?

标签: ruby-on-rails ruby redis whenever


【解决方案1】:

这更像是一个系统级别的问题,而不是特定于 ruby​​ 的问题。

首先,将所有时间在内部存储为 GMT。时区不是真实的,除了作为偏移视图中时间的首选项设置(在用户表中)。进行数学运算的坐标系应该是一致的。

然后每个频率对应一个 cronjob 设置:每天、星期一、星期二等。实际上,从表中的数据来看,您不需要两列,除非您看到它在变化。

然后,当 cron 触发时,使用调度程序(如 linux AT)来处理电子邮件何时发出。这更像是一个系统级别的调度问题,或者至少,我更相信系统可以处理这个问题。它需要处理系统重启的情况,包括服务和应用程序。

但是,需要注意的是,如果您要发送大量邮件,您确实无法保证邮件会按照偏好发送。实际上,您可能必须将其限制为避免被转储/阻止(也就是说,1000 条/消息分布在一个小时内)。不同的网络会有不同的使用门槛。

所以基本上:

cron -> at -> 发送邮件

【讨论】:

    【解决方案2】:

    我刚刚为我的项目实现了这个,我发现了一个非常简单的方法是使用Whenever Gem(在这里找到https://github.com/javan/whenever)。

    要开始,请转到您的应用并放置

    gem 'whenever'
    

    然后使用:

    wheneverize .
    

    在终端中。这将在您的配置文件夹中创建一个 schedule.rb 文件。

    您将您的规则放入您的 schedule.rb(如下所示)并让它们调用某些方法 - 例如,我调用模型方法 DataProviderUser.cron 它将运行我在那里的任何代码。

    创建此文件后,要启动 cron 作业,请在命令行上使用:

    whenever
    whenever -w
    

    whenever -c
    

    停止/清除 cron 作业。

    github 上的 gems 文档非常有用,但我建议您将输出设置为您自己的日志文件(如下所示)。希望有帮助:)

    在我的 schedule.rb 我有:

    set :output, 'log/cron.log'
    every :hour do
    runner "DataProviderUser.cron('hourly')"
    end
    
    every :day do
    runner "DataProviderUser.cron('daily')"
    end
    
    every '0 0 * * 0' do
    runner "DataProviderUser.cron('weekly')"
    end
    
    every 14.days do
    runner "DataProviderUser.cron('fortnightly')"
    end
    
    every :month do
    runner "DataProviderUser.cron('monthly')"
    end
    

    【讨论】:

      【解决方案3】:

      我将使用resque-scheduler gem 扩展 fmendez 的答案。

      首先,让我们创建发送电子邮件的工作人员

      class SubscriptionWorker
        def self.perform(subscription_id)
          subscription = Subscription.find subscription_id
      
          # ....
          # handle sending emails here
          # ....
      
          # Make sure that you don't have duplicate workers
          Resque.remove_delayed(SubscriptionWorker, subscription_id)        
      
          # this actually calls this same worker but sets it up to work on the next
          # sending time of the subscription.  next_sending_time is a method that
          # we implement in the subscription model.
          Resque.enqueue_at(subscription.next_sending_time, SubscriptionWorker, subscription_id)
        end
      end
      

      在您的订阅模型中,添加一个next_sending_time 方法来计算下一次应该发送电子邮件的时间。

      # subscription.rb
      def next_sending_time
        parsed_time = Time.parse("#{time} #{time_zone}") + 1.day
      
        if frequency == 'daily'
          parsed_time
        else
          # this adds a day until the date matches the day in the subscription
          while parsed_time.strftime("%A").downcase != day.downcase
            parsed_time += 1.day
          end
        end
      end
      

      【讨论】:

        【解决方案4】:

        我过去曾使用delayed_job 完成类似的任务。或许您可以对resque 使用相同的技术。本质上,您必须在当前作业结束时安排下一个作业。

        class Subscription < ActiveRecord::Base
          after_create :send_email        
          def send_email 
            # do stuff and then schedule the next run
          ensure
            send_email
          end
          handle_asynchronously :send_email, :run_at => Proc.new{|s| s.deliver_at }
        
          def daily? (frequency == "daily");end
          def max_attempts 1;end
        
          def time_sec
            hour,min=time.split(":").map(&:to_i)
            hour.hours + min.minutes
          end
        
          def days_sec
            day.nil? ? 0 : Time::DAYS_INTO_WEEK[day.to_sym].days
          end
        
          def interval_start_time
            time = Time.now.in_time_zone(time_zone)
            daily? ?  time.beginning_of_day : time.beginning_of_week
          end
        
          def deliver_at
            run_at = interval_start_time + days_sec + time_sec
            if time.past?
              run_at = daily? ? run_at.tomorrow : 1.week.from_now(run_at)
            end
            run_at
          end        
        end
        

        重新安排注意事项

        更新代码以处理循环终止。您可以通过添加名为activeboolean 列来处理此问题(默认设置为true)。要禁用订阅,请将列设置为 false。

          def send_email
            return unless active?
            # do stuff and then schedule the next run
          ensure
            send_email if active?
          end
        

        将作业的max_attempts 设置为1。否则您将淹没队列。在上述解决方案中,send_email 的作业将被尝试一次。

        【讨论】:

        • 更新了答案以优雅地处理max_attempts
        【解决方案5】:

        您可以使用 DelayedJobResque Gem,它们将计划任务的每个 实例 表示为数据库表中的一行。有一些方法可以触发、安排和从日历中撤回任务。

        【讨论】:

          【解决方案6】:

          我认为你可以利用这个 gem 来达到这个目的:https://github.com/bvandenbos/resque-scheduler

          问候,

          【讨论】:

          • 不确定这是否是正确的解决方案,似乎您不能动态创建重复任务(不使用 yaml 配置文件)
          • @Ryan 在任务配置中另有说明: # 如果您希望能够动态更改计划,请取消注释此行。可以通过 #Resque::Scheduler.set_schedule(和 remove_schedule)方法更新动态计划。 # 当 dynamic 设置为 true 时,调度程序进程会查找 # schedule 更改并即时应用它们。 # 注意:此功能仅在 >=2.0.0 中可用。 #Resque::Scheduler.dynamic = true
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-11-08
          • 2012-02-28
          • 2019-04-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多