|
using Hangfire; |
|
using Hangfire.MemoryStorage; |
|
using Hangfire.Storage; |
|
using Microsoft.Extensions.DependencyInjection; |
|
using Sitecore; |
|
using Sitecore.Abstractions; |
|
using Sitecore.Data; |
|
using Sitecore.Data.Items; |
|
using Sitecore.DependencyInjection; |
|
using Sitecore.Diagnostics; |
|
using Sitecore.Jobs; |
|
using Sitecore.Owin.Pipelines.Initialize; |
|
using Sitecore.Tasks; |
|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Text.RegularExpressions; |
|
|
|
namespace Scms.Foundation.Scheduling.Pipelines |
|
{ |
|
public class PrecisionScheduler : InitializeProcessor |
|
{ |
|
private const string SCHEDULE_DATABASE = "master"; |
|
|
|
public string RefreshSchedule { get; set; } = "*/2 * * * *"; |
|
public int StartupDelaySeconds { get; set; } = 15; |
|
|
|
private static void LogMessage(string message) |
|
{ |
|
Log.Info($"[PrecisionScheduler] {message}", nameof(PrecisionScheduler)); |
|
} |
|
|
|
public override void Process(InitializeArgs args) |
|
{ |
|
var app = args.App; |
|
|
|
app.UseHangfireAspNet(() => |
|
{ |
|
GlobalConfiguration.Configuration.UseMemoryStorage(); |
|
return new[] { new BackgroundJobServer() }; |
|
}); |
|
|
|
LogMessage("Starting up precision scheduler."); |
|
BackgroundJob.Schedule(() => Initialize(RefreshSchedule), TimeSpan.FromSeconds(StartupDelaySeconds)); |
|
} |
|
|
|
private static string GenerateMultiDayCronExpression(TimeSpan runTime, List<DayOfWeek> daysToRun) |
|
{ |
|
var castedDaysToRun = daysToRun.Cast<int>().ToList(); |
|
return $"{ParseCronTimeSpan(runTime)} * * {ParseMultiDaysList(castedDaysToRun)}"; |
|
} |
|
|
|
private static string ParseCronTimeSpan(TimeSpan timeSpan) |
|
{ |
|
if (timeSpan.Days > 0) |
|
{ |
|
//At HH:mm every day. |
|
return $"{timeSpan.Minutes} {timeSpan.Hours}"; |
|
} |
|
else if (timeSpan.Hours > 0) |
|
{ |
|
//At m minutes past the hour, every h hours. |
|
return $"{timeSpan.Minutes} */{timeSpan.Hours}"; |
|
} |
|
else if (timeSpan.Minutes > 0) |
|
{ |
|
//Every m minutes. |
|
return $"*/{timeSpan.Minutes} *"; |
|
} |
|
|
|
return $"*/30 *"; |
|
} |
|
|
|
private static string ParseMultiDaysList(List<int> daysToRun) |
|
{ |
|
if (daysToRun.Any() && daysToRun.Count == 7) return "*"; |
|
|
|
return string.Join(",", daysToRun); |
|
} |
|
|
|
private static List<DayOfWeek> ParseDays(int days) |
|
{ |
|
var daysOfWeek = new List<DayOfWeek>(); |
|
if (days <= 0) return daysOfWeek; |
|
|
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Sunday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Sunday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Monday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Monday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Tuesday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Tuesday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Wednesday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Wednesday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Thursday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Thursday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Friday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Friday); |
|
} |
|
if (MainUtil.IsBitSet((int)DaysOfWeek.Saturday, days)) |
|
{ |
|
daysOfWeek.Add(DayOfWeek.Saturday); |
|
} |
|
|
|
return daysOfWeek; |
|
} |
|
|
|
public static void RunSchedule(ID itemId) |
|
{ |
|
var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase("master", true); |
|
var item = database.GetItem(itemId); |
|
|
|
if (item == null) |
|
{ |
|
LogMessage($"Removing background job for {itemId}."); |
|
RecurringJob.RemoveIfExists(itemId.ToString()); |
|
return; |
|
} |
|
|
|
var jobName = $"{nameof(PrecisionScheduler)}-{itemId}"; |
|
var runningJob = JobManager.GetJob(jobName); |
|
if(runningJob != null && runningJob.Status.State == JobState.Running) |
|
{ |
|
LogMessage($"Background job for {itemId} is already running."); |
|
return; |
|
} |
|
|
|
LogMessage($"Running background job for {itemId}."); |
|
var scheduleItem = new ScheduleItem(item); |
|
|
|
var jobOptions = new DefaultJobOptions(jobName, "scheduling", "scheduler", Activator.CreateInstance(typeof(JobRunner)), "Run", new object[] { ID.Parse(itemId) }); |
|
JobManager.Start(jobOptions); |
|
} |
|
|
|
public static void Initialize(string refreshSchedule) |
|
{ |
|
ManageJobs(true); |
|
RecurringJob.AddOrUpdate(nameof(ManageJobs), () => ManageJobs(false), refreshSchedule); |
|
} |
|
|
|
public static void ManageJobs(bool isStartup) |
|
{ |
|
var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase(SCHEDULE_DATABASE, true); |
|
var descendants = database.SelectItems($"/sitecore/system/tasks/schedules//*[@@templateid='{TemplateIDs.Schedule}']"); |
|
var schedules = new Dictionary<string, string>(); |
|
|
|
foreach (var item in descendants) |
|
{ |
|
if (item.TemplateID != TemplateIDs.Schedule) continue; |
|
var itemId = item.ID.ToString(); |
|
var schedule = GetSchedule(item); |
|
if (string.IsNullOrEmpty(schedule)) continue; |
|
schedules.Add(itemId, schedule); |
|
} |
|
|
|
var jobs = JobStorage.Current.GetConnection().GetRecurringJobs(); |
|
var existingJobs = new List<string>(); |
|
foreach (var job in jobs) |
|
{ |
|
if (!ID.IsID(job.Id)) continue; |
|
var itemId = job.Id; |
|
|
|
if (!schedules.ContainsKey(itemId)) |
|
{ |
|
LogMessage($"Removing {itemId} from recurring schedule."); |
|
RecurringJob.RemoveIfExists(itemId); |
|
continue; |
|
} |
|
|
|
var item = database.GetItem(itemId); |
|
var schedule = GetSchedule(item); |
|
if (string.IsNullOrEmpty(schedule)) |
|
{ |
|
LogMessage($"Removing {itemId} from recurring schedule with invalid expression."); |
|
RecurringJob.RemoveIfExists(itemId); |
|
continue; |
|
} |
|
|
|
if (!string.Equals(job.Cron, schedule, StringComparison.InvariantCultureIgnoreCase)) |
|
{ |
|
LogMessage($"Updating {itemId} with a new schedule '{schedule}'."); |
|
RecurringJob.AddOrUpdate($"{itemId}", () => RunSchedule(ID.Parse(itemId)), schedule, TimeZoneInfo.Local); |
|
} |
|
|
|
existingJobs.Add(itemId); |
|
} |
|
|
|
var missingJobs = schedules.Keys.Except(existingJobs); |
|
foreach (var missingJob in missingJobs) |
|
{ |
|
var itemId = missingJob; |
|
var item = database.GetItem(itemId); |
|
var schedule = GetSchedule(item); |
|
|
|
LogMessage($"Registering recurring job for {itemId} with schedule '{schedule}'."); |
|
RecurringJob.AddOrUpdate($"{itemId}", () => RunSchedule(ID.Parse(itemId)), schedule, TimeZoneInfo.Local); |
|
} |
|
|
|
if (isStartup) |
|
{ |
|
var recurringJobs = JobStorage.Current.GetConnection().GetRecurringJobs(); |
|
if (recurringJobs == null) return; |
|
|
|
foreach (var recurringJob in recurringJobs) |
|
{ |
|
if (!ID.IsID(recurringJob.Id)) continue; |
|
var itemId = recurringJob.Id; |
|
|
|
if (!schedules.ContainsKey(itemId)) continue; |
|
var item = database.GetItem(itemId); |
|
var scheduleItem = new ScheduleItem(item); |
|
|
|
var missedLastRun = (recurringJob.NextExecution - scheduleItem.LastRun) > TimeSpan.FromHours(24); |
|
if (missedLastRun) |
|
{ |
|
LogMessage($"Running missed job {itemId}."); |
|
var jobName = $"{nameof(PrecisionScheduler)}-{itemId}"; |
|
|
|
var jobOptions = new DefaultJobOptions(jobName, "scheduling", "scheduler", Activator.CreateInstance(typeof(JobRunner)), "Run", new object[] { ID.Parse(itemId) }); |
|
JobManager.Start(jobOptions); |
|
} |
|
} |
|
} |
|
} |
|
|
|
private static string GetSchedule(Item item) |
|
{ |
|
var schedule = item.Fields[ScheduleFieldIDs.Schedule].Value; |
|
if (string.IsNullOrEmpty(schedule)) return string.Empty; |
|
|
|
if (Regex.IsMatch(schedule, @"^(((\d+,)+\d+|(\d+|\*(\/|-)\d+)|\d+|\*)\s?){5,7}$", RegexOptions.Compiled)) |
|
{ |
|
return schedule; |
|
} |
|
|
|
var recurrence = new Recurrence(schedule); |
|
if (recurrence.Days == DaysOfWeek.None || |
|
recurrence.Interval == TimeSpan.Zero || |
|
recurrence.InRange(DateTime.UtcNow) != true) return string.Empty; |
|
|
|
return GenerateMultiDayCronExpression(recurrence.Interval, ParseDays((int)recurrence.Days).ToList()); |
|
} |
|
} |
|
|
|
public class JobRunner |
|
{ |
|
public void Run(ID itemId) |
|
{ |
|
var database = ServiceLocator.ServiceProvider.GetRequiredService<BaseFactory>().GetDatabase("master", true); |
|
var item = database.GetItem(itemId); |
|
if (item == null) return; |
|
|
|
var scheduleItem = new ScheduleItem(item); |
|
scheduleItem.Execute(); |
|
} |
|
} |
|
} |