May 18 2010

Just tap me on the shoulder when it's time

Many applications have a requirement to execute code on a recurring schedule. In cases such as this, there is a tendency to create dedicated background threads to perform this function. This pattern certainly works, but can carry some in-efficiencies when you have large numbers of threads that spend the vast majority of their life sleeping.
As an alternative, consider using an invocation callback pattern.

To implement this pattern we need to start with an interface to define the invoker.

using System;

public interface IInvocationTimer : IDisposable
{
    /// <summary>
    /// The interval at which the invocation targets are to be called
    /// </summary>
    TimeSpan Interval { get; set; }

    /// <summary>
    /// Starts the invocations
    /// </summary>
    void Start();

    /// <summary>
    /// Stops the invocations
    /// </summary>
    void Stop();

    /// <summary>
    /// Subscription point for invocations
    /// </summary>
    event EventHandler<EventArgs> InvocationTarget;

    /// <summary>
    /// Subscription point for all invokation exceptions
    /// </summary>
    event EventHandler<InvocationExceptionEventArgs> OnInvocationException;
}

Next we need to define the concrete implementation. Under the covers this uses the System.Timers.Timer which manages schedule-based invocations via the ThreadPool. This approach is lightweight, easy to manage, and testable in isolation.

using System;
using System.Runtime.Remoting.Messaging;
using System.Timers;

public class InvocationTimer : IInvocationTimer
{
    private readonly object _lock;
    private readonly Timer _timer;

    public InvocationTimer()
        : this(TimeSpan.Zero)
    {
        _lock = new Object();
    }

    public InvocationTimer(TimeSpan interval)
    {
        _timer = new Timer { AutoReset = true, Enabled = false };

        if (interval > TimeSpan.Zero)
            _timer.Interval = interval.TotalMilliseconds;

        _timer.Elapsed += TimerNotificationChain;
    }

    public event EventHandler<EventArgs> InvocationTarget;
    public event EventHandler<InvocationExceptionEventArgs> OnInvocationException;

    public TimeSpan Interval
    {
        get
        {
            return TimeSpan.FromMilliseconds(_timer.Interval);
        }
        set
        {
            _timer.Interval = value.TotalMilliseconds;
        }
    }

    public void Start()
    {
        if (_timer.Interval < 1)
            throw new InvalidOperationException("The invocation 'Interval' must be set to a value above zero before the timer can be started.");

        _timer.Start();
    }

    public void Stop()
    {
        _timer.Stop();
    }

    public void Dispose()
    {
        _timer.Stop();
        _timer.Dispose();
    }

    private void TimerNotificationChain(object sender, ElapsedEventArgs e)
    {
        lock (InvocationTarget)
        {
            foreach (EventHandler<EventArgs> handler in InvocationTarget.GetInvocationList())
            {
                handler.BeginInvoke(this, new EventArgs(), InvocationCompletionCallback, null);
            }
        }
    }

    private void InvocationCompletionCallback(IAsyncResult ar)
    {
        lock (_lock)
        {
            var asyncResult = (AsyncResult)ar;
            var handler = (EventHandler<EventArgs>)asyncResult.AsyncDelegate;

            try
            {
                handler.EndInvoke(asyncResult);
            }
            catch (Exception exp)
            {
                RaiseInvocationExceptionEvent(new InvocationExceptionEventArgs
                                                    {
                                                        Exception = exp
                                                    });
            }
        }
    }

    private void RaiseInvocationExceptionEvent(InvocationExceptionEventArgs e)
    {
        if (OnInvocationException == null) return;

        OnInvocationException(this, e);
    }
}

Finally, lets take a look at how to use this.

public class MyClass
{
    private IInvocationTimer _timer;
    private volatile bool _inInvokation;

    public MyClass()
        : this(new InvocationTimer())
    { }

    internal MyClass(IInvocationTimer timer)
    {
        _timer = timer;
        _timer.Interval = TimeSpan.FromSeconds(5);
        _timer.InvocationTarget += _timer_InvocationTarget;
        _timer.OnInvocationException += _timer_OnInvocationException;
    }

    void _timer_OnInvocationException(object sender, InvocationExceptionEventArgs e)
    {
        if (_inInvokation) return;

        _inInvokation = true;

        //TODO: Do Work

        _inInvokation = false;
    }

    void _timer_InvocationTarget(object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }
}

Other Considerations / Selection Criteria

  • The InvocationTimer implementation of this pattern runs out of the ThreadPool. Consequently, when the pool is under pressure, things may not fire exactly on schedule. If you have a strict schedule to keep, consider an alternate pattern
  • The InvocationTimer is not blocked while waiting for Invoked members to complete an invocation. Consequently, the subscriber must manage concerns with regard to re-entrancy. The sample above shows one simple way to do this
  • The InvocationTimer is backed by the IInvocationTimer interface to make it mockable. Doing so, allows this concern to be removed from consuming layers for more isolated testing

Nice!!!

Tags: , ,

Comments are closed