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:
1 2 3 4 5 6 7 8 9 10 | 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:
1 2 3 4 5 6 7 8 9 10 11 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | /// <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); } } |