BaseServer: Abstract class for timer based jobs
In one of my systems I’m using background process that hosts jobs that import and update some data and communicate with external services. These jobs are running after some interval or on specific moments. I generalized the common base part of these jobs so I can handle them as one in different background processes like Azure worker roles and Windows services. This blog post introduces my work and shows real-life implentation of jobs base.
Here are some examples of tasks that my background service does:
- import telemetric data from external service,
- generate invoices,
- send out notifications using e-mail and SMS,
- update debtors list.
All these tasks happen continuously after some time interval.
There are also some problems to solve:
- how to handle all these jobs same way in hosting processes?
- how to avoid overlap of timer callbacks when some task takes more time than expected?
Base server
My first implementation was simple base server that handles triggering of actual job. As every different job may have different interval I added interval as a abstract property to base server class. Base server uses timer to trigger Run() method that is implemented in inherited class. Run() method is the place where actual job is done.
NB! I added some console output to code to make it easier for you to test my code in Visual Studio. For real-life implementations you should remove console output or at least find some better way to log what base server is doing.
abstract class BaseServer : IDisposable
{
private object _lock = new object();
private Timer _timer;
public abstract int Interval { get; }
protected abstract void Run();
public void Start()
{
if (_timer == null)
_timer = new Timer(TimerCallback, null, Interval, Interval);
else
_timer.Change(Interval, Interval);
}
private void TimerCallback(object state)
{
if (Monitor.TryEnter(_lock))
{
Console.WriteLine("Lock entered");
try
{
Run();
}
finally
{
Monitor.Exit(_lock);
Console.WriteLine("Lock exited");
}
}
else
{
Console.WriteLine("Lock is on");
}
}
public void Stop()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
public void Dispose()
{
if (_timer == null)
return;
_timer.Dispose();
_timer = null;
}
}
I’m using Monitor class to find out if some of previous timer callbacks is running. If there is previous callback still alive then current one just exits because it can’t aquire lock on locker attribute. If there’s no lock then callback calls Run() method.
Sliding interval
For some jobs we need sliding time interval. Sliding interval means that timer is not triggering callback constantly like in code above but stops timer when Run() method is called and waits for interval amount of time before invoking callback again.
abstract class BaseServer : IDisposable
{
private object _lock = new object();
private Timer _timer;
public abstract int Interval { get; }
public abstract bool UseSlidingInterval { get; }
protected abstract void Run();
public void Start()
{
if (_timer == null)
_timer = new Timer(TimerCallback, null, Interval, Interval);
else
_timer.Change(Interval, Interval);
}
private void TimerCallback(object state)
{
if (Monitor.TryEnter(_lock))
{
Console.WriteLine("Lock entered");
try
{
if (UseSlidingInterval)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
Run();
}
finally
{
if(UseSlidingInterval)
{
_timer.Change(Interval, Interval);
}
Monitor.Exit(_lock);
Console.WriteLine("Lock exited");
}
}
else
{
Console.WriteLine("Lock is on");
}
}
public void Stop()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
public void Dispose()
{
if (_timer == null)
return;
_timer.Dispose();
_timer = null;
}
}
When sliding interval is used we stop timer before calling Run() method. When Run() method gets job done then timer is activated again and before calling callback method it waits for time given by Interval property.
Running on given time
Invoice are sent out once per month and it happens automatically on fifth day of every month. For this type of jobs I made it possible for server implementation to tell next run time to base server.
abstract class BaseServer : IDisposable
{
private object _lock = new object();
private Timer _timer;
public DateTime LastRunTime { get; private set; }
public abstract bool SupportsInterval { get; }
public abstract int Interval { get; }
public abstract bool UseSlidingInterval { get; }
public abstract DateTime NextRunTime { get; }
protected abstract void Run();
public void Start()
{
if (SupportsInterval)
{
if (_timer == null)
_timer = new Timer(TimerCallback, null, Interval, Interval);
else
_timer.Change(Interval, Interval);
}
else
{
Console.WriteLine("Running at " + NextRunTime);
var interval = GetNextRunTimeInterval();
_timer = new Timer(TimerCallback, null, interval, Timeout.Infinite);
}
}
private int GetNextRunTimeInterval()
{
var span = NextRunTime - DateTime.Now;
var interval = int.MaxValue;
if (span.TotalMilliseconds < int.MaxValue)
interval = (int)span.TotalMilliseconds;
if (interval < 0)
throw new Exception("Timer interval cannot be less than zero");
return interval;
}
private void TimerCallback(object state)
{
if (Monitor.TryEnter(_lock))
{
Console.WriteLine("Lock entered");
try
{
if (SupportsInterval && UseSlidingInterval)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
LastRunTime = DateTime.Now;
Run();
}
finally
{
if (SupportsInterval && UseSlidingInterval)
{
_timer.Change(Interval, Interval);
}
else
{
_timer.Change(GetNextRunTimeInterval(), Timeout.Infinite);
}
Monitor.Exit(_lock);
Console.WriteLine("Lock exited");
}
}
else
{
Console.WriteLine("Lock is on");
}
}
public void Stop()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
public void Dispose()
{
if (_timer == null)
return;
_timer.Dispose();
_timer = null;
}
}
Here I use simple trick. I find the interval in milliseconds between current time and next time when job must run. Then I initialize timer as a one shot timer. It waits with first run the amount of milliseconds to next run and then runs the callback. In callback I initialize timer again using next running time. Interval in all cases is infinity which means that timer runs just once per initialization.
Testing base server
I’m sure you want to test base server and see how it works. I wrote simple test server so you can try it out, modify it and do what ever experiments you like to do.
class TestServer : BaseServer
{
public override int Interval
{
get { return 5000; }
}
public override bool UseSlidingInterval
{
get { return true; }
}
protected override void Run()
{
Console.WriteLine(DateTime.Now + " Run");
}
public override bool SupportsInterval
{
get { return false; }
}
private DateTime _nextRunTime = DateTime.MinValue;
public override DateTime NextRunTime
{
get
{
if (_nextRunTime < DateTime.Now)
_nextRunTime = DateTime.Now.AddMinutes(1);
return _nextRunTime;
}
}
}
And here is program file of my console application.
class Program
{
static void Main(string[] args)
{
var server = new TestServer();
server.Start();
Console.WriteLine("Press any key to exit ...");
Console.ReadLine();
}
}
When you run the application you should see output similar to screenshot below.
Running multiple servers
Suppose we have three different test servers. This is close to my real code where I have around six of them. The following code shows how to use arbitrary number of server instances.
class Program
{
static void Main(string[] args)
{
Console.WriteLine(" ");
var servers = new List<BaseServer>();
servers.Add(new TestServer1());
servers.Add(new TestServer2());
servers.Add(new TestServer3());
foreach (var server in servers)
server.Start();
Console.WriteLine("Press any key to exit ...");
Console.ReadLine();
foreach (var server in servers)
{
server.Stop();
server.Dispose();
}
}
}
Wrapping up
Instead of having multiple background jobs each with its own implementation we created base server class that provides solid base for our jobs. Base server takes care of running jobs based on interval, sliding interval or fixed moment of time. Base server takes also care of overlaping callback issues. Having same base class means that we can handle multiple jobs easily. We can use lists or other collections to store them and this way we don’t need one variable per server.
Hi
Nice work Gunnar! It’s nice to know what is under the hood of scheduling libraries and how semaphores work.
For practical reasons I would also suggest https://github.com/fluentscheduler/FluentScheduler for it’s flexible scheduling and task composition features. Also one can mark the tasks as non-reentrant.
Using ready-made libraries is also considerable option. My intention was to get away with minimal amount of code that is completely under my control.