Windows Service with System.Threading.Timer action for an interval within a range

1k views Asked by At

I have a requirement to create a Windows Service (Windows Task Scheduler and third-party libraries/utilities like Quartz.net are not options for me) that runs an action once every hour, on the hour, for 12 hours beginning at 8:00 AM.

I've written code using the System.Threading.Timer that executes fine sometimes, but other times the callback is executed a few milliseconds early. I feel like this will be problematic down the line if I need to add/remove files from a directory. This is my first real foray into Windows Services and timers, so any help would be appreciated.

Here's my code:

private Timer timer;
private DateTime startTime;       // Equals 8:00 AM
private DateTime endTime;
private Int32 interval;           // Equals 1 hour
private Int32 duration;           // Equals 12 hours
private DateTime nextRunTime;
private DateTime lastRunTime;

public ScheduleService()
{
    InitializeComponent();
    InitGlobalsFromConfigFile();  // This assigns values to the globals above.
}

protected override void OnStart(string[] args)
{
    this.WriteToFile("Started {0}.");
    this.StartService();
}

protected override void OnStop()
{
    this.WriteToFile("Stopped {0}.");
    timer.Dispose();
}

private void ScheduleService()
{
    DateTime now = DateTime.Now;
    DateTime runTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, 0);

    endTime = startTime.AddHours(duration);

    if (now < startTime)
    {
        nextRunTime = startTime;
    }
    else if (now >= startTime && now < endTime)
    {
        nextRunTime = runTime.AddHours(interval);
    }
    else
    {
        nextRunTime = startTime.AddDays(1);
    }

    this.WriteToFile(string.Format("Now: {0}", DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.ff tt")));
    this.WriteToFile(string.Format("runTime: {0}", runTime.ToString("MM/dd/yyyy hh:mm:ss.ff tt")));
    this.WriteToFile("Interval: " + interval.ToString() + " hour(s)");
    this.WriteToFile("Duration: " + duration.ToString() + " hour(s)");
    this.WriteToFile("Start Time: " + startTime.ToString());
    this.WriteToFile("End Time: " + endTime.ToString());
    this.WriteToFile(string.Format("Last Run Time: {0}", lastRunTime.ToString("MM/dd/yyyy hh:mm:ss.ff tt")));
    this.WriteToFile(string.Format("Next Run Time: {0}", nextRunTime.ToString("MM/dd/yyyy hh:mm:ss.ff tt")));

    try
    {
        timer = new Timer(new TimerCallback(ScheduleServiceCallback));

        TimeSpan timeSpan = nextRunTime.Subtract(DateTime.Now);
        int dueTime = Convert.ToInt32(timeSpan.TotalMilliseconds);

        this.WriteToFile("dueTime: " + dueTime.ToString());
        this.WriteToFile("");

        timer.Change(dueTime, Timeout.Infinite);
    }
    catch (Exception ex)
    {
        this.WriteToFile("Error on: {0} " + ex.Message + ex.StackTrace);

        using (System.ServiceProcess.ServiceController serviceController = new System.ServiceProcess.ServiceController("ScheduleService"))
        {
            serviceController.Stop();
        }
    }
}

private void ScheduleServiceCallback(object e)
{
    this.WriteToFile("Log: {0}.");
    lastRunTime = nextRunTime;
    this.ScheduleService();
}

Here's a sample from the log:

Started 02/17/2016 09:22:33.59 AM.
[Log entry #1]
Now: 02/17/2016 09:22:33.59 AM
runTime: 02/17/2016 09:00:00.00 AM
Interval: 1 hour(s)
Duration: 12 hour(s)
Start Time: 2/17/2016 7:00:00 AM
End Time: 2/17/2016 7:00:00 PM
Last Run Time: 01/01/0001 12:00:00.00 AM
Next Run Time: 02/17/2016 10:00:00.00 AM
Updated Now: 02/17/2016 09:22:33.59 AM
dueTime: 2246403

[Log entry #2]
Log: 02/17/2016 09:59:59.87 AM.
Now: 02/17/2016 09:59:59.87 AM
runTime: 02/17/2016 09:00:00.00 AM
Interval: 1 hour(s)
Duration: 12 hour(s)
Start Time: 2/17/2016 7:00:00 AM
End Time: 2/17/2016 7:00:00 PM
Last Run Time: 02/17/2016 10:00:00.00 AM
Next Run Time: 02/17/2016 10:00:00.00 AM
Updated Now: 02/17/2016 09:59:59.88 AM
dueTime: 118

[Log entry #3]
Log: 02/17/2016 10:00:00.00 AM.
Now: 02/17/2016 10:00:00.00 AM
runTime: 02/17/2016 10:00:00.00 AM
Interval: 1 hour(s)
Duration: 12 hour(s)
Start Time: 2/17/2016 7:00:00 AM
End Time: 2/17/2016 7:00:00 PM
Last Run Time: 02/17/2016 10:00:00.00 AM
Next Run Time: 02/17/2016 11:00:00.00 AM
Updated Now: 02/17/2016 10:00:00.00 AM
dueTime: 3599994

Notice how in log entry #2 that nextRunTime and the lastRunTime are the same value and Now is 09:59:59.87 AM instead of 10:00:00.00 AM. Then in log entry #3, it appears as though the callback runs again less than a second later. How can I ensure that the callback isn't run twice back to back because the nextRunTime wasn't updated?

Thanks.

2

There are 2 answers

4
spender On

So your timings will never be perfect. You should just accept this truism. Windows isn't a real-time OS.

With regards to your problem, you could write an asynchronous method that takes care of most of your concerns:

async Task ScheduleAsync(Action action, 
                    DateTime startTime, 
                    TimeSpan interval,
                    CancellationToken cancellationToken = default(CancellationToken))
{
    if(cancellationToken.IsCancellationRequested)
    {
        throw new TaskCanceledException();
    }
    var now = DateTime.UtcNow;
    if(now < startTime)
    {
        var delayTime = startTime - now;
        await Task.Delay(delayTime, cancellationToken);
        await ScheduleAsync(action, startTime, interval);
        return;
    }
    action();
    var nextTime = startTime+interval;
    await ScheduleAsync(action, nextTime, interval);
}

and use it like

//hold onto this, and cancel it when you shut down the service.
var cancellationTokenSource = new CancellationTokenSource(); 

ScheduleAsync(() => MyWork(), 
              myDesiredStartTimeUtc, 
              TimeSpan.FromHours(1), 
              cancellationTokenSource.Token);
0
Andrew Jay On

I agree with spender, you will never get accurate results this way. Part of the reason is because the event sinks kick in at approximately the right time, but, whether there are other threads, or other priorities, that get the CPU's attention is something you cannot control.

As suggested, you could use an asynchronous method, but, that also relies on a message sink kicking in at the right time. If you are after absolute precision, you'll need to launch your own thread which loops until the timing is where you want to execute your procedure. and even then, Windows will preemptively multitask in the middle of your operation if it wants to. So even though kicking in occurs at the right moment, there's no guarantee that your code will not be interrupted in the middle.

A better solution is to design your app with the idea that precision is not guaranteed - only approximate, and can be affected by other applications and hardware configurations. Depending on what you're doing, you might need to set up locks or mutexes, threads, and so on.