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());
}
}