Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Send Newsletter to Users at 9:00 AM in their Local Time Zone

I am using the following gems:

  • timezone
  • tzinfo

I am trying to have a newsletter sent at user's time zone, so that they get the newsletter at their 9:00:00; not 09:00:00 PST where my server is.

I am storing the time zones by name, like "Pacific Time (US & Canada)". Each user has a column user.time_zone where their specific time zone is stored.

I have built a job that checks every hour for users who could receive the newsletter (i.e. users whose local time is 9:00 AM).

class NewsletterTimezoneJob < ActiveJob::Base
  def self.perform
      users = User.all.gets_newsletter
      users.each do |user|
      d = Date.current
      t = "09:00:00 -0700"
      t = t.to_time
      newsletter_sendtime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
      newsletter_sendtime = newsletter_sendtime.to_s
      if user.is_in_time_around(newsletter_sendtime) 
        NewsletterMailer.send_mailer(user, newsletter_sendtime).deliver_later
      end
    end
  end
end

I have built a method on user that tries to see if the user's local time matches newsletter_sendtime.

I added a +/- 5 minute window to account for slow servers.

User.rb:

def is_in_time_around(timeofday)
   timeofday = timeofday.to_time
   user_zoned =  timeofday.in_time_zone(self.time_zone)
   user_zoned = user_zoned.strftime("%r").to_time
   timeofday = timeofday.strftime("%r").to_time
   if user_zoned >= (timeofday - 5.minutes) && user_zoned <= (timeofday + 5.minutes)
    true
   end
 end

This job and method should run every hour and send to different users in different time zones each hour; and yet it currently only sends to users in "Pacific Time (US & Canada)," each hour and never sends it to any other `users.

like image 363
NothingToSeeHere Avatar asked Dec 14 '22 07:12

NothingToSeeHere


1 Answers

I see a few problems with your approach: the code seems overly complex and you are most probably (as Jon noticed in his comment above) comparing only against 9AM Pacific time. Also, with your approach you need to traverse all users and compare the timezone of each of them - this will lead to bad performance when the users table grows.

I suggest a completely opposite approach, in plain words it's this: Find all timezone names in which the current local time is around 9AM now and then select users in those timezones.

You are using Rails, everything is possible with built-in methods then. With the help of this answer, you can get a given time (e.g. 9AM) in the given timezone (e.g. Pacific time) like this:

ActiveSupport::TimeZone["Pacific Time (US & Canada)"].parse("09:00:00")
# => Tue, 29 Mar 2016 09:00:00 PDT -07:00

The TimeZone module also provides the all method to get all defined timezones. We need to compare the local time (rounded to hours) with the desired time (9AM) in all timezones to select those zones in which the hour is currently 9AM. First let's get the local time rounded to hours:

Time.zone.now
# => Tue, 29 Mar 2016 13:46:25 CEST +02:00
Time.zone.parse("#{Time.zone.now.hour}:00:00")
# => Tue, 29 Mar 2016 13:00:00 CEST +02:00

Next, compare this rounded local time to 9AM in all timezones and get the matching timezone names:

local_time = Time.zone.parse("#{Time.zone.now.hour}:00:00")
ActiveSupport::TimeZone.all.select { |tz| tz.parse("09:00:00") == local_time }.map(&:name)
# => ["Greenland", "Mid-Atlantic"]

This particular result shows that while it is 13 hours (1PM) here in Prague, it is 9 hours (9AM) in the Greenland zone, for example.

Having that, the rest is easy:

class NewsletterTimezoneJob < ActiveJob::Base
  def self.perform
    User.all.gets_newsletter.with_time_zone(zones_with_local_time("09:00")).each do |user|
      NewsletterMailer.send_mailer(user).deliver_later
      # for simplicity, I removed the `newsletter_sendtime` parameter from the mailer call
    end
  end
end

Where the zones_with_local_time is a helper function defined anywhere which returns the timezones in which the local time is the given time:

# if defined in the NewsletterTimezoneJob, it should be a class method
def self.zones_with_local_time(hhmm)
  local_time = Time.zone.parse("#{Time.zone.now.hour}:00:00")
  ActiveSupport::TimeZone.all.select { |tz| tz.parse("#{hhmm}:00") == local_time }.map(&:name)
end

And with_time_zone is just a scope acting upon the timezone condition in User model:

# app/models/user.rb
scope :with_time_zone,
    ->(zones) { where("time_zone in (?)", zones) }

Now all that you have to do is set up a cron job to run every hour e.g. the 1st minute of the hour.

like image 86
Matouš Borák Avatar answered Feb 24 '23 05:02

Matouš Borák