Как правильно выполнять повторяющиеся задачи с заданным временем/частотой

Пользователи подписываются на электронные письма, содержащие последние видеоролики, но также устанавливают, когда следует получать эти письма.

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 как базу данных для этого, дайте мне знать, как сделать это правильно!

Ответ 1

Я собираюсь расширить ответ на fmendez, используя драгоценный камень resque-scheduler.

Сначала создайте рабочего, который отправляет письма

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

Ответ 2

Я использовал 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

Предостережения о перепланировке

Обновите код для завершения цикла. Вы можете справиться с этим, добавив столбец boolean с именем active (по умолчанию установите его 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 будут предприняты один раз.

Ответ 3

Это скорее проблема системного уровня, чем специфичность для ruby.

Прежде всего, сохраните все свое время внутри страны как GMT. Часовые пояса не являются реальными, за исключением настройки предпочтений (в таблице пользователя), которая компенсирует время в представлении. Система координат, в которой выполняется математика, должна быть последовательной.

Затем каждая частота соответствует настройке cronjob: ежедневно, понедельникам, вторникам и т.д. Действительно, из данных в таблице вам не нужны два столбца, если вы не видите это как изменение.

Затем, когда срабатывает cron, используйте планировщик (например, linux AT) для обработки, когда электронная почта гаснет. Это скорее проблема планирования уровня системы, или, по крайней мере, я бы доверял системе больше, чтобы справиться с этим. Он должен обрабатывать случай перезагрузки системы, включая службы и приложение.

Одно из предостережений заключается в том, что если вы отправляете большой объем почты, вы действительно не можете гарантировать, что почта будет отправлена ​​в соответствии с предпочтением. Вам, возможно, придется отключить его, чтобы избежать сбрасывания/блокировки (скажем, 1000/сообщений, распространяемых в течение часа). Различные сети будут иметь разные пороговые значения.

Итак, в основном:

cron → at → отправить почту

Ответ 5

Вы можете использовать DelayedJob или Resque Gems, которые представляют каждый экземпляр запланированной задачи как строку в таблице базы данных. Существуют способы запуска, планирования и снятия задач из календаря.

Ответ 6

Я только что реализовал это для своего проекта, я нашел очень простой способ сделать это - использовать Whenever Gem (найденный здесь https://github.com/javan/whenever).

Чтобы начать работу, зайдите в свое приложение и поместите

gem 'whenever'

затем используйте:

wheneverize .

в терминале. Это создаст файл schedule.rb в вашей папке config.

Вы помещаете свои правила в свой расписание .rb(показано ниже) и разрешаете им вызывать определенные методы - например, мои вызовы метода модели DataProviderUser.cron, который будет запускать любой код, который у меня есть.

Как только вы создали этот файл, чтобы запустить задание cron, в командной строке используйте:

whenever
whenever -w

и

whenever -c

останавливает/очищает задания cron.

Документация gems на github действительно полезна, но я рекомендую вам установить вывод в свой собственный файл журнала (как я сделал ниже). Надеюсь, что помогает:)

в моем 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