Как правильно выполнять повторяющиеся задачи с заданным временем/частотой
Пользователи подписываются на электронные письма, содержащие последние видеоролики, но также устанавливают, когда следует получать эти письма.
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 → отправить почту
Ответ 4
Думаю, вы можете использовать этот камень для этой цели: https://github.com/bvandenbos/resque-scheduler
Привет,
Ответ 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