Seems like a good place to mention this: I have a proposal to augment ScheduleTask functionality.
We've seen requests to allow for schedule tasks to run at a defined
time, which is difficult when you must account for app recycles, or simply running on a particular day of a month. I don't like the idea of hardcoding an allowed run period in the task itself, so I copied the existing files and created CustomTask.cs, CustomTaskManager.cs, and CustomTaskThread.cs and tried to implement "NextRunDate" behavior while changing as little as possible.
Here are additional properties for
ScheduleTask.cs entity:
partial class ScheduleTask : BaseEntity
{
public DateTime? NextRunDateTimeUtc { get; set; } // DateTime NextRunDateTime is redundant naming, apologies
public int RunIntervalId {get; set; }
public RunInterval RunInterval
{
get { return ((RunInterval) (RunIntervalId)); }
set { RunIntervalId = (int)value; }
}
}
public enum RunInterval
{
CUSTOM = 0, //use "Seconds"
MINUTELY = 1,
HOURLY = 2,
DAILY = 3,
WEEKLY = 4,
MONTHLY = 5,
ANNUALLY = 6
}
In
CustomTaskManager.cs we add ScheduleTasks that have a defined "NextRunDate" to their own thread with the new CustomTaskThread property "HasNextRunDate."
....
var taskService = EngineContext.Current.Resolve<IScheduleTaskService>();
var scheduleTasks = taskService
.GetAllTasks()
.OrderBy(x => x.Seconds)
.ToList();
var scheduleTasksWithNextRunDate = scheduleTasks.Where(q => q.NextRunDateTimeUtc.HasValue).ToList();
var scheduleTasksWithoutNextRunDate = scheduleTasks.Except(scheduleTasksWithNextRunDate).ToList();
//each task with start date lives in own thread
foreach (var scheduleTask in scheduleTasksWithNextRunDate)
{
var taskThread = new CustomTaskThread
{
HasNextRunDate = true //new property of CustomTaskThread
};
var task = new CustomTask(scheduleTask);
taskThread.AddTask(task);
this._taskThreads.Add(taskThread);
}
//group by threads with the same seconds
foreach (var scheduleTaskGrouped in scheduleTasksWithoutNextRunDate.GroupBy(x => x.Seconds))
{
//create a thread
var taskThread = new CustomTaskThread
{
Seconds = scheduleTaskGrouped.Key
};
foreach (var scheduleTask in scheduleTaskGrouped)
{
var task = new CustomTask(scheduleTask);
taskThread.AddTask(task);
}
this._taskThreads.Add(taskThread);
}
In
CustomTaskThread.cs we add the property HasNextRunDate and adjust the interval getter.
....
public bool HasNextRunDate { get; set; }
public int Interval
{
get
{
return HasNextRunDate
? 30000 //if next run date specified, default interval to 30 seconds
: this.Seconds * 1000;
}
}
in
CustomTask.cs we must determine if it's appropriate to run this task - if so, and "NextRunDate" is defined, we update accordingly when finished as new-next-run-date = old-next-run-date + interval. Added "runNow" arg for when user specifically clicks "Run Now" button.
public void Execute(bool throwException = false, bool dispose = true, bool ensureRunOnOneWebFarmInstance = true, bool runNow = false)
{
var scope = EngineContext.Current.ContainerManager.Scope();
var scheduleTaskService = EngineContext.Current.ContainerManager.Resolve<IScheduleTaskService>("", scope);
var scheduleTask = scheduleTaskService.GetTaskByType(this.Type);
var utcNow = DateTime.UtcNow;
if (scheduleTask != null && scheduleTask.NextRunDateTimeUtc.HasValue
&& scheduleTask.NextRunDateTimeUtc.Value > utcNow && !runNow) //if NextRunDate
{
return;
}
.... //now task has executed
if (scheduleTask != null)
{
scheduleTask.LastEndUtc = this.LastEndUtc;
scheduleTask.LastSuccessUtc = this.LastSuccessUtc;
if (scheduleTask.NextRunDateTimeUtc.HasValue && !runNow)
{
var timeSpan = GetNextRunDateInterval((RunInterval) scheduleTask.RunIntervalId,
scheduleTask.NextRunDateTimeUtc.Value, scheduleTask.Seconds);
scheduleTask.Seconds = timeSpan.Seconds;
scheduleTask.NextRunDateTimeUtc += timeSpan;
}
scheduleTaskService.UpdateTask(scheduleTask);
}
....
public static TimeSpan GetNextRunDateInterval(RunInterval runInterval, DateTime startDate, int seconds = 0)
{
TimeSpan timeSpan, totalTimeSpan = new TimeSpan(0,0,0,0);
DateTime nextRunDate = startDate;
do
{
switch (runInterval)
{
case RunInterval.MINUTELY:
timeSpan = startDate.AddMinutes(1) - startDate;
break;
case RunInterval.HOURLY:
timeSpan = startDate.AddHours(1) - startDate;
break;
case RunInterval.DAILY:
timeSpan = startDate.AddDays(1) - startDate;
break;
case RunInterval.WEEKLY:
timeSpan = startDate.AddDays(7) - startDate;
break;
case RunInterval.MONTHLY:
timeSpan = startDate.AddMonths(1) - startDate;
break;
case RunInterval.ANNUALLY:
timeSpan = startDate.AddYears(1) - startDate;
break;
default:
timeSpan = startDate.AddSeconds(seconds > 0 ? seconds : 60) -
startDate;
break;
}
nextRunDate += timeSpan;
totalTimeSpan += timeSpan;
} while (nextRunDate < DateTime.UtcNow); //ensure next run date is in future
return totalTimeSpan;
}
I'll spare you the details about the admin view for now. I'm interested if anyone else has had any similar requests, and/or their feelings on this. Task manageability is sometimes an important part of an ecommerce system, thus I believe there is value in making adjustments similar to this in nopCommerce.