Un task scheduler în .NET sau cum să aleg o componentă open source

De curând, la un mic pet project la care lucram de ceva vreme, am avut nevoie de un task scheduller – după câteva încercări de a meșteri ceva pe cont propriu, mi-am dat seama că în ultimii nouă ani am mai facut asa ceva (în aplicații comerciale) de vreo două ori, și nu e trivial. Cum din motive evidente nu mai pot folosi acele componente în afara firmei, și cum nu am timp sa reinventez roata, am început să ma uit în jur după ceva gata făcut.

Ne fiind vorba să fac vreun profit din jucaria de aplicație la care lucram, zgârcit de felul meu, m-am apucat să mă uit după ceva gratis.
Totul pleacă de la requirements, și în acest caz ele ar fi ceva de genul:
– un task scheduller simplu care, într-o aplicație server-side (să zicem Windows service sau Azure worker role, nu contează), care să-mi permită să:
– execut periodic diverse acțiuni/taskuri
– eventual în paralel
– taskurile să poata fi definite în .NET ca parte a proiectului (nu executabile externe)
– definiția și starea taskurilor să poată fi persistată
– dacă aplicația e oprită temporar să știe să reia execuția
– simplu de folosit, fără deployment complex, cât mai independent de componente externe
Ce nu intră la requirements:
– workflows și alte minuni

După câteva căutari am găsit ceva ce sună interesant: Quartz.NET.

Hai să vedem dacă merită folosit:

– poate fi instalat usor?

PM> Install-Package Quartz
'Common.Logging (≥ 1.2.0)' not installed. Attempting to retrieve dependency from source...
Done.
Successfully installed 'Common.Logging 1.2.0'.
Successfully installed 'Quartz 1.0.3'.
Successfully added 'Common.Logging 1.2.0' to TestQuartzNet.
Successfully added 'Quartz 1.0.3' to TestQuartzNet.

.. se pare că da

– e open-source? – da (http://quartznet.sourceforge.net/license.html)
– are documentație și un tutorial? – așa se pare (http://quartznet.sourceforge.net/tutorial/index.html)
– s-au obosit să creeze un changelog și release notes după fiecare versiune? – yep (https://github.com/quartznet/quartznet/blob/master/changelog.txt)
– au un forum/mailing list activ, ceva de genul, unde pot pune întrebări? – da (http://groups.google.com/group/quartznet)
– proiectul mai e activ? – da, din câte se vede pe Github

Next, o scurtă privire peste calitatea componentei:
– au unit teste? da: https://github.com/quartznet/quartznet/tree/master/src/Quartz.Tests.Unit
– ceva integration tests? asa se pare: https://github.com/quartznet/quartznet/tree/master/src/Quartz.Tests.Integration
– au reinventat roata fără să se uite în jur? Se pare că nu – componenta e un port în .NET a unei componente de success din Java, Quartz, de la aceeași sursă ce produce Terracota – un nume cunoscut în lumea Java.
Ne place sau nu, .NET a intrat un pic mai târziu în joc, și unele din cele mai bune și de success componente (open source) din lumea .NET sunt portate sau inspirate din componente similare Java: NHibernate, NUnit, CruiseControl.NET, Spring.Net etc. – ceea ce nu e rău – o arhitectură sau idee de success, acolo unde e cazul, nu merită recreată doar de dragul originalității.
La componente comerciale povestea e puțin diferită, normal.

Dar destul cu vorbăria, ar trebui să văd un pic de cod.
O metodă simplă de a vedea dacă o componentă e funcțională, dar și pentru a vedea niște exemple de cum trebuie folosită, e să arunc o privire peste unit teste. Cum sunt leneș și nu vreau să compilez sursele, am “extras” proiectul cu unit teste din .zip-ul de la http://sourceforge.net/projects/quartznet/files/quartznet/Quartz.NET%201.0.3/, l-am mutat în soluția proprie, și am modificat referințele a.î. să pointeze la assembly-urile aduse cu NuGet.
Un quick run la toate testele, și arată mulțumitor deocamdată:
123 passed, 1 failed, 16 skipped (see ‘Task List’), took 10.57 seconds (NUnit 2.4).

Comparat cu numarul de teste, ruleaza suficient de rapid, ceea ce e un indiciu ca s-ar putea să fie ok scrise.

Cum scopul meu nu e să fac un code review, și plecând de la unit teste risc să mă pierd în diverse scenarii avansate, nu ar strica niște exemple la obiect. Din fericire, autorii s-au obosit să creeze și așa ceva: Quartz.Examples:

Quartz examples run

Exemplele sunt bine comentate, chiar dacă codul e self-describing (un alt criteriu important – ideal codul nu ar trebui să aibă nevoie de multe comentarii pentru a putea fi înteles:

  1. public class SimpleJob : IJob
  2.     {
  3.         private static ILog _log = LogManager.GetLogger(typeof(SimpleJob));
  4.         ///<summary>
  5.         /// Empty constructor for job initilization.
  6.         ///</summary>
  7.         public SimpleJob()
  8.         {
  9.         }
  10.         ///<summary>
  11.         /// Called by the <see cref=”IScheduler” /> when a
  12.         ///<see cref=”Trigger” /> fires that is associated with
  13.         /// the <see cref=”IJob” />.
  14.         ///</summary>
  15.         public virtual void  Execute(JobExecutionContext context)
  16.         {
  17.             // This job simply prints out its job name and the
  18.             // date and time that it is running
  19.             string jobName = context.JobDetail.FullName;
  20.             _log.Info(string.Format(“SimpleJob says: {0} executing at {1}”, jobName, DateTime.Now.ToString(“r”)));
  21.         }
  22.     }

Și niște exemple:

  1. ILog log = LogManager.GetLogger(typeof (SimpleTriggerExample));
  2. // First we must get a reference to a scheduler
  3. ISchedulerFactory sf = new StdSchedulerFactory();
  4. IScheduler sched = sf.GetScheduler();
  5. // jobs can be scheduled before sched.start() has been called
  6. // get a “nice round” time a few seconds in the future…
  7. DateTime ts = TriggerUtils.GetNextGivenSecondDate(null, 15);
  8.  
  9. // job6 will run indefinitely, every 50 seconds
  10. JobDetail job = new JobDetail(“job6”, “group1”, typeof (SimpleJob));
  11. trigger = new SimpleTrigger(“trigger6”, “group1”, “job6”, “group1”, ts, null,
  12.                             SimpleTrigger.RepeatIndefinitely, TimeSpan.FromSeconds(50));
  13. ft = sched.ScheduleJob(job, trigger);
  14. log.Info(string.Format(“{0} will run at: {1} and repeat: {2} times, every {3} seconds”,
  15.     job.FullName, ft.ToString(“r”), trigger.RepeatCount, trigger.RepeatInterval.TotalSeconds));
  16. log.Info(“——- Starting Scheduler —————-“);
  17. // All of the jobs have been added to the scheduler, but none of the jobs
  18. // will run until the scheduler has been started
  19. sched.Start();
  20.   
  21. // jobs can also be scheduled after start() has been called…
  22. // job7 will repeat 20 times, repeat every five minutes
  23. job = new JobDetail(“job7”, “group1”, typeof (SimpleJob));
  24. trigger = new SimpleTrigger(“trigger7”, “group1”, “job7”, “group1”, ts, null, 20, TimeSpan.FromMinutes(5));
  25. ft = sched.ScheduleJob(job, trigger);
  26. log.Info(string.Format(“{0} will run at: {1} and repeat: {2} times, every {3} seconds”,
  27.     job.FullName, ft.ToString(“r”), trigger.RepeatCount, trigger.RepeatInterval.TotalSeconds));
  28. // jobs can be fired directly… (rather than waiting for a trigger)
  29. job = new JobDetail(“job8”, “group1”, typeof (SimpleJob));
  30. job.Durable = (true);
  31. sched.AddJob(job, true);
  32. log.Info(“‘Manually’ triggering job8…”);
  33. sched.TriggerJob(“job8”, “group1”);
  34. sched.Shutdown(true);

Pare destul de clean, clasele au responsabilități clare: IJob – acțiunea/taskul custom în sine, JobDetail – diverse atribute asociate cu instanța unui job
Trigger – definește un “schedule”, modul în care un job va fi executat (când, cat de des etc.)
Scheduller – responsabil pentru executia efectivă a job-urilor.

Nu o să intru aici în detalii legate de Quartz.Net, cum scopul nu e să scriu un tutorial – din fericire există un tutorial foarte bun deja facut: http://quartznet.sourceforge.net/tutorial/index.html

Cum totuși e o componenta externă, nu ar strica să fie usor de mock-uit la nevoie in unit teste – să vedem dacă oferă interfețe pentru principalele clase..
ISchedulerFactory, IScheduler, IJob – fain.

Ce aș mai verifica înainte de a mă declara satisfăcut de componentă: cateva teste de load și performance, câteva teste pentru partea de error handling (ex.: ce se întamplă când un job aruncă o excepție, când crapă procesul host, când se umple thread pool-ul etc.). Nu în ultimul rând, aș arunca o privire peste codul sursă să vad cât de îngrijit e scris.

Disclaimer: nu știu dacă e cea mai bună componentă pentru așa ceva – probabil sunt altele mai bune. Ideea era să arat cam care ar fi pașii care îi urmez cand aleg o componentă gratis sau open source. Când e vorba de un proiect comercial pe bune, povestea se schimbă într-o oarecare masură: ce buget se poate aloca pentru componente thrid-party, customer support, licensing, company policies (în multe firme se incearcă standardizarea componentelor folosite, nu fiecare proiect să foloseasca ceva diferit) etc.

Advertisements
This entry was posted in .NET, General and tagged , , , , . Bookmark the permalink.

4 Responses to Un task scheduler în .NET sau cum să aleg o componentă open source

  1. Andrei Ignat says:

    Super! Pot sa folosesc de la tine metoda?

  2. Timotei says:

    Super. Și eu am căutat câteva zile tot un task scheduler, și tot la Quartz am ajuns 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s