Ostatnio w moim projekcie pojawił się problem wywoływania konkretnych zadań w określonym czasie wybieranym przez użytkownika. Pierwsze co przyszło mi do głowy to MSMQ, jednak aplikacja asp.net, znajduje sięna hostingu i nie będę mógł tam postawic sobie tego narzędzia :/
Szukałem dalej i w odpowiedziach zaczeła przwijac się nazwa nieznanej mi do tej pory biblioteki - Quartz.Net. To kolejna biblioteka która pojawiła się w środowisku .netowym od Javy.
Oto co potrafi:
- Działanie jako serwis windowsowy lub w samej aplikacji
- Planowanie zadań
- Wykonywanie zadań
- Trwałość zadań (przechowywanie ich w RAM-ie lub bazie danych)
- Clustering
- Listeners & Plug-ins
Implementacja biblioteki zawiera się w paru krokach:
- Dodajemy biblioteke ściągniętą z quartznet.sourceforge.net
- Dodajemy konfiguracje pliku we.config
- Implementujemy własny Job
- Dodajemy do Global.asax aby Scheduler zaczął pracę
Aby dodać własnego Job-a należy dziedziczyć z klasy Job
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Quartz;
public class Job : IJob
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#region IJob Members
public void Execute(IJobExecutionContext context)
{
// This job simply prints out its job name and the
// date and time that it is running
JobKey jobKey = context.JobDetail.Key;
log.InfoFormat(“SimpleJob says: {0} executing at {1}”, jobKey, DateTime.UtcNow.ToString());
}
#endregion
}
W metodzie Execute dodajemy to co chcielibyśmy aby się wykonało. Parametr context pozwala nam podczas wykonywania zadania sprawdzić jego parametry jak np key. Ja w przykładzie wykorzystuje bibliotekę log4net aby zalogować są informacje, że zadanie zostało wykonane i o jakiej porze.
Zarządzaniem zadaniami zajmuje się Scheduler. Dobrze jest stworzyć klasę obsługująca go jako singleton:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Quartz.Impl;
using Quartz;
using System.Collections.Specialized;
public class Scheduler
{
static Scheduler()
{
NameValueCollection properties = new NameValueCollection();
properties["quartz.scheduler.instanceName"] = “myApp”;
properties["quartz.scheduler.instanceId"] = “MyApp”;
properties["quartz.threadPool.type"] = “Quartz.Simpl.SimpleThreadPool, Quartz”;
properties["quartz.threadPool.threadCount"] = “10″;
properties["quartz.threadPool.threadPriority"] = “Normal”;
properties["quartz.scheduler.instanceName"] = “TestScheduler”;
properties["quartz.scheduler.instanceId"] = “instance_one”;
properties["quartz.jobStore.type"] = “Quartz.Impl.AdoJobStore.JobStoreTX, Quartz”;
properties["quartz.jobStore.useProperties"] = “true”;
properties["quartz.jobStore.dataSource"] = “default”;
properties["quartz.jobStore.tablePrefix"] = “QRTZ_”;
// if running MS SQL Server we need this
properties["quartz.jobStore.lockHandler.type"] = “Quartz.Impl.AdoJobStore.UpdateLockRowSemaphore, Quartz”;
properties["quartz.dataSource.default.connectionString"] = “Server=localhost;Database=db;Uid=lukas;Pwd=Pass”;
properties["quartz.dataSource.default.provider"] = “SqlServer-20″;
_schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = _schedulerFactory.GetScheduler();
}
public static IScheduler GetScheduler()
{
return _scheduler;
}
private static readonly ISchedulerFactory _schedulerFactory;
private static readonly IScheduler _scheduler;
}
W kolekcji properties znajduje się konfiguracja dla biblioteki. Może się ona również znaleźć w pliku web.config, jednak zauważyłem, że podawanie jej bezpośrednio w konstruktorze SchedulerFactory sprawdza się lepiej.
W moim przykładzie wykorzystuje baze danych jako przechowalnie dla moich zadań. Można również uzyć pamęci do tego i użyć jako wartość dla jobStore.type “ ”Quartz.Impl.RAMJobStore, Quartz”;”. Dla mnie baza danych jest pewniejsza, gdyż dane będą tam bezpieczne nawet po restacie pool-i aplikacji. Dodatkowe wartości to prefix tabel i connection sting bazy. Co należy wspomnieć to, że skrypt sql generujący tabelki dla użytku Quartz.net znajdziemy w folderze database w plikach ściągniętych z strony biblioteki.
Dodatkowo w Global.axax startujemy naszego Schedulera:
void Application_Start(object sender, EventArgs e)
{
Scheduler.GetScheduler().Start();
}
można również wyłączać go gdy aplikacja się zamyka.
void Application_End(object sender, EventArgs e)
{
Scheduler.GetScheduler().Shutdown();
}
A oto najważniejsze dodawanie zadań do schedulera:
DateTime SelectedDate = this.Calendar1.SelectedDate;
int hour = Convert.ToInt32(this.tbHour.Text);
int minute = Convert.ToInt32(this.tbMinute.Text);
int second = Convert.ToInt32(this.tbSecond.Text) – 1;
DateTime startTime = new DateTime(SelectedDate.Year, SelectedDate.Month, SelectedDate.Day, hour, minute, second, DateTimeKind.Local);
DateTimeOffset runTime = DateBuilder.EvenSecondDate(startTime);
IJobDetail job = JobBuilder.Create<Job>()
.WithIdentity(“job” + Guid.NewGuid().ToString(), “group1″)
.WithDescription(“will run at: ” + runTime.ToString())
.Build();
ISimpleTrigger trigger = (ISimpleTrigger)TriggerBuilder.Create()
.WithIdentity(“trigger” + Guid.NewGuid().ToString(), “group1″)
.StartAt(runTime)
.WithSchedule(SimpleScheduleBuilder.Create())
.Build();
Scheduler.GetScheduler().ScheduleJob(job, trigger);
Używając kontrolki kalendarza pobieram datę, a z texboxów czas wywołania zadania. Używam DateTimeKind.Local aby dostać datę w formacie lokalnym, a następnie za pomocą DateBuilder dostaje datę jako DateTiemOffset, którego oczekuje trigger.
Tworzymy zadanie(Job) z unikalną nazwą i grupą. Jeśli nazwa(key) nie będzie unikalna nie zostanie ona dodana do kolejki, dlatego ja używam Guid-a.
Kolejna sprawa to stworzenie triggera, który wywoła nasze zadanie o konkretnym czasie. Na samym końcu dodajemy zadanie i trigger do schedulera.
Jeśli scheduler został włączony w Global.asax to nasze zadanie zostanie przetwarzane automatycznie , w przeciwnym razie należy włączyć scheduler.
Mam nadzieję, że pomogłem w początkach z biblioteką. Sam co dopiero zaczynam się bawić nią