Skip to content

Instantly share code, notes, and snippets.

@abitofhelp
Created August 14, 2018 22:14
Show Gist options
  • Save abitofhelp/0e49a626d4d1cbe2550c8f60f317fdc0 to your computer and use it in GitHub Desktop.
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.
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