Source code for Service Framework posts.
In the
previous post I covered the ThreadServiceBase class that serves as the foundation class for services whose main work is a loop that runs in a thread for the life of the service. This final post in the series covers two classes for services that are Timer based.
TimerServiceBase
This is the base class for services whose work is a relatively short duration method that is executed repeatedly by regular, periodic firings of a System.Timers.Timer event. This is much better than having a long running method with a loop and a Thread.Sleep() call within the loop. Using the timer allows the service to be much more responsive to requests from the service control manager to stop or pause.
Since it's possible that the work method might at times take longer to finish than the timer interval, you'll need to decide what the appropriate behavior is in that case. If that occurs, the timer will call the method again on a second thread. TimerServiceBase has an AllowOverlappingWork property to specify the behavior you want. If you set it to true, then your method will be called each time the timer fires, even if one or more previous executions of the method have not finished. Each execution of the work method will be on a separate thread. If you set AllowOverlappingWork to false, then TimerServiceBase will skip calling your work method if it is still running from the previous timer event. If your work method may execute for longer than a few seconds, you should make use of the passed in abortFlag and continueFlag to ensure your service is responsive to requests originating from the service control manager.
This following code shows a simple example of a service that descends from TimerServiceBase.
// Copyright ©2012 SoftWx, Inc.
// Released under the MIT License the text of which appears at the end of this file.
// <authors> Steve Hatchett
using System;
using System.Threading;
using SoftWx.ServiceProcess;
namespace ServiceTest {
partial class ExampleTimerService : TimerServiceBase {
public ExampleTimerService() {
InitializeComponent();
}
private void InitializeComponent() {
//
// ExampleTimerService
//
this.AllowOverlappingWork = true;
this.CanPauseAndContinue = true;
this.CanShutdown = true;
this.Interval = 2000;
this.ServiceName = "ExampleTimerService";
}
protected override void OnWork(SignalToken abortFlag, SignalToken continueFlag) {
DateTime now = DateTime.Now;
// show we've entered this invocation of OnWork
string timeMsg = "OnWork time=" + now.ToString("hh:mm:ss ffff");
WriteLogEntry(timeMsg, System.Diagnostics.EventLogEntryType.Information);
// simulate doing some work
Thread.Sleep(800);
// example of pause handling that stays within the method, rather than just exiting the method on pause
if (!continueFlag.IsSet) {
WaitHandle.WaitAny(new WaitHandle[] { continueFlag.WaitHandle, abortFlag.WaitHandle});
}
// show we're at he end of this invocation of OnWork
WriteLogEntry(" exiting " + timeMsg, System.Diagnostics.EventLogEntryType.Information);
}
}
}
In the code for TimerServiceBase, you can see how it handles the basic housekeeping of starting/stopping and pausing/continuing the service, and complete management of the underlying Timer.
// Copyright ©2012 SoftWx, Inc.
// Released under the MIT License the text of which appears at the end of this file.
// version 1.3 - 7/25/2012
// <authors> Steve Hatchett
using System;
using System.Diagnostics;
using System.Threading;
namespace SoftWx.ServiceProcess {
/// <summary>
/// Service base class that uses a Timer to execute work at periodic intervals.
/// </summary>
public class TimerServiceBase : ExecutableServiceBase {
private readonly SignalSource abortFlag = new SignalSource(); // flag used to signal worker method we need to stop
private readonly SignalSource continueFlag = new SignalSource(); // flag used to signal worker method we can continue (i.e. not paused)
private System.Timers.Timer timer = new System.Timers.Timer(); // service timer - calls work method in a pooled thread
private volatile bool allowOverlappingWork = false; // specifies whether work method will execute on timer fire when the method from previous fire still hasn't finished
private int workingCount = 0; // count of work methods currently exectuting (will be 0 or 1 if allowOverlappingWork is false, otherwise 0..n)
/// <summary> Create a new instance of TimerServiceBase.</summary>
public TimerServiceBase() {
this.abortFlag.Set(false);
this.continueFlag.Set(true);
// initialize the Timer
this.timer.Elapsed += TimerFired;
this.timer.AutoReset = true;
}
/// <summary>
/// Gets or sets the interval in milliseconds at which to call the OnWork method.
/// </summary>
public double Interval {
get { return this.timer.Interval; }
set { this.timer.Interval = value; }
}
/// <summary>
/// Gets or sets the value indicating whether the OnWork method will be executed when
/// the timer fires before the OnWork method from the previous timer event has completed.
/// </summary>
public bool AllowOverlappingWork {
get { return this.allowOverlappingWork; }
set { this.allowOverlappingWork = value; }
}
/// <summary>
/// Performs the service's work upon the firing of the timer event.
/// </summary>
/// <param name="abortFlag">Signals that the service must stop as soon as possible.</param>
/// <param name="continueFlag">Signals that the service can continue (i.e. it is not paused).</param>
protected virtual void OnWork(SignalToken abortFlag, SignalToken continueFlag) { }
/// <summary>Start the service.</summary>
protected override void OnStart(string[] args) {
this.timer.Start();
}
/// <summary>Stop the service.</summary>
protected override void OnStop() {
// indicate that we are stopping - this will signal
// any worker code currently executing, and also
// prevent our TimerFired from doing any work if
// it gets called any more
abortFlag.Set(true);
var ticks = Environment.TickCount;
// stop the Timer from firing - note that the Stop is queued
// up for execution on another thread. Because of that, it's
// possible that the timer could still fire after we call Stop,
// but if it does, work will be skipped because we've set the
// abortFlag above.
this.timer.Stop();
// wait for work in the TimerFired to finish
int seconds = 0;
bool cleanExit = true;
while (Interlocked.CompareExchange(ref this.workingCount, -1, 0) != 0) {
if (seconds >= 10) {
seconds = 0;
var elapsed = Environment.TickCount - ticks;
if (elapsed >= MaxWaitSeconds * 1000) {
cleanExit = false;
break;
}
if (IsRunningAsService) {
this.RequestAdditionalTime(elapsed + (15 * 1000));
}
WriteLogEntry("Requesting additional time to stop", EventLogEntryType.Information);
}
Thread.Sleep(1000); // normally like to avoid Sleep, but this only executes rarely, during service stopping
seconds++;
}
ticks = Environment.TickCount - ticks;
if (cleanExit) {
WriteLogEntry("Worker methods exited cleanly in " + ticks + " milliseconds", EventLogEntryType.Information);
} else {
WriteLogEntry("Worker methods did not exit cleanly within " + ticks + " milliseconds", EventLogEntryType.Information);
}
}
/// <summary>Pause the service.</summary>
protected override void OnPause() {
this.continueFlag.Set(false);
this.timer.Stop();
}
/// <summary>Continue the paused service.</summary>
protected override void OnContinue() {
this.continueFlag.Set(true);
this.timer.Start();
}
/// <summary>Releases all resources used by the TimerServiceBase.</summary>
protected override void Dispose(bool disposing) {
this.timer.Dispose();
}
/// <summary>Handles the timer event.</summary>
private void TimerFired(object sender, System.Timers.ElapsedEventArgs e) {
// When overlapping work execution is not allowed, ensure that only one
// execution of work is going on at one time. If work is still ongoing
// when the timer fires, processing is skipped.
bool allowOverlapping = this.allowOverlappingWork;
if (allowOverlapping || (Interlocked.CompareExchange(ref this.workingCount, 1, 0) == 0)) {
if (allowOverlapping) Interlocked.Increment(ref this.workingCount);
try {
// if we're not currently stopping, do the work
if (!abortFlag.IsSet) OnWork(this.abortFlag.Token, this.continueFlag.Token);
}
catch (OperationCanceledException) {
// quietly swallow OperationCancelledException as it's an acceptable response to abortFlag signalling need to abort.
}
finally {
Interlocked.Decrement(ref this.workingCount);
}
}
}
}
}
ScheduleServiceBase
This is the base class for services whose work is a relatively short duration method that is executed at a scheduled time. Like TimerServiceBase, it uses a System.Timers.Timer under the covers. Rather than call your work method at regular, periodic intervals, ScheduleServiceBase calls the work method at a scheduled DateTime. Upon exit, your work method returns the next DateTime that it wishes to be called again. On service start, the work method is called almost immediately after start to get things going. This class could be used for situations where you have a list of jobs, with each needing execution at different periodic intervals. Rather than create timers for each one, you can maintiain a list of the jobs, along with when they next need to run. When you enter the work method, you run through the list, executing all the jobs whose time has arrived. When you exit the work method, you return the time of the job with the soonest next execution time.
This following code shows a simple example of a service that descends from ScheduleServiceBase.
// Copyright ©2012 SoftWx, Inc.
// Released under the MIT License the text of which appears at the end of this file.
// <authors> Steve Hatchett
using System;
using System.Threading;
using SoftWx.ServiceProcess;
namespace ServiceTest {
partial class ExampleScheduleService : ScheduleServiceBase {
private Periodic[] periodics;
public ExampleScheduleService() {
InitializeComponent();
// set up three periodic events, each occurring at different periods
this.periodics = new Periodic[3];
this.periodics[0] = new Periodic(10, DateTime.Now);
this.periodics[1] = new Periodic(15, DateTime.Now);
this.periodics[2] = new Periodic(25, DateTime.Now);
}
private void InitializeComponent() {
//
// ExampleScheduleService
//
this.CanPauseAndContinue = true;
this.CanShutdown = true;
this.ServiceName = "ExampleScheduleService";
}
protected override DateTime OnWork(SignalToken abortFlag, SignalToken continueFlag) {
// show that scheduled work started
DateTime time = DateTime.Now;
WriteLogEntry("OnWork time=" + time.ToString("hh:mm:ss"),
System.Diagnostics.EventLogEntryType.Information);
// loop through all scheduled events, simulate processing the ones
// that are due (or will be due within a time so short that it's near the
// resolution of the system timer), and determine next scheduled time
DateTime next = DateTime.MaxValue;
foreach(var periodic in this.periodics) {
DateTime now = DateTime.Now;
if (periodic.Next <= now.AddMilliseconds(15)) {
WriteLogEntry(" processing " + periodic.IntervalSecs + " second periodic",
System.Diagnostics.EventLogEntryType.Information);
// set up next time for this event, ensuring next time is in the future.
// We need the future check because this could be the first call to
// OnWork after a long service Pause.
do {
periodic.Next = periodic.Next.AddSeconds(periodic.IntervalSecs);
} while (periodic.Next < now);
}
// next scheduled time for work is whichever next event time is the soonest
if (periodic.Next < next) next = periodic.Next;
}
WriteLogEntry(" next scheduled time=" + next.ToString("hh:mm:ss"), System.Diagnostics.EventLogEntryType.Information);
return next;
}
private class Periodic {
public int IntervalSecs;
public DateTime Next;
public Periodic(int interval, DateTime next) {
IntervalSecs = interval;
Next = next;
}
}
}
}
In the code for ScheduleServiceBase, you can see how it handles the basic housekeeping of starting/stopping and pausing/continuing the service, and complete management of the underlying Timer. One big difference from TimerServiceBase is that the timer runs with AutoReset set to false. So the whole issue of the timer event executing the work method multiple times cannot arise. As soon as the work method is called, the timer is off. When the work method exits, it returns the next scheduled time, and with that information, ScheduleServiceBase can set the new timer Interval, and start the timer again.
// Copyright ©2012 SoftWx, Inc.
// Released under the MIT License the text of which appears at the end of this file.
// version 1.3 - 7/25/2012
// <authors> Steve Hatchett
using System;
using System.Diagnostics;
using System.Threading;
namespace SoftWx.ServiceProcess {
/// <summary>
/// Service base class that uses a Timer to execute work at the next scheduled time.
/// </summary>
public class ScheduleServiceBase : ExecutableServiceBase {
private readonly SignalSource abortFlag = new SignalSource(); // flag used to signal worker method we need to stop
private readonly SignalSource continueFlag = new SignalSource(); // flag used to signal worker method we can continue (i.e. not paused)
private System.Timers.Timer timer = new System.Timers.Timer(); // service timer - calls work method in a pooled thread
private int workingCount = 0; // count of work methods currently exectuting (will be 0 or 1)
private DateTime nextFiring; // the desired DateTime of the next scheduled timer event
/// <summary>Create a new instance of ScheduleServiceBase.</summary>
public ScheduleServiceBase() {
this.abortFlag.Set(false);
this.continueFlag.Set(true);
// initialize the Timer
this.timer.Elapsed += TimerFired;
this.timer.AutoReset = false;
}
/// <summary>
/// Performs the service's work upon the firing of the timer event.
/// </summary>
/// <param name="abortFlag">Signals that the service must stop as soon as possible.</param>
/// <param name="continueFlag">Signals that the service can continue (i.e. it is not paused).</param>
protected virtual DateTime OnWork(SignalToken abortFlag, SignalToken continueFlag) {
return DateTime.Now.AddSeconds(1);
}
/// <summary>Start the service.</summary>
protected override void OnStart(string[] args) {
this.timer.Interval = 100;
this.timer.Start();
}
/// <summary>Stop the service.</summary>
protected override void OnStop() {
// indicate that we are stopping - this will signal
// any worker code currently executing, and also
// prevent our TimerFired from doing any work if
// it gets called any more
abortFlag.Set(true);
var ticks = Environment.TickCount;
// stop the Timer from firing - note that the Stop is queued
// up for execution on another thread. Because of that, it's
// possible that the timer could still fire after we call Stop,
// but if it does, work will be skipped because we've set the
// abortFlag above.
this.timer.Stop();
// wait for work in the TimerFired to finish
int seconds = 0;
bool cleanExit = true;
while (Interlocked.CompareExchange(ref this.workingCount, -1, 0) != 0) {
if (seconds >= 10) {
seconds = 0;
var elapsed = Environment.TickCount - ticks;
if (elapsed >= MaxWaitSeconds * 1000) {
cleanExit = false;
break;
}
if (IsRunningAsService) {
this.RequestAdditionalTime(elapsed + (15 * 1000));
}
WriteLogEntry("Requesting additional time to stop", EventLogEntryType.Information);
}
Thread.Sleep(1000); // normally like to avoid Sleep, but this only executes rarely, during service stopping
seconds++;
}
ticks = Environment.TickCount - ticks;
if (cleanExit) {
WriteLogEntry("Worker method exited cleanly in " + ticks + " milliseconds", EventLogEntryType.Information);
} else {
WriteLogEntry("Worker method did not exit cleanly within " + ticks + " milliseconds", EventLogEntryType.Information);
}
}
/// <summary>Pause the service.</summary>
protected override void OnPause() {
this.continueFlag.Set(false);
this.timer.Stop();
}
/// <summary>Continue the paused service.</summary>
protected override void OnContinue() {
if (Interlocked.CompareExchange(ref this.workingCount, 0, 0) == 0) {
// not paused in the middle of the OnWork method
this.continueFlag.Set(true);
SetInterval();
this.timer.Start();
} else {
// paused in the middle of the OnWork method
this.continueFlag.Set(true);
}
}
/// <summary>Releases all resources used by the ScheduleServiceBase.</summary>
protected override void Dispose(bool disposing) {
this.timer.Dispose();
}
private void TimerFired(object sender, System.Timers.ElapsedEventArgs e) {
// note that since timer has AutoReset=false, it should not be possible for this
// method to get called a second time before the previous invocation has finished.
// We still used the interloced workingCount though, so we can stop the service
// cleanly.
if (Interlocked.CompareExchange(ref this.workingCount, 1, 0) == 0) {
try {
// if we're not currently stopping, do the work
if (!abortFlag.IsSet) {
this.nextFiring = OnWork(this.abortFlag.Token, this.continueFlag.Token);
}
}
catch (OperationCanceledException) {
// quietly swallow OperationCancelledException as it's an acceptable response to abortFlag signalling need to abort.
} finally {
Interlocked.Decrement(ref this.workingCount);
}
}
SetInterval();
this.timer.Start();
}
private void SetInterval() {
double msecsToNextFire = (this.nextFiring - DateTime.Now).TotalMilliseconds;
if (msecsToNextFire <= 0) msecsToNextFire = 15;
this.timer.Interval = msecsToNextFire;
}
}
}