Created
August 14, 2018 22:14
-
-
Save abitofhelp/0e49a626d4d1cbe2550c8f60f317fdc0 to your computer and use it in GitHub Desktop.
This gist implements a job scheduler that makes it easier to have long running jobs go to sleep and wake up on a schedule.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.Threading; | |
using NLog; | |
using NodaTime; | |
using NodaTime.Text; | |
// Copyright 2009 The Noda Time Authors. All rights reserved. | |
// Use of this source code is governed by the Apache License 2.0, | |
// as found in the LICENSE.txt file: | |
// https://github.com/nodatime/nodatime/blob/master/LICENSE.txt. | |
namespace WbLib.Scheduling | |
{ | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// JobScheduler implements methods to make it easier to work scheduling putting a system to | |
/// sleep and awaking on a schedule for a long running job. | |
/// </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public class JobScheduler : IDisposable | |
{ | |
#region CONSTANTS | |
#endregion | |
#region ENUMERATIONS | |
#endregion | |
#region FIELDS | |
/// <summary> NLog logging. </summary> | |
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); | |
/// <summary> Indicates whether the resources have already been disposed. </summary> | |
[DebuggerDisplay("_alreadyDisposed = {_alreadyDisposed}")] | |
private bool _alreadyDisposed = false; | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// The default time zone to use if there is an issue with getting one from Settings. | |
/// </summary> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
[DebuggerDisplay("_defaultTimeZone = {_defaultTimeZone}")] | |
private DateTimeZone _defaultTimeZone = DateTimeZoneProviders.Tzdb["America/Los_Angeles"]; | |
/// <summary> This is our active timezone. </summary> | |
[DebuggerDisplay("_timeZone = {_timeZone}")] | |
private DateTimeZone _timeZone = null; | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// While "sleeping" until the starting moment, the cancellation token is monitored to cancel the | |
/// thread. | |
/// </summary> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
[DebuggerDisplay("_cancellationToken = {_cancellationToken}")] | |
private CancellationToken _cancellationToken = default(CancellationToken); | |
#endregion | |
#region PROPERTIES | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> The starting datetime in the time zone. </summary> | |
/// | |
/// <value> The starting date time in time zone. </value> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
[DebuggerDisplay("StartingDateTimeInTimeZone = {StartingDateTimeInTimeZone}")] | |
public ZonedDateTime StartingDateTimeInTimeZone { get; private set; } | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> The ending datetime in the time zone. </summary> | |
/// | |
/// <value> The ending date time in time zone. </value> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
[DebuggerDisplay("EndingDateTimeInTimeZone = {EndingDateTimeInTimeZone}")] | |
public ZonedDateTime EndingDateTimeInTimeZone { get; private set; } | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// Permits a shutdown of the thread that is "sleeping". We use the slim version so it can | |
/// monitor our cancellation token. So, we can wait until we send a signal, or the timeout | |
/// expires, or a cancellation token request. | |
/// </summary> | |
/// | |
/// <value> The shutdown event. </value> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
[DebuggerDisplay("ShutdownEvent = {ShutdownEvent}")] | |
public ManualResetEventSlim ShutdownEvent { get; private set; } | |
#endregion | |
#region DELEGATES | |
#endregion | |
#region CONSTRUCTORS/DESTRUCTORS | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> Constructor. </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
/// | |
/// <exception cref="ArgumentNullException"> Thrown when one or more required arguments are | |
/// null. </exception> | |
/// | |
/// <param name="timeZone"> The NodaTime time zone. </param> | |
/// <param name="startingTime"> A string representing the starting time in 24-hour | |
/// format. HH:mm For example: 09:10, 14:15. </param> | |
/// <param name="endingTime"> A string representing the ending time in 24-hour format. | |
/// HH:mm For example: 09:10, 14:15. </param> | |
/// <param name="cancellationToken"> While "sleeping" until the starting moment, the | |
/// cancellation token is monitored to cancel the thread. </param> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public JobScheduler( | |
string timeZone, | |
string startingTime, | |
string endingTime, | |
CancellationToken cancellationToken) | |
{ | |
if (string.IsNullOrEmpty(timeZone)) | |
{ | |
throw new ArgumentNullException("timeZone", "String is null or whitespace"); | |
} | |
if (string.IsNullOrEmpty(startingTime)) | |
{ | |
throw new ArgumentNullException("startingTime", "String is null or whitespace"); | |
} | |
if (string.IsNullOrEmpty(timeZone)) | |
{ | |
throw new ArgumentNullException("endingTime", "String is null or whitespace"); | |
} | |
_cancellationToken = cancellationToken; | |
// This is used to wake a "sleeping" thread so it can exit when the user presses ESC. | |
ShutdownEvent = new ManualResetEventSlim(false); | |
var tz = timeZone.Trim(); | |
if (!DateTimeZoneProviders.Tzdb.Ids.Contains(tz)) | |
{ | |
// If there is an issue with the time zone, we will use our default and log a warning. | |
_timeZone = _defaultTimeZone; | |
Logger.Warn("The time zone, '{0}', could not be found, so your default, '{1}', is being used.", | |
tz, _defaultTimeZone.Id); | |
} | |
else | |
{ | |
_timeZone = DateTimeZoneProviders.Tzdb[tz]; | |
} | |
StartingDateTimeInTimeZone = CreateDateTimeInTimeZone(startingTime); | |
EndingDateTimeInTimeZone = CreateDateTimeInTimeZone(endingTime); | |
} | |
#endregion | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> Put the thread to sleep until the next starting datetime. </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public void SleepUntilNextStart() | |
{ | |
// If it is time to sleep, then do it. | |
// Otherwise, keep processing. | |
if (IsSleepTime()) | |
{ | |
var nowInTimeZone = NowInTimeZone(); | |
// If we are here, we are outside of the timeframe for activity, and need to sleep. | |
// So, we will need to update our starting and ending datetimes for the next day. | |
// So, we will figure out how much time until the next starting period, and add a day. | |
// Check if the starting time needs to be bumped to the next day. | |
if (StartingDateTimeInTimeZone.TrimToHourMinute() < NowInTimeZone().TrimToHourMinute()) | |
{ | |
StartingDateTimeInTimeZone += Duration.FromStandardDays(1); | |
EndingDateTimeInTimeZone += Duration.FromStandardDays(1); | |
} | |
Utility.WriteAtLine( | |
string.Format( | |
"'{0}' in the '{1}' time zone is outside the processing interval, so the Producers will sleep until '{2}'.", | |
nowInTimeZone.ToString("MM/dd/yyyy HH:mm:ss", | |
CultureInfo.InvariantCulture), | |
_timeZone.Id, | |
StartingDateTimeInTimeZone.ToString("MM/dd/yyyy HH:mm:ss", | |
CultureInfo.InvariantCulture)), | |
71); | |
Logger.Info( | |
"*** The current time, '{0}', in the '{1}' time zone, is outside of the processing interval, so the Producers will sleep until '{2}'.", | |
nowInTimeZone.ToString(), | |
_timeZone.Id, | |
StartingDateTimeInTimeZone.ToString()); | |
var duration = (nowInTimeZone > StartingDateTimeInTimeZone) | |
? nowInTimeZone.ToInstant() - StartingDateTimeInTimeZone.ToInstant() | |
: StartingDateTimeInTimeZone.ToInstant() - nowInTimeZone.ToInstant(); | |
// "Sleep" until the next starting datetime or if signaled to awake for termination. | |
// While "sleeping" until the starting moment, the cancellation token | |
// is monitored to cancel the thread. | |
//System.Threading.Thread.Sleep(duration.ToTimeSpan()); | |
// Wait on the event to be signaled | |
// or the token to be canceled, | |
// whichever comes first. The token | |
// will throw an exception if it is canceled | |
// while the thread is waiting on the event. | |
try | |
{ | |
ShutdownEvent.Wait(duration.ToTimeSpan(), _cancellationToken); | |
} | |
catch (OperationCanceledException) | |
{ | |
// No more waiting... | |
return; | |
} | |
// After we awake. | |
Utility.WriteAtLine( | |
string.Format( | |
"The current time, in the '{0}' time zone, is inside of the processing interval, so we are migrating documents into the DMS.", | |
_timeZone.Id), | |
71); | |
Logger.Info( | |
"*** The current time, in the {0} time zone, is inside of the processing interval, so we are migrating documents into the DMS. ***", | |
_timeZone.Id); | |
} | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> The current datetime in the time zone. </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
/// | |
/// <returns> Null on error. </returns> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public ZonedDateTime NowInTimeZone() | |
{ | |
// Determine current time in the time zone.. | |
return NowInTimeZone(_timeZone.Id, _defaultTimeZone.Id); | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> The current datetime in the time zone. </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
/// | |
/// <param name="timezone"> This is our active timezone. </param> | |
/// <param name="defaultTimezone"> (Optional) | |
/// The default time zone to use if there is an issue with | |
/// getting one from Settings. </param> | |
/// | |
/// <returns> Null on error. </returns> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public static ZonedDateTime NowInTimeZone(string timezone, string defaultTimezone = "America/Los_Angeles") | |
{ | |
var cleanTimezone = timezone.Trim(); | |
DateTimeZone tz = null; | |
if (!DateTimeZoneProviders.Tzdb.Ids.Contains(cleanTimezone)) | |
{ | |
// If there is an issue with the time zone, we will use our default and log a warning. | |
cleanTimezone = defaultTimezone.Trim(); | |
Logger.Warn("The time zone, '{0}', could not be found, so your default, '{1}', is being used.", | |
timezone, defaultTimezone); | |
} | |
tz = DateTimeZoneProviders.Tzdb[cleanTimezone]; | |
// Determine current time in the time zone.. | |
Instant now = SystemClock.Instance.Now; | |
ZonedDateTime timezoneNow = now.InZone(tz); | |
return timezoneNow; | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> Determines whether we are within the active processing interval. </summary> | |
/// | |
/// <remarks> Only compares hours and minutes. </remarks> | |
/// | |
/// <param name="startingTime"> (Optional) A string representing the starting time in 24-hour | |
/// format. HH:mm For example: 09:10, 14:15. If it is null, the | |
/// starting time that was used when instantiated will be used, | |
/// otherwise, it will create the starting datetime for the | |
/// startingTime. </param> | |
/// <param name="endingTime"> (Optional) A string representing the ending time in 24-hour | |
/// format. HH:mm For example: 09:10, 14:15. If it is null, the | |
/// ending time that was used when instantiated will be used, | |
/// otherwise, it will create the ending datetime for the endingTime. </param> | |
/// | |
/// <returns> True to sleep, otherwise false. </returns> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public bool IsSleepTime(string startingTime = null, string endingTime = null) | |
{ | |
var isSleepTime = true; | |
// Determine current time in the time zone. | |
var nowInTimezone = NowInTimeZone().TrimToHourMinute(); | |
ZonedDateTime startingDateTimeInTimeZone = StartingDateTimeInTimeZone.TrimToHourMinute(); | |
if (!string.IsNullOrEmpty(startingTime)) | |
{ | |
startingDateTimeInTimeZone = CreateDateTimeInTimeZone(startingTime).TrimToHourMinute(); | |
} | |
ZonedDateTime endingDateTimeInTimeZone = EndingDateTimeInTimeZone.TrimToHourMinute(); | |
if (!string.IsNullOrEmpty(endingTime)) | |
{ | |
endingDateTimeInTimeZone = CreateDateTimeInTimeZone(endingTime).TrimToHourMinute(); | |
} | |
// The test for ending is less than so we terminate at the start of the ending datetime. | |
if (nowInTimezone >= startingDateTimeInTimeZone && nowInTimezone < endingDateTimeInTimeZone) | |
{ | |
isSleepTime = false; | |
} | |
return isSleepTime; | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// Takes a time specified in a string with a format of HH:mm and creates it in the time zone. | |
/// </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
/// | |
/// <param name="time"> a time specified in a string with a format of HH:mm. </param> | |
/// | |
/// <returns> | |
/// The time string expressed in today in the time zone., or null for an error. | |
/// </returns> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public ZonedDateTime CreateDateTimeInTimeZone(string time) | |
{ | |
// Parse the time string using Noda Time's pattern API | |
LocalTimePattern pattern = LocalTimePattern.CreateWithCurrentCulture("HH:mm"); | |
ParseResult<LocalTime> parseResult = pattern.Parse(time); | |
if (!parseResult.Success) | |
{ | |
Logger.Error("Failed to parse the time string '{0}'. It must be in HH:mm format.", time); | |
} | |
LocalTime localTime = parseResult.Value; | |
// Determine current time in the time zone.. | |
Instant now = SystemClock.Instance.Now; | |
LocalDate today = now.InZone(_timeZone).Date; | |
// Combine the date and time | |
LocalDateTime ldt = today.At(localTime); | |
// Bind it to the time zone | |
ZonedDateTime result = ldt.InZoneLeniently(_timeZone); | |
return result; | |
} | |
#region IDISPOSABLE IMPLEMENTATION | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> | |
/// Implement the only method in IDisposable. It calls the virtual Dispose() and suppresses | |
/// finalization. | |
/// </summary> | |
/// | |
/// <remarks> Mgardner, 10/27/2016. </remarks> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
public void Dispose() | |
{ | |
Dispose(true); | |
GC.SuppressFinalize(this); | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
/// <summary> This method performs the clean-up work. </summary> | |
/// | |
/// <remarks> This method will be implemented in sealed classes, too. </remarks> | |
/// | |
/// <param name="isDisposing"> . </param> | |
//////////////////////////////////////////////////////////////////////////////////////////////////// | |
private void Dispose(bool isDisposing) | |
{ | |
// Don't dispose more than once! | |
if (!_alreadyDisposed) | |
{ | |
if (isDisposing) | |
{ | |
// Dispose of MANAGED resources by calling their | |
// Dispose() method. | |
_defaultTimeZone = null; | |
_timeZone = null; | |
//Api = null; | |
//if (_YearsToDmsFolderIds != null) | |
//{ | |
// _YearsToDmsFolderIds.Dispose(); | |
// _YearsToDmsFolderIds = null; | |
//} | |
// Dispose of UNMANAGED resources here and set the disposed flag. | |
//if (nativeResource != IntPtr.Zero) | |
//{ | |
// Marshal.FreeHGlobal(nativeResource); | |
// nativeResource = IntPtr.Zero; | |
//} | |
// Indicate that disposing has been completed. | |
_alreadyDisposed = true; | |
} | |
} | |
// Tell the base class to free its resources because | |
// it is responsible for calling GC.SuppressFinalize(). | |
// base.Dispose(isDisposing); | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment