Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compensating for TimeZone offsets while running Quartz jobs

I have a bit of a unique problem in that my quartz job scheduler implementation which build using the quartz.net code base ver 2.0.1, recently discovered that the time zone and utc offsets are being ignored while running and executing jobs. This is an inherit bug in this version of quartz.net and updating to version 2.1.1 is out of scope right now, so I wrote a quick and dirty way of calculating the offset using this algorithm:

(ServerTime - ClientTime) - TargetTime = New_TargetTime_With_Offset

The idea here is the client, who is say in NYC, makes a job at 5:00pm and wants it to run at 2:00pm. The Server (where this app and the job server runs) current time is 2:00pm, so we take the client time and server time to get the offset and apply that offset to the target time, which is the time the job should run.

My question is that this feels like a round about way of calculating the dates, but seems like it would do the job. Is there a better / more reliable way of doing this date math? Also this seems to be buggy in edge cases, what am I missing?

Here is the implementation:

    /// <summary>
    /// Takes three dates and returns the adjusted hour value.
    /// All date data is ignored except for the hour. 
    /// </summary>
    /// <param name="serverTime"></param>
    /// <param name="clientTime"></param>
    /// <param name="targetTime"></param>
    /// <returns></returns>
    private static DateTime OutputDate(DateTime serverTime, DateTime clientTime, DateTime targetTime)
    {
        DateTime? output = null;
        TimeSpan? dateDiff;

        if (serverTime < clientTime)
        {
            dateDiff = (clientTime - serverTime);
        }
        else
        {
            dateDiff = (serverTime - clientTime);
        }

        output = (targetTime - dateDiff);

        return output.Value;
    }

and here are two examples of leveraging it:

    /// <summary>
    /// -5 Offset (NYC)
    /// </summary>
    /// <returns></returns>
    private static Int32 ZoneTest001()
    {
        var targetTime = DateTime.Parse("6/12/2013 5:00PM");  // NYC (est) [The time the report should be received in NYC]
        var clientTime = DateTime.Parse("6/12/2013 5:00PM");   // NYC (est) [The time of the client when the report is created (now) ]
        var serverTime = DateTime.Parse("6/12/2013 2:00PM");  // SEA (pst) [The time of the app server when the report is created (now) ]

        //
        // NYC Wants to send a report at 5:00pm EST
        // The server time will be 2:00pm PST
        // The client time will be 5:00pm EST

        double outputHour = 0;   // should end up as 2:00pm PST

        //
        // 1) Get offset (diff between client & server time)
        // 2) Subtract offset from "targetTime"
        // 3) Set the report to be sent at the new hour value.

        outputHour = OutputDate(serverTime, clientTime, targetTime).Hour;

        return (int)outputHour;

    }

    /// <summary>
    /// +5 Offset (India)
    /// </summary>
    /// <returns></returns>
    private static Int32 ZoneTest002()
    {
        var targetTime = DateTime.Parse("6/12/2013 5:00PM"); // IND (ist)
        var clientTime = DateTime.Parse("6/12/2013 9:00AM");  // IND (ist)
        var serverTime = DateTime.Parse("6/12/2013 2:00PM"); // SEA (pst)

        //
        // INDIA Wants to send a report at 5:00pm IST
        // The server time will be 2:00pm PST
        // The client time will be 9:00am PST

        double outputHour = 0;   // should end up as 2:00pm PST
        outputHour = OutputDate(serverTime, clientTime, targetTime).Hour;

        return (int)outputHour;

    }

Thank you.

like image 622
Shawn J. Molloy Avatar asked Dec 16 '22 09:12

Shawn J. Molloy


1 Answers

You're missing quite a bit actually.

  1. Time zone offsets are not constant. Many time zones switch offset for daylight saving time (a.k.a. "Summer Time"). So when you are calculating the offsets based on the "now" of each location (server, client, target), that is only reflective of the current offset.

  2. In any time zone that has DST, there is a missing hour when the clocks roll forward, and a duplicated hour when the clocks roll backward. If you are dealing with local time, and a scheduled event falls into an ambiguous time period, you cannot be certain about what actual moment to run it in. In order to disambiguate, you either need to be told what the corresponding offset is, or you need to deal in UTC.

  3. If you are going to be converting from one time zone to another, you need to deal in time zones, not just their offsets. In .Net, you can use the built in Windows time zone database and the corresponding TimeZoneInfo class. Alternatively, you could use the more standard IANA time zone database, with a library such as Noda Time.

  4. When working with DateTime types, be very careful about what the .Kind property is set to. Many functions have different behaviors when working with different kinds. It would be much safer and more useful to use the DateTimeOffset type instead.

  5. You really should never be dependent on the time zone of the server that your code is running on. Server code should be time zone neutral. The only place where you should ever involve DateTime.Now or TimeZoneInfo.Local or any similar functionality is in desktop or mobile applications. Server code should only depend on UTC.

  6. I don't really see why you have nullable values inside your OutputDate method. There's no reason for that. Also, you are effectively taking the absolute value of the difference - which is dropping directionality. Time zone offsets are indeed directional, so you are probably going to get invalid results with your current implementation.

  7. I looked at the Quartz.net API, and it would seem that they prefer you schedule event times in UTC. This is a very good thing, since there is no ambiguity concern with UTC. From the Quartz.Net Tutorial, the trigger.StartTimeUtc is clearly a UTC DateTime. Since you said you couldn't use the latest version, I also checked their older 1.0 API documentation and it is still UTC there.

    Update: Quartz.Net 2.5 and greater handles time zones better. See #317 for details.

Let's put this all together for your example use case. A customer in NYC wants to run a job at 2:00PM in his local time zone. The time zone of the server is irrelevant, and so is the time that he created the job.

// June 6, 2013 2:00 PM  Kind = Unspecified
DateTime dt = new DateTime(2013, 6, 13, 14, 0, 0);

// This is the correct Windows time zone for New York
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

// Get the time in UTC - The kind matters here.
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);

// Feed it to a quartz event trigger
trigger.StartTimeUtc = utc;

When I converted the time to UTC in the third step, if the time was ambiguous, .Net will assume that you wanted the standard time instead of the daylight time. If you want to be more specific, you'd have to check for ambiguity and then ask your user which of the two local times they wanted. Then you would have to use a DateTimeOffset to distinguish between them. If you think you might need this, let me know and I can produce a sample, but it is a bit more complicated.

And just for good measure, if you wanted to use IANA time zones with Noda Time, it would look like this:

LocalDateTime ldt = new LocalDateTime(2013, 6, 13, 14, 0);
DateTimeZone tz = DateTimeZoneProviders.Tzdb["America/New_York"];
ZonedDateTime zdt = ldt.InZoneLeniently(tz);
trigger.StartTimeUtc = zdt.ToDateTimeUtc();

The InZoneLeniently method will give the same behavior as the above code. But there are other options you can specify if desired.

Oh, and not that it matters, but India is +5:30, not +5

like image 52
Matt Johnson-Pint Avatar answered Jan 13 '23 10:01

Matt Johnson-Pint