网站需要定时执行不同的任务,比如清理无效的数据、定时发送mail等,Nop的这个定时任务设计比较好,简单的说就是将所有任务相同的属性持久化,具体的执行通过继承接口来实现。
持久化对象:ScheduleTask
ScheduleTask定义了Seconds,Type等属性,分别记录执行周期和任务类型。
public class ScheduleTask:BaseEntity { public string Name { get; set; } ////// Gets or sets the run period (in seconds) /// public int Seconds { get; set; } /// /// Gets or sets the type of appropriate ITask class /// public string Type { get; set; } /// /// Gets or sets the value indicating whether a task is enabled /// public bool Enabled { get; set; } /// /// Gets or sets the value indicating whether a task should be stopped on some error /// public bool StopOnError { get; set; } /// /// Gets or sets the machine name (instance) that leased this task. It's used when running in web farm (ensure that a task in run only on one machine). It could be null when not running in web farm. /// public string LeasedByMachineName { get; set; } /// /// Gets or sets the datetime until the task is leased by some machine (instance). It's used when running in web farm (ensure that a task in run only on one machine). /// public DateTime? LeasedUntilTime { get; set; } /// /// Gets or sets the datetime when it was started last time /// public DateTime? LastStartTime { get; set; } /// /// Gets or sets the datetime when it was finished last time (no matter failed ir success) /// public DateTime? LastEndTime { get; set; } /// /// Gets or sets the datetime when it was sucessfully finished last time /// public DateTime? LastSuccessTime { get; set; } }
比如定义一个deleteGuestTask,即2小时删除一次guest。
var deleteGuestTask = new ScheduleTask { Name = "Delete Guest Task", Seconds = 7200, Type = "Portal.Services.Users.DeleteGuestTask,Portal.Services", StopOnError = true, LeasedByMachineName = "0", Enabled = true, };
对数据库的基本操作就交给了ScheduleTaskService。ScheduleTaskService继承IScheduleTaskService。
public partial interface IScheduleTaskService { void DeleteTask(ScheduleTask task); ScheduleTask GetTaskById(int taskId); ScheduleTask GetTaskByType(string type); IListGetAllTasks(bool showHidden = false); void InsertTask(ScheduleTask task); void UpdateTask(ScheduleTask task); }
面向接口ITask
ITask接口只有一个方法:
public partial interface ITask { ////// Execute task /// void Execute(); }
Nop实现了多个任务
例如DeleteGuestTask:
public class DeleteGuestTask:ITask { private readonly IUserService _userService; public DeleteGuestTask(IUserService userService) { _userService = userService; } public void Execute() { //60*24 = 1 day var olderThanMinutes = 1440; _userService.DeleteGuestUsers(null, DateTime.Now.AddMinutes(-olderThanMinutes), true); } }
而ScheduleTask如何和ITask关联的任务就交给了Task类。
public partial class Task { ////// Ctor for Task /// private Task() { this.Enabled = true; } /// /// Ctor for Task /// /// Task public Task(ScheduleTask task) { this.Type = task.Type; this.Enabled = task.Enabled; this.StopOnError = task.StopOnError; this.Name = task.Name; } private ITask CreateTask(ILifetimeScope scope) { ITask task = null; if (this.Enabled) { var type2 = System.Type.GetType(this.Type); if (type2 != null) { object instance; if (!EngineContext.Current.ContainerManager.TryResolve(type2, scope, out instance)) { //not resolved instance = EngineContext.Current.ContainerManager.ResolveUnregistered(type2, scope); } task = instance as ITask; } } return task; } /// /// Executes the task /// /// /// A value indicating whether exception should be thrown if some error happens /// A value indicating whether all instances hsould be disposed after task run public void Execute(ILifetimeScope scope=null,bool throwException = false, bool dispose = true) { this.IsRunning = true; //background tasks has an issue with Autofac //because scope is generated each time it's requested //that's why we get one single scope here //this way we can also dispose resources once a task is completed if (scope == null) { scope = EngineContext.Current.ContainerManager.Scope(); } var scheduleTaskService = EngineContext.Current.ContainerManager.Resolve ("", scope); var scheduleTask = scheduleTaskService.GetTaskByType(this.Type); try { var task = this.CreateTask(scope); if (task != null) { this.LastStartUtc = DateTime.UtcNow; if (scheduleTask != null) { //update appropriate datetime properties scheduleTask.LastStartTime = this.LastStartUtc; scheduleTaskService.UpdateTask(scheduleTask); } //execute task task.Execute(); this.LastEndUtc = this.LastSuccessUtc = DateTime.UtcNow; } } catch (Exception exc) { this.Enabled = !this.StopOnError; this.LastEndUtc = DateTime.UtcNow; //log error if (throwException) throw; } if (scheduleTask != null) { //update appropriate datetime properties scheduleTask.LastEndTime = this.LastEndUtc; scheduleTask.LastSuccessTime = this.LastSuccessUtc; scheduleTaskService.UpdateTask(scheduleTask); } //dispose all resources if (dispose) { scope.Dispose(); } this.IsRunning = false; } /// /// A value indicating whether a task is running /// public bool IsRunning { get; private set; } /// /// Datetime of the last start /// public DateTime? LastStartUtc { get; private set; } /// /// Datetime of the last end /// public DateTime? LastEndUtc { get; private set; } /// /// Datetime of the last success /// public DateTime? LastSuccessUtc { get; private set; } /// /// A value indicating type of the task /// public string Type { get; private set; } /// /// A value indicating whether to stop task on error /// public bool StopOnError { get; private set; } /// /// Get the task name /// public string Name { get; private set; } /// /// A value indicating whether the task is enabled /// public bool Enabled { get; set; } }
CreateTask方法用Autofac Resolve(相当于反射)出了对应的任务类型。
private ITask CreateTask(ILifetimeScope scope) { ITask task = null; if (this.Enabled) { var type2 = System.Type.GetType(this.Type); if (type2 != null) { object instance; if (!EngineContext.Current.ContainerManager.TryResolve(type2, scope, out instance)) { //not resolved instance = EngineContext.Current.ContainerManager.ResolveUnregistered(type2, scope); } task = instance as ITask; } } return task; }
而Task的Execute方法,调用的是各自任务的Execute。
TaskManager统一管理,TaskThread真正执行
TaskManager统一加载和执行任务,在Global中调用。
protected void Application_Start() { //... TaskManager.Instance.Initialize(); TaskManager.Instance.Start(); }
这里说一下TaskThread,它包含一个Timer和Dictionary
public partial class TaskThread : IDisposable { private Timer _timer; private bool _disposed; private readonly Dictionary<string, Task> _tasks; internal TaskThread() { this._tasks = new Dictionary<string, Task>(); this.Seconds = 10 * 60; } private void Run() { if (Seconds <= 0) return; this.StartedUtc = DateTime.Now; this.IsRunning = true; foreach (Task task in this._tasks.Values) { task.Execute(Scope,false,false); } this.IsRunning = false; } private void TimerHandler(object state) { this._timer.Change(-1, -1); this.Run(); if (this.RunOnlyOnce) { this.Dispose(); } else { this._timer.Change(this.Interval, this.Interval); } } ////// Disposes the instance /// public void Dispose() { if ((this._timer != null) && !this._disposed) { lock (this) { this._timer.Dispose(); this._timer = null; this._disposed = true; } } } private ILifetimeScope Scope { get; set; } /// /// Inits a timer /// public void InitTimer(ILifetimeScope scope) { Scope = scope; if (this._timer == null) { this._timer = new Timer(new TimerCallback(this.TimerHandler), null, this.Interval, this.Interval); } } /// /// Adds a task to the thread /// /// The task to be added public void AddTask(Task task) { if (!this._tasks.ContainsKey(task.Name)) { this._tasks.Add(task.Name, task); } } /// /// Gets or sets the interval in seconds at which to run the tasks /// public int Seconds { get; set; } /// /// Get or sets a datetime when thread has been started /// public DateTime StartedUtc { get; private set; } /// /// Get or sets a value indicating whether thread is running /// public bool IsRunning { get; private set; } /// /// Get a list of tasks /// public IList Tasks { get { var list = new List (); foreach (var task in this._tasks.Values) { list.Add(task); } return new ReadOnlyCollection (list); } } /// /// Gets the interval at which to run the tasks /// public int Interval { get { return this.Seconds * 1000; } } /// /// Gets or sets a value indicating whether the thread whould be run only once (per appliction start) /// public bool RunOnlyOnce { get; set; } }
在Run方法中执行所有包含的任务。这里其实是相同周期的任务。
TaskManager有一个_taskThreads集合,而Initialize方法的主要任务是从数据库加载ScheduleTask.然后将相同周期的任务组装成同一个TaskThread.还区分了只执行一次的任务。
public void Initialize() { this._taskThreads.Clear(); var taskService = EngineContext.Current.Resolve(); var scheduleTasks = taskService .GetAllTasks() .OrderBy(x => x.Seconds) .ToList(); //group by threads with the same seconds foreach (var scheduleTaskGrouped in scheduleTasks.GroupBy(x => x.Seconds)) { //create a thread var taskThread = new TaskThread { Seconds = scheduleTaskGrouped.Key }; foreach (var scheduleTask in scheduleTaskGrouped) { var task = new Task(scheduleTask); taskThread.AddTask(task); } this._taskThreads.Add(taskThread); } var notRunTasks = scheduleTasks .Where(x => x.Seconds >= _notRunTasksInterval) .Where(x => !x.LastStartTime.HasValue || x.LastStartTime.Value.AddSeconds(_notRunTasksInterval) < DateTime.UtcNow) .ToList(); //create a thread for the tasks which weren't run for a long time if (notRunTasks.Count > 0) { var taskThread = new TaskThread { RunOnlyOnce = true, Seconds = 60 * 5 //let's run such tasks in 5 minutes after application start }; foreach (var scheduleTask in notRunTasks) { var task = new Task(scheduleTask); taskThread.AddTask(task); } this._taskThreads.Add(taskThread); } }
在Star中执行任务。
public void Start() { foreach (var taskThread in this._taskThreads) { taskThread.InitTimer(); } }
在后台管理任务的情况,可以立即执行。
public ActionResult RunNow(int id) { var scheduleTask = _taskService.GetTaskById(id); if (scheduleTask == null) return View("NoData"); var task = new Task(scheduleTask); task.Enabled = true; task.Execute(null,false,false); return RedirectToAction("Index"); }
个人感觉还是值得借鉴的,我已经用到自己的项目中,可以方便的扩展到自己的其他任务。
Nop源码3.7:http://www.nopcommerce.com/downloads.aspx