According to the documentation the maximum dueTime value is 4294967294 milliseconds. But if you set the timer to this value it fires a callback immediately.
Run the below program and click “c” – the callback will fire immediately, click “o”, it will fire after 49 days, click “v” it will fire after 1s. Is this a bug or am I doing something wrong?
using System;
using System.Threading;
namespace Test
{
class Program
{
private static bool isRunning;
private static Timer t;
static void Main(string[] args)
{
t = new Timer(OnTimerCallback, null, Timeout.Infinite, Timeout.Infinite);
isRunning = true;
while (isRunning)
{
var command = Console.ReadKey();
switch (command.KeyChar)
{
case 'q':
isRunning = false;
break;
case 'c':
t.Change(TimeSpan.FromMilliseconds(4294967294), TimeSpan.FromMilliseconds(-1));
break;
case 'o':
t.Change(TimeSpan.FromMilliseconds(4294000000), TimeSpan.FromMilliseconds(-1));
break;
case 'v':
t.Change(TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(-1));
break;
}
}
Environment.Exit(0);
}
private static void OnTimerCallback(object x)
{
Console.WriteLine("Timer callback");
t.Change(TimeSpan.FromMilliseconds(4294967294), TimeSpan.FromMilliseconds(-1));
}
}
}
UPDATE
If you set timer to the max dueTime value in a callback it will not fire immediatelly and will work as expected 🙂
This looks like a race in the CLR’s implementation of the timer. I need to wave my hands at the exact cause, I don’t really understand how it can go wrong. At least from looking at the SSCLI20 source code (clr\src\vm\win32threadpool.cpp), there’s a good chance that this is no longer accurate with the currently shipping code.
I can repro the problem easily by setting a breakpoint at the Change() call but not when I let the code run without a break. It behaves like it calculates the due time from the last observed value returned by GetTickCount() instead of the current value. Adding 0xfffffffe to that stale value then calculates a time that already expired. The tick count only has 32-bits of resolution. I think the real race is in the calculation of how long to SleepEx() to wait for the next time due event, but that’s a guess.
It is possible that this race can occur also if you don’t use the debugger to pause the timer thread. Although it would probably be rare. I’d have to recommend you stay away from any value close to 0xfffffffe to be sure this won’t happen. No hardship there, sleeping for 24 days is not less efficient than sleeping for 49 days 🙂 Implement longer intervals by counting sleep periods yourself.