I could have sworn that I posted something about this before, but apparently not. Anyway, a while back I was working on a project that required a number of tasks to occur that couldn't be accomplished easily in the normal post/response of a web request. Basically they were database and file updates that took about five to ten minutes to complete and needed to occur in 30 minute intervals. Now normally I would create a separate desktop app that would just run on the server that would handle the task but I didn't have rights to do that... So I had to build this as a set of background tasks within a web app. And I made it generic enough that I could package it up and release it. So let's look at some code:
/// <summary>
/// Manager for the task scheduler
/// </summary>
public class Manager:Singleton<Manager>
{
#region Constructor
/// <summary>
/// Constructor
/// </summary>
protected Manager()
: base()
{
try
{
Config = Gestalt.Manager.Instance.GetConfigFile<Configuration.Configuration>("TaskScheduler");
Workers = new List<Worker>();
for (int x = 0; x < Config.NumberOfThreads; ++x)
{
Worker TempWorker = new Worker("");
Workers.Add(TempWorker);
}
}
catch { throw; }
}
#endregion
#region Public Functions
/// <summary>
/// Starts the task manager
/// </summary>
/// <param name="TaskAssemblyLocation">Location of the task assembly</param>
public void Start(string TaskAssemblyLocation)
{
try
{
Assembly TaskAssembly = Assembly.LoadFile(TaskAssemblyLocation);
AddTasks(TaskAssembly);
StartWorkers();
}
catch { throw; }
}
/// <summary>
/// Starts the task manager
/// </summary>
/// <param name="TaskAssembly">The task assembly</param>
public void Start(Assembly TaskAssembly)
{
try
{
AddTasks(TaskAssembly);
StartWorkers();
}
catch { throw; }
}
/// <summary>
/// Starts the task manager
/// </summary>
/// <param name="TaskAssemblies">The task assemblies</param>
public void Start(List<Assembly> TaskAssemblies)
{
try
{
for (int x = 0; x < TaskAssemblies.Count; ++x)
{
AddTasks(TaskAssemblies[x]);
}
StartWorkers();
}
catch { throw; }
}
/// <summary>
/// Stops the tasks
/// </summary>
public void Stop()
{
try
{
for (int x = 0; x < Config.NumberOfThreads; ++x)
{
Workers[x].Stop();
}
}
catch { throw; }
}
#endregion
#region Private Functions
private void AddTasks(Assembly TaskAssembly)
{
try
{
List<Type> TaskTypes = Utilities.Reflection.GetTypes(TaskAssembly, "EchoNet.Task");
for (int x = 0; x < TaskTypes.Count; )
{
for (int y = 0; y < Workers.Count && x < TaskTypes.Count; ++y, ++x)
{
Task TempTask = (Task)TaskTypes[x].Assembly.CreateInstance(TaskTypes[x].FullName);
TempTask.Setup(TaskTypes[x].Name);
Workers[y].AddTask(TempTask);
}
}
}
catch { throw; }
}
private void StartWorkers()
{
try
{
for (int x = 0; x < Config.NumberOfThreads; ++x)
{
Workers[x].Start();
}
}
catch { throw; }
}
#endregion
#region Private Properties
private List<Worker> Workers { get; set; }
private Configuration.Configuration Config { get; set; }
#endregion
}
The code above is the manager for everything, it handles the basic creation of worker threads, setting up the individual tasks, etc. You'll notice that it uses a Singleton class as a base class. This is just a helper class from my utility library. The other thing to note is it uses Gestalt.Net for configuration, so the Configuration class is nothing but a stub and not that interesting. Well, it does set up the number of threads to set up. But to be honest, that's not that interesting. Other than that the only thing that the manager touches are the Worker class and Task class, so let's look at the Worker class:
public class Worker : Worker<bool, string>
{
#region Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="Params">Not used</param>
public Worker(string Params)
: base(Params)
{
try
{
Tasks = new List<Task>();
}
catch { throw; }
}
#endregion
#region Functions
/// <summary>
/// Adds a task to
/// </summary>
/// <param name="Task">Task to add</param>
public void AddTask(Task Task)
{
try
{
lock (Tasks)
{
Tasks.Add(Task);
}
}
catch { throw; }
}
#endregion
#region Overridden Functions
protected override bool Work(string Params)
{
try
{
while (true)
{
if (Stopping)
return true;
lock (Tasks)
{
for (int x = 0; x < Tasks.Count; ++x)
{
if (Tasks[x].NextRunTime < DateTime.Now)
{
Tasks[x].DoWork();
Tasks[x].UpdateTime(true);
}
}
}
Sleep(1000);
}
}
catch { throw; }
}
#endregion
#region Private Properties
private List<Task> Tasks { get; set; }
#endregion
}
The Worker class is the actual background thread class. Once again it uses a base class helper from my utility library. All it does is wraps the threading code (starting a thread, stopping it, etc.). The Worker thread simply holds the individual tasks, sees if they should be called, and if need be runs them. The task class is where the actual interesting code occurs:
public abstract class Task:ITask
{
#region Constructor
/// <summary>
/// Constructor
/// </summary>
public Task()
{
}
#endregion
#region Abstract Functions
public abstract void DoWork();
#endregion
#region Internal Functions
internal void Setup(string ClassName)
{
try
{
Config = Gestalt.Manager.Instance.GetConfigFile<TaskConfiguration>(ClassName);
NextRunTime = Config.NextRunTime;
if (Config.Frequency != EchoNet.Enum.RunTime.Once)
{
while (NextRunTime < DateTime.Now || NextRunTime < Config.Start)
{
UpdateTime(false);
}
}
else
{
if (NextRunTime < DateTime.Now)
NextRunTime = DateTime.Now;
if (NextRunTime < Config.Start)
NextRunTime = Config.Start;
}
if (NextRunTime > Config.End)
NextRunTime = DateTime.MaxValue;
}
catch { throw; }
}
internal void UpdateTime(bool Save)
{
try
{
if (Config.Frequency == EchoNet.Enum.RunTime.Hourly)
{
NextRunTime = NextRunTime.AddHours(1.0d);
}
else if (Config.Frequency == EchoNet.Enum.RunTime.Daily)
{
NextRunTime = NextRunTime.AddDays(1.0d);
}
else if (Config.Frequency == EchoNet.Enum.RunTime.Monthly)
{
NextRunTime = NextRunTime.AddMonths(1);
}
else if (Config.Frequency == EchoNet.Enum.RunTime.Yearly)
{
NextRunTime = NextRunTime.AddYears(1);
}
else if (Config.Frequency == EchoNet.Enum.RunTime.Weekly)
{
NextRunTime = NextRunTime.AddDays(7.0d);
}
else if (Config.Frequency == EchoNet.Enum.RunTime.Once)
{
NextRunTime = DateTime.MaxValue;
}
if (Save)
{
Config.NextRunTime = NextRunTime;
Config.Save();
}
}
catch { throw; }
}
#endregion
#region Properties
protected TaskConfiguration Config { get; set; }
internal DateTime NextRunTime { get; set; }
#endregion
}
OK, I lied, it's not that interesting. The Task class is nothing more than a base class and contains only a function for setup (figuring out the next time to run and getting the config info), an abstract DoWork function, and an UpdateTime function (that figures out the next time to run). And the interface that uses, just sets up the DoWork function. Now I did mention that the task loads config information. This, once again, uses Gestalt.Net to load/save our info. The base class for it looks like this:
public class TaskConfiguration:Gestalt.Config<TaskConfiguration>
{
#region Properties
/// <summary>
/// Frequency the task is run
/// </summary>
public virtual RunTime Frequency { get; set; }
/// <summary>
/// Start date
/// </summary>
public virtual DateTime Start { get; set; }
/// <summary>
/// End date
/// </summary>
public virtual DateTime End { get; set; }
/// <summary>
/// Next run time
/// </summary>
public virtual DateTime NextRunTime { get; set; }
#endregion
}
That's it. All the system cares about is if this has a start/end time period, the next time it should run (which it updates itself), and the frequency it should run (hourly, daily, weekly, etc.). And that's all there is to the entire system, It's very basic but surprisingly flexible. As an example, we can set up a task by simply doing the following:
public class Task1:EchoNet.Task
{
public override void DoWork()
{
try
{
Utilities.FileManager.SaveFile("This is a test", @"C:\MyFiles\File1.txt");
}
catch { throw; }
}
}
Or at least that's it for the task itself. We still need to set up the config files:
public class TaskSchedulerConfig:EchoNet.Configuration.Configuration
{
}
[Gestalt.Attributes.Config(Name = "Task1")]
public class Task1Config : EchoNet.Configuration.TaskConfiguration
{
public Task1Config()
{
NextRunTime = new DateTime(2010, 5, 20, 11, 11, 0);
Start = new DateTime(1900, 1, 1);
End = DateTime.MaxValue;
Frequency = EchoNet.Enum.RunTime.Hourly;
}
protected override string ConfigFileLocation { get { return @"C:\TestWebApp\App_Data\Task.config"; } }
}
You may have noticed that the first item is simply an empty class. It inherits from our main config file for the task manager and just lets the standard 4 threads be the default. In the second class, we're inheriting from the individual task's config object. In this case, because we never named the base class, we have to set the name in the attribute (this associates this config with the Task1 class. We then set some defaults in the constructor and override the ConfigFileLocation property to set a path for the config to be saved (so we can edit it outside of the code if need be). That's it to the configuration, from here all we have to do is actually start it up:
Gestalt.Manager.Instance.RegisterConfigFile(typeof(Default).Assembly);
EchoNet.Manager.Instance.Start(typeof(Default).Assembly);
The two function calls are rather simple. First we start Gestalt.Net, telling it where our config classes are and then the same thing with the task manager (EchoNet is the namespace for it). In a web app this would go in the global Application_Start function. And finally we need to stop it:
EchoNet.Manager.Instance.Stop();
And this would go in your Application_Stop function. Anyway, that's it. The start would load up the tasks, start the threads, and let things run. The Stop would simply stop the threads and shut things down. Really simple and rather effective, so take a look, leave feedback, and happy coding.
67f3b94a-0f27-46e1-b10a-70d7ee2bbac0|0|.0