Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perform daily action at a specific time based on user's local time zone

My Java application needs to send an email out to all users once per day. I'd like to deliver it at approximately 2AM based on each user's local time. So a user in New York would get the message at 2AM America/New_York time. A user in Los Angeles would get the message at 2AM America/Los_Angeles time. My mailing process would run once each hour of the day.

I plan to use HTML5 geo-location features or IP Address geo-locating to automatically determine each user's time zone during sign up. Of course, the user can modify the timezone manually if these values are incorrect. I intend to store the selected timezone in the User object as a Timezone ID string, such as "America/New_York". This will persist to the database as a string, but this could be changed if necessary.

Things to consider:

  • I need to account for daylight savings time, so the user always receives the email around 2AM. This means I can't just store GMT-8 or similar UTC offsets in the user object. Part of the year Los Angeles is in GMT-7, and other parts it is in GMT-8.
  • Another SO question has answers that suggest storing offset information, but I don't see how that would work as the times in different places can change throughout the year. I'm not going to update all my user objects timezones whenever a timezone change event somewhere in the world happens.
  • I am using JodaTime in my application, so I can take advantage of it if it will help.
  • I'm using Hibernate for database queries and would like to find a solution that could be handled in a hibernate query, without needing to process hundreds of thousands of user records in Java.

I'm using Spring Scheduling cron features (via Quartz) to run a method at 2 minutes past the hour every hour of the day. Right now most of my users are in America/Los_Angeles, so I'm manually forcing all mail to be sent at 2:02AM Pacific time. The following method runs every hour at :02 past the hour, but it manually checks the time and only proceeds if the current time in Los Angeles is in the 2:00 hour. This means users in other places will get the email at a different time than 2AM.

@Scheduled(cron="0 2 * * * *")
public void sendDailyReportEmail()  {
    DateTime now = new DateTime(DateTimeZone.forID("America/Los_Angeles"));
    if (now.getHourOfDay() != 2) {
        log.info("Not 2AM pacific, so skipping email reports");
        return;
    }
    // send email reports
}

So, what I need is to perform a database query that will give me all users that have a local time in the 2:00AM hour, based on the time zone in their User object. Any thoughts on how to accomplish this?

like image 682
Tauren Avatar asked Oct 26 '22 06:10

Tauren


1 Answers

I've given this more thought and have a possible solution. I haven't implemented it yet, but I think it should work. Basically, it involves performing the following query (NOTE: this is MySQL specific):

select * from members where hour(convert_tz(now(),'UTC',timezone)) = 3;

This query selects all members that have a current local time between 3AM and 3:59AM by doing the following:

  1. It gets the current server time using now(), which is a time in UTC since the database server is configured for UTC time.
  2. It then converts that time with the convert_tz() function to the timezone of the member as specified in the timezone column of the members table. Note that the Member class contains a timezone field in the format "America/Los_Angeles".
  3. Next, it checks what hour it is with the hour() function to see if it is 3, for 3AM.
  4. The query then returns all members which meet these requirements and sends email to each of them.

I'm a little concerned about doing this timezone conversion and calculation for every member in the system every hour. I'll have to test things to see what kind of load it puts on the database, but it will probably choke with millions or even 10k's of users.

An alternative that should significantly lighten the load, but isn't quite as elegant is to do the following (NOTE this code is UNTESTED!):

@Scheduled(cron="0 2 * * * *")
public void sendDailyReportEmail()  {

    Query query = session.createQuery(
        "select distinct m.timezone from Member m");
    List<String> timezones = query.list();

    for (String timezone : timezones) {
        DateTime now = new DateTime(DateTimeZone.forID(timezone));
        if (now.getHourOfDay() == 3) {
            Query query2 = session.createQuery(
                "from Member where timezone = :zone");
            query2.setParameter("zone", timezone);
            List<Member> members = query2.list();
            // send email reports
        }
    }
}

This code does this:

  1. Execute method at 2 minutes past the hour, every hour of every day
  2. Query the database for a list of all unique timezones in the members table.
  3. For each timezone found, determine if it is currently in the 3AM hour.
  4. If a timezone is in the 3AM hour, select all of the members in that timezone and send email to them.

This adds a little extra overhead at the beginning of each hour, but requires no per-user timezone processing. Chances are that there will only be a few dozen unique time zones actually used by the system. It's very unlikely that I'll have members in all 500+ timezones.

One last thing. Instead of sending email during the 2AM hour, I moved it to 3AM. This is because when daylight savings time changes in the spring, time changes from 2AM to 3AM. So there is no such thing as 2:02AM, meaning emails wouldn't get sent that day. Furthermore, in the fall, the system could send out 2 emails per user since an hour repeats itself. I need to find out if non-USA timezones change time at different times, because the 3AM hour might be a problem elsewhere.

like image 148
Tauren Avatar answered Nov 03 '22 02:11

Tauren