JSON конфигурация списка задач Hangfire и их runtime обновление
Явное описание задач в коде делает их достаточно неповоротливыми и неудобными в поддержке. Также было бы явным излишеством описывать однотипные задачи.
Было бы удобнее хранить их минимальное описание с параметрами в отдельном файле с JSON или XML разметкой, который бы автоматически подгружался при старте планировщика и далее уже обновлял список при условии, если он был изменен. У известного планировщика Quartz, такая фича идет уже из коробки, но к сожалению, в Hangfire ее нет.
Опишем общий интерфейс задачи.
public interface IJob
{
void Execute(Dictionary<string, string> parameters);
}
Для начала зарегистрируем имеющиеся джобы, удобным нам DI-контейнером. Для примера используем Castle Windsor.
Регистрируем все джобы на базе интерфейса IJob
.
container.Register(Classes.FromThisAssembly().BasedOn<IJob>());
Устанавливаем из NuGet дополнительный пакет:
Install-Package HangFire.Windsor
И подключаем JobActivator
GlobalConfiguration.Configuration.UseActivator(new WindsorJobActivator(container.Kernel));
После того как наши джобы зарегистрированы, приступим непосредственно к механизму конфигурации шедулера.
Создаем файл конфигурации config.json
. И определим формат записи задач.
[
{
"Id": "1",
"Name": "ExampleJob",
"CronExpression": "0 0 * * *",
"Parameters": {
"Parameter1": "Text",
"Parameter2": "123"
}
},…
]
Name
будет выступать как в роли названия джобы, так и названия ее класса.
CronExpression
- Cron
выражения. Parameters
- неограниченный список параметров, которые можно передать в тело джобы.
Создадим метод который будет загружать и сериализовать эти задачи:
private IList<JobItem> GetJobsList()
{
using (var sr = new StreamReader(this.configPath))
{
return JsonConvert.DeserializeObject<IList<JobItem>>(sr.ReadToEnd());
}
}
После того как наш шедулер может получать конфигурацию джоб извне создадим метод который будет добавлять в расписание и обновлять их конфигурацию.
private void UpdateConfiguration()
{
var jobs = this.GetJobsList();
foreach (var item in jobs)
{
var jobType = this.container.Kernel.GetAssignableHandlers(typeof(IJob))
.Single(
h => h.ComponentModel.Implementation.Name
.Equals(item.Name, StringComparison.InvariantCultureIgnoreCase))
.ComponentModel.Implementation;
var job = (IJob) this.container.Resolve(jobType);
RecurringJob.AddOrUpdate(item.Id, () => job.Execute(item.Parameters), item.CronExpression);
}
}
Напишем функцию которая будет наблюдать за файлом конфигурации и обновлять задачи, в случае его изменения.
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
private FileSystemWatcher CreateWatcher()
{
var path = Path.IsPathRooted(this.configPath)
? this.configPath
: Path.Combine(Directory.GetCurrentDirectory(), this.configPath);
var configDir = Path.GetDirectoryName(path) ?? string.Empty;
var extension = Path.GetExtension(path);
var watcher = new FileSystemWatcher
{
Path = configDir,
NotifyFilter = NotifyFilters.LastWrite,
Filter = "*" + extension
};
watcher.Changed += this.OnConfigChanged;
watcher.EnableRaisingEvents = true;
return watcher;
}
private void OnConfigChanged(object sender, FileSystemEventArgs e)
{
if (DateTime.UtcNow < this.lastConfigChange.AddMilliseconds(200))
{
return;
}
this.lastConfigChange = DateTime.UtcNow;
this.UpdateConfiguration();
}
Если происходит изменение файла конфигурации, выполняется событие OnConfigChanged
. И так как оно срабатывает несколько раз, то пропишем условие if (DateTime.UtcNow < this.lastConfigChange.AddMilliseconds(200))
. Этого достаточно, чтобы оно сработало ровно один раз.