В течение почти семи лет у KickoffLabs была возможность отправлять еженедельные или ежедневные электронные письма о прогрессе в предыдущие дни нашим клиентам. Чтобы сохранить актуальность, мы всегда делали все возможное, чтобы отправить его около 8 утра по местному времени.

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

Моя реализация, написанная почти семь лет назад, работала примерно так:

  1. Получите четкий список известных часовых поясов для наших клиентов
  2. Прокрутите их и посмотрите, где текущий час 8
  3. Найдите пользователей в этих часовых поясах

Глядя на живой код, я вижу, что 95% его было написано в сентябре 2011 года (и единственными реальными изменениями было подключение отличного сервиса DeadManSnitch для обеспечения их отправки)

Для IndieTriage я хотел иметь те же электронные письма с учетом часового пояса, но с дополнительным поворотом. Я хочу, чтобы пользователи (в конце концов) могли выбрать час, когда они хотят получить электронное письмо. Может быть, у них есть время в начале дня, или, может быть, имеет смысл получить это письмо за обедом.

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

Это 2019 год. Мы можем добиться большего.

ORM, такие как ActiveRecord, избавляют от 99% случаев утомительной грязной работы. К сожалению, они также скрывают некоторые потрясающие встроенные функции базы данных.

Вместо этого мы можем написать некоторый SQL, используя функцию Postgresql In Time Zone и соединить ее с функцией date_part.

В итоге мы получаем запрос, который выглядит примерно так:

select subscribers.id from subscribers
where date_part('hour', (now() AT TIME ZONE subscribers.time_zone)) = subscribers.hour;

Затем мы можем создать такую ​​​​область (предполагается, что столбцы с именами time_zone и hour)

scope :current_hour_by_time_zone, -> {
  where(Arel.sql("date_part('hour', now() AT TIME ZONE #{table_name}.time_zone) = #{table_name}.hour"))
}

Наконец, мы можем объединить все это в рабочем процессе cron Sidekiq:

class EnqueueDailyEmailsForSubscribersWorker
  include Sidekiq::Worker

  def perform
    Subscriber.
      select(:id).
      current_hour_by_time_zone.
      confirmed.
      last_emailed(22.hours.ago).
      find_in_batches(batch_size: 1000) do |subscribers|

      Sidekiq::Client.push_bulk(
        "class" => "SendDailyEmailToSubscriberWorker",
        "args" => subscribers.pluck(:id).map {|id| [id]}
      )
    end
  end
end

Этот рабочий выполняется ежечасно. Каждый раз он будет получать список подписчиков, которые соответствуют необходимым условиям, включая правильный час в их часовом поясе.

Первоначально опубликовано на scottw.com.