Repeating / Scheduled Tasks

Home / Repeating / Scheduled Tasks

For services that I write that repeat a task over a set interval, I generally use a simple repeater loop. For the cases where I needed a bit more granular control of how/when Tasks repeat, I created a simple scheduler with a Fluent API.


Basically, you can schedule Actions (which are executed through a Task) to be executed on a regular basis. For simple repeating tasks, it supports running every X minutes, X seconds, X milliseconds. Any interval/length of time more than that on a simple repeat would be some multiple of seconds/milliseconds/minutes.

It supports a start date and start time as DateTime and TimeSpan, running once a month, running only on certain days of the week (with the “RunMonthly”, it will expect to run on a single day), running daily, etc etc.

Here’s an example of a task/thread to be executed once a day at 3:15AM would look like, for example:

var schedule3 = new TaskSchedule()
       .WithName("DailySchedule All Days @ 3:15 AM")
       .RepeatDaily()
       .WithStartDate(new DateTime(2016, 9, 1))
       .WithStartTime(new TimeSpan(3, 15, 0)) // Run at 03:15 AM
       .WithAction(() =>
       {
              Console.WriteLine();
              Console.WriteLine("I'm your action3. {0:MM/dd/yyyy HH:mm:ss.fff}", TaskScheduleTimer.UtcNow.ToLocalTime());
       }, cancellationTokenSource.Token);
 

Here’s what a task/thread to be executed once a day but only on certain days would look like:

var schedule4 = new TaskSchedule()
       .WithName("DailySchedule Days Sat, Sun, Wed @ 6PM")
       .RepeatDaily()
       .WithStartDate(new DateTime(2016, 9, 3))
       .WithStartTime(new TimeSpan(18, 0, 0))
       .WithDaysOfWeek(new List<DayOfWeek>() { DayOfWeek.Saturday, DayOfWeek.Sunday, DayOfWeek.Wednesday })
       .WithAction(() =>
       {
       Console.WriteLine();
              Console.WriteLine("Here's some action4. {0:MM/dd/yyyy HH:mm:ss.fff}", DateTime.UtcNow.ToLocalTime());
       }, cancellationTokenSource.Token);

Full source code is below, and I also put this into a Gist. You’ll find it’s relatively simple, while the bulk of code comprises the fluent interface.

/// <summary>
/// Simple repeater based on a timed interval
/// </summary>
public static class TaskRepeater
{
    public static Task Interval(TimeSpan pollInterval, Action action, CancellationToken token, bool runImmediately = false)
    {
        // We don't use Observable.Interval:
        // If we block, the values start bunching up behind each other.
        return Task.Factory.StartNew(
            () =>
            {
                if (runImmediately)
                {
                    for (;;)
                    {
                        action();

                        if (token.WaitCancellationRequested(pollInterval))
                            break;
                    }
                }
                else
                {
                    for (;;)
                    {
                        if (token.WaitCancellationRequested(pollInterval))
                            break;

                        action();
                    }
                }
            }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }
}

/// <summary>
/// Task scheduler with many options and FluentAPI
/// </summary>
public class TaskSchedule
{
    private int _repeatMilliseconds = 0;
    private bool _repeatDaily = false;
    private bool _repeatMonthly = false;
    private string _name = "NoName";

    private List<DayOfWeek> _daysOfWeek = new List<DayOfWeek>() { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday,
    DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday };

    private DateTime _lastRun = DateTime.UtcNow.AddDays(-1);
    private DateTime _nextRun = DateTime.UtcNow.AddDays(-1);

    private DateTime _startDate = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
    private TimeSpan _startTime = new TimeSpan(0, 0, 0);
    private bool _acceleratedTime = false;

    private Action _action;
    private CancellationToken _token;

    public DateTime NextRun { get { return _nextRun; } set { _nextRun = value; } }
    public DateTime LastRun { get { return _lastRun; } set { _lastRun = value; } }

    public int RepeatMilliseconds { get { return _repeatMilliseconds; } set { _repeatMilliseconds = value; } }
    public bool RepeatDaily { get { return _repeatDaily; } set { _repeatDaily = value; _lastRun = _startDate + _startTime; } }
    public bool RepeatMonthly { get { return _repeatMonthly; } set { _repeatMonthly = value; _lastRun = _startDate + _startTime; } }
    public string Name { get { return _name; } set { _name = value; } }
    public List<DayOfWeek> DaysOfWeek { get { return _daysOfWeek; } set { _daysOfWeek = value; } }
    public DateTime StartDate { get { return _startDate; } set { _startDate = value; _lastRun = _startDate + _startTime; } }
    public TimeSpan StartTime { get { return _startTime; } set { _startTime = value; _lastRun = _startDate + _startTime; } }
    public bool AcceleratedTime { get { return _acceleratedTime; } set { _acceleratedTime = value; } }
    public Action ScheduleAction { get { return _action; } set { _action = value; } }
    public CancellationToken ScheduleToken { get { return _token; } set { _token = value; } }

    public TaskSchedule()
    {
    }

    public DateTime GetNextRun(DateTime? dateTime = null)
    {
        var currentDateTime = dateTime ?? TaskScheduleTimer.UtcNow;
        var testDate = currentDateTime.Date;
        if (_nextRun == DateTime.MinValue)
        {
            _lastRun = _nextRun = testDate = currentDateTime.AddMilliseconds(1);
            return testDate;
        }
        if (_repeatDaily)
        {
            testDate = testDate + _startTime;
            if (testDate < currentDateTime)
            {
                do
                {
                    testDate = testDate.AddDays(1);
                } while (!_daysOfWeek.Any(x => x == testDate.DayOfWeek) && _daysOfWeek != null && _daysOfWeek.Count > 0);
            }
        }
        else if (_repeatMonthly)
        {
            testDate = new DateTime(testDate.Year, testDate.Month, _startDate.Date.Day) + _startTime;
            if (testDate < currentDateTime)
            {
                testDate = testDate.AddMonths(1);
                testDate = new DateTime(testDate.Year, testDate.Month, _startDate.Date.Day) + _startTime;
                while (!_daysOfWeek.Any(x => x == testDate.DayOfWeek) && _daysOfWeek != null && _daysOfWeek.Count > 0)
                {
                    testDate = testDate.AddDays(1);
                }
            }
        }
        else
        {
            _lastRun = _nextRun;
            testDate = _lastRun;
            do
            {
                testDate = testDate.AddMilliseconds(_repeatMilliseconds);
            } while (testDate < currentDateTime);
        }
        _nextRun = testDate;
        return testDate;
    }

    public Task CreateTask()
    {
        return Task.Factory.StartNew(() =>
        {
            TimeSpan pollInterval;
            DateTime nextRunDate;
            for (;;)
            {
                var currentDateTime = TaskScheduleTimer.UtcNow;
                nextRunDate = this.GetNextRun(currentDateTime);
                pollInterval = nextRunDate - currentDateTime;

                if (_acceleratedTime)
                {
                    pollInterval = TimeSpan.FromMilliseconds(500);
                }
                Console.WriteLine(string.Format("[* {0} *]: Sleeping until {1:MM/dd/yyyy HH:mm:ss.fff}, Interval: {2}", _name, nextRunDate.ToLocalTime(), pollInterval));

                // We have to chunk the wait if we exceed Int32.MaxValue
                var totalMilliseconds = pollInterval.TotalMilliseconds;
                if (totalMilliseconds <= int.MaxValue)
                {
                    if (_token.WaitCancellationRequested(pollInterval))
                        break;
                }
                else
                {
                    while (totalMilliseconds > 0 && !_token.IsCancellationRequested)
                    {
                        var currentDelay = totalMilliseconds > int.MaxValue ? int.MaxValue : (int)totalMilliseconds;
                        if (_token.WaitCancellationRequested(TimeSpan.FromMilliseconds(currentDelay)))
                            break;
                        totalMilliseconds -= currentDelay;
                    }

                    if (_token.IsCancellationRequested)
                    {
                        break;
                    }
                }

                _action();

                if (_acceleratedTime)
                {
                    TaskScheduleTimer.SetCurrent(nextRunDate);
                }
            }

            if (_token.IsCancellationRequested)
            {
                Console.WriteLine(string.Format("[* {0} *]: Cancelled", _name));
            }

        }, _token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }
}

public class TaskScheduleTimer
{
    private static DateTime _startTime;
    private static Stopwatch _stopWatch = null;
    private static TimeSpan _maxIdle = TimeSpan.FromSeconds(10);

    public static DateTime UtcNow
    {
        get
        {
            if ((_stopWatch == null) || (_startTime.Add(_maxIdle) < DateTime.UtcNow))
            {
                Reset();
            }
            return _startTime.AddTicks(_stopWatch.Elapsed.Ticks);
        }
    }

    public static void SetCurrent(DateTime dateTime)
    {
        _startTime = dateTime;
    }

    private static void Reset()
    {
        _startTime = DateTime.UtcNow;
        _stopWatch = Stopwatch.StartNew();
    }

    public static Stopwatch Stopwatch
    {
        get { return _stopWatch; }
    }
}

public static class Extensions
{
    public static TaskSchedule WithName(this TaskSchedule schedule, string name)
    {
        schedule.Name = name;
        return schedule;
    }

    public static TaskSchedule RepeatMilliseconds(this TaskSchedule schedule, int milliseconds)
    {
        schedule.RepeatMilliseconds = milliseconds;
        return schedule;
    }

    public static TaskSchedule RepeatSeconds(this TaskSchedule schedule, int seconds)
    {
        schedule.RepeatMilliseconds = seconds * 1000;
        return schedule;
    }

    public static TaskSchedule RepeatMinutes(this TaskSchedule schedule, int minutes)
    {
        schedule.RepeatMilliseconds = minutes * 60000;
        return schedule;
    }

    public static TaskSchedule WithRunImmediately(this TaskSchedule schedule)
    {
        schedule.NextRun = DateTime.MinValue;
        return schedule;
    }

    public static TaskSchedule RepeatDaily(this TaskSchedule schedule, bool repeatDaily = true)
    {
        schedule.RepeatDaily = repeatDaily;
        schedule.RepeatMonthly = false;
        return schedule;
    }

    public static TaskSchedule RepeatMonthly(this TaskSchedule schedule, bool repeatMonthly = true)
    {
        schedule.RepeatMonthly = repeatMonthly;
        schedule.RepeatDaily = false;
        return schedule;
    }

    public static TaskSchedule WithDaysOfWeek(this TaskSchedule schedule, List<DayOfWeek> daysOfWeek)
    {
        schedule.DaysOfWeek = daysOfWeek;
        return schedule;
    }

    public static TaskSchedule WithStartDate(this TaskSchedule schedule, DateTime startDate)
    {
        schedule.StartDate = startDate.Date >= DateTime.UtcNow.Date ? startDate : DateTime.UtcNow.Date;
        return schedule;
    }

    public static TaskSchedule WithStartTime(this TaskSchedule schedule, TimeSpan startTime)
    {
        schedule.StartTime = startTime;
        if (schedule.RepeatMilliseconds > 0) { schedule.NextRun = schedule.StartDate + schedule.StartTime; };
        return schedule;
    }

    public static TaskSchedule WithAcceleratedTime(this TaskSchedule schedule)
    {
        schedule.AcceleratedTime = true;
        return schedule;
    }
    public static TaskSchedule WithAction(this TaskSchedule schedule, Action action, CancellationToken token)
    {
        schedule.ScheduleAction = action;
        schedule.ScheduleToken = token;
        schedule.CreateTask();
        return schedule;
    }

    public static bool WaitCancellationRequested(this CancellationToken token, TimeSpan timeout)
    {
        return token.WaitHandle.WaitOne(timeout);
    }
}

Leave a Reply