Occasionally...

Running an Azure classic cloud service, which at the top level is a big while(true) block, one sometimes needs to run an action sometimes and not every time around the loop. We could solve this in a multi-threaded way by creating a thread per action and blocking for a set amount of time in each thread. A more low-tech and more "tell-dont-ask" approach might be something like this.

An occasional task runs as part of a while loop, single threaded whenever it feels like the next run is required:


public class OccasionalAction
{
	private DateTime nextRun;
	private TimeSpan frequency;

	private Action action;

	public OccasionalAction(Action a, TimeSpan freq)
		: this(a, freq, DateTime.MinValue)
	{
	}

	public OccasionalAction(Action a, TimeSpan freq, DateTime future)
	{
		this.action = a;
		this.frequency = freq;
		this.nextRun = future;
	}

	public void Run()
	{
		if (RunNow())
		{
			this.action();
			this.nextRun = DateTime.UtcNow + this.frequency;
		}
	}

	protected virtual bool RunNow()
	{
		return (DateTime.UtcNow > this.nextRun);
	}
}

Tests confirm the shape of the client code I was shooting for:


[Test]
public void OccasionalAction_Always_Executed_First_Time()
{
	bool actionCalled = false;

	OccasionalAction action = new OccasionalAction(() =>
	{
		actionCalled = true;
	},
	TimeSpan.FromDays(1));

	action.Run();

	Assert.IsTrue(actionCalled);
}

[Test]
public void OccasionalAction_Executes_Once_For_Long_Frequency()
{
	int callCount = 0;

	OccasionalAction action = new OccasionalAction(() =>
	{
		callCount++;
	},
	TimeSpan.FromDays(365));

	for(int i = 0; i < 10; ++i)
		action.Run();

	Assert.AreEqual(1, callCount);
}

[Test]
public void OccasionalAction_Executes_Multiply_For_Short_Frequency()
{
	int callCount = 0;

	OccasionalAction action = new OccasionalAction(() =>
	{
		callCount++;
	},
	TimeSpan.FromMilliseconds(1));

	for (int i = 0; i < 10; ++i)
	{
		action.Run();
		System.Threading.Thread.Sleep(TimeSpan.FromMilliseconds(10));
	}

	Assert.AreEqual(10, callCount);
}


[Test]
public void OccasionalAction_Future_Action_Not_Run_First_Time()
{
	bool actionCalled = false;

	OccasionalAction action = new OccasionalAction(() =>
	{
		actionCalled = true;
	},
	TimeSpan.FromDays(1),
	DateTime.Now.AddHours(1));

	action.Run();

	Assert.IsFalse(actionCalled);
}

And a collection of occasional tasks to round things off:


public class OccasionalActions
{
	private List<OccasionalAction> actions = new List<OccasionalAction>();

	public void Add(OccasionalAction action)
	{
		this.actions.Add(action);
	}

	public void Add(Action a, TimeSpan frequency)
	{
		this.Add(new OccasionalAction(a, frequency));
	}

	public void Run()
	{
		this.actions.ForEach(x => x.Run());
	}
}