4

I just thought about the way I solved a service which runs a task every 24 hours and how DST could possibly hurt it.

To run the task daily, I used a System.Threading.Timer with a period of 24 hours, like this:

_timer = new System.Threading.Timer(TimerCallback, null,
    requiredTime - DateTime.Now, new TimeSpan(24, 0, 0));

Suddenly thinking about daylight saving time correction I had three thoughts:

  • DST is useless and we should get rid of it.
  • Does the Timer handle this correctly? I think not - it simply waits 24 hours - no matter if the clock has changed. It just waits it's specified period and then calls the TimerCallback again.
  • DST is useless and we should really get rid of it.

Is my second thought correct? If yes, what can I do to avoid this problem? The task must not be run one hour later or earlier if a DST correction has happened.

Ray
  • 7,940
  • 7
  • 58
  • 90
  • 1
    This might solve your problem: http://stackoverflow.com/questions/3243348/how-to-call-a-method-daily-at-specific-time-in-c – meilke Sep 27 '13 at 08:41
  • 1
    I would never trust such a long duration timer anyway. I would prefer to store the date/time that I want it to next run, and then use a short interval timer (maybe 10 seconds) to check if that date/time has been reached, and then run the code – musefan Sep 27 '13 at 08:51
  • @musefan: We had an older service doing it like that before, but I was not allowed to do it like that since it was weirdly seen as a "waste of resources to check so often". – Ray Sep 27 '13 at 09:12

3 Answers3

2

SystemEvents.TimeChanged will tell you if the clock has been changed by the user. It doesn't fire on a regular basis with each tick of the clock. So it's not useful for scheduling events, except that you might recalculate timers when it occurs. (I think it also will fire if the system syncs with a time server, but I'm not positive about that.)

If you try to calculate the difference in wall time as Hans suggested, be careful. You can't just use DateTime. Observe:

// With time zone set for US Pacific time, there should only be 23 hours
// between these two points
DateTime a = new DateTime(2013, 03, 10, 0, 0, 0, DateTimeKind.Local);
DateTime b = new DateTime(2013, 03, 11, 0, 0, 0, DateTimeKind.Local);
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 24

Even with local kind specified, it does not take DST into account.

If you're going to take this approach, you must use DateTimeOffset types.

DateTimeOffset a = new DateTimeOffset(2013, 03, 10, 0, 0, 0, TimeSpan.FromHours(-8));
DateTimeOffset b = new DateTimeOffset(2013, 03, 11, 0, 0, 0, TimeSpan.FromHours(-7));
TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 23

You will probably need to gather your inputs with something like this:

DateTime today = DateTime.Today;      // today at midnight
DateTime tomorrow = today.AddDays(1); // tomorrow at midnight

TimeZoneInfo tz = TimeZoneInfo.Local;
DateTimeOffset a = new DateTimeOffset(today, tz.GetUtcOffset(today));
DateTimeOffset b = new DateTimeOffset(tomorrow, tz.GetUtcOffset(tomorrow));

TimeSpan t = b - a;
Debug.WriteLine(t.TotalHours);  // 23, 24, or 25 depending on DST

However - it is probably not such a great idea to set a single timer to run for such a long period of time. Not only might the clock change by the user or a system time sync, but the application or system might shut down or restart. Also, if you have many of these tasks, you could end up with a lot of resources consumed just to sit idle.

One idea would be to keep a list with the next time to fire events. In your application, you would fire a short-lived polling timer (say once per minute or so), and compare the current time against the values in the list to know if you really need to do anything.

Another idea would be a slight variation of that. Instead of short polling, you would keep your list sorted, and set a time for the delay until the next event, with some maximum delay (perhaps an hour). Again, when the timer fires, you see if there's anything to do, or if you need to set another timer delay. You have to run this when your application starts, and any time a new event is scheduled.

With either of these approaches, you should schedule using either a DateTimeOffset, or the equivalent UTC DateTime. Otherwise, you risk firing at the wrong time for DST - or even firing twice during the DST fall-back transition.

If all of that sounds too complicated, then you might try a pre-built solution, such as Quartz.net. In particular, read this section of their FAQ.

Regarding your first and third bullet points, I agree wholeheartedly - but it is never going to happen. Even if it does we will still have to consider all of the many years of history that it did occur. If you haven't watched this video already, you should.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Thanks for the explanations and observations! Actually, I previously planned to use the TimeChanged event to recalculate the milliseconds my Timer has to wait before raising the event, not to check the time every second. – Ray Sep 30 '13 at 16:02
1

If anyone got an answer without the need for such a message loop

The SystemEvents class already provides its own hidden window and dispatcher loop if you don't provide one yourself. It may seem magical that it knows that you have one, but it goes by a well established contract in Windows programming. It uses Thread.GetApartmentState() on the thread you use to add the event handler. It that returns STA, like it does in any GUI app, then it trusts that your program implements the STA contract and pumps a message loop and SystemEvents doesn't do anything special.

If it returns MTA, like it does in console mode app or service then it assumes that your program doesn't have a dispatcher loop and supports threading like MTA promises and starts up a new thread. You can see that thread back in the debugger's Debug + Windows + Threads window, the thread's name is ".NET SystemEvents". That thread creates a hidden window and pumps a loop, equivalent to Application.Run(). You can see that window back as well with Spy++, its name is ".NET-BroadcastEventWindow.xxxx".

Notable is that this behavior is responsible for a lot of GUI programs failing badly, typically on the UserPreferenceChanged event in a GUI app and typically when the user unlocks the workstation. This happens when the program creates a splash screen on a worker thread that isn't STA. SystemEvents assumes that the program needs help and creates that helper thread. Which now fires the events on the wrong thread, one that the program uses to update its UI. It also goes wrong if it an STA thread but that thread is allowed to exit, SystemEvents tries to fire the event on a thread that isn't there anymore and falls back to a TP thread if that failed. That's all very, very bad in a GUI app, deadlock is a common outcome.

But of course in your case you very much like the way SystemEvents class works, you really want that helper thread so you don't have to write it yourself. Just keep in mind that the TimeChanged event fires on a completely arbitrary thread that is unrelated to any threads that your service started, so properly interlocking is certainly required. Same problem you've got with your own btw.

Do consider the simple solution. You just need to calculate the timer interval from the difference between tomorrow's wall clock time and today's time. In other words, absolute time, not incremental. Which will produce 23 or 25 hours if it spans a DST change.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
0

I found my solution, even though it's probably not the best one for a Windows service.

I now use SystemEvents.TimeChanged to get an event when the system time has been changed (surprise), however, this event requires a message loop like the one of a Form.

The answer of this question helped me implementing the message loop in a Windows service: https://stackoverflow.com/a/9807963/777985

If anyone got an answer without the need for such a message loop, I'll accept it!

Community
  • 1
  • 1
Ray
  • 7,940
  • 7
  • 58
  • 90