Miroslav Holec

Software & Cloud Architect

miroslavholec.cz / blog / azure-worker-role-a-quartz-net

Azure Worker role a Quartz.NET

Miroslav Holec

Publikován 16. ledna 2015 , aktualizace: 29. března 2016 | Cloud Services, Web

Tento článek je starší 18 měsíců a je proto možné, že popisuje postupy nebo technologie, které v uplynulé době mohly doznat výraznějších změn. Názory a myšlenky v tomto článku již nemusí vyjadřovat současné stanovisko autora nebo autorů. Článek byl napsán 16. ledna 2015.

Pokud hledáte efektivní způsob spouštění naplánovaných úloh, který bude svázán s webovou aplikací a zároveň používáte Windows Azure Cloud Services, není nic snazšího, než vytvořit Worker Roli. V článku vysvětlím jak Worker Role funguje a jak ji rozšířit, aby se dala využít k spouštění více úloh s pomocí Quartz.NET.

K čemu je Worker Role

Při zakládání nové služby v prostředí Visual Studia je celá řada možností. Asi znáte standardní Web Roli, která nedělá nic jiného, než že vytváří prostředí pro běh nějaké webové aplikace (ať už MVC / WebForms / jiné). Oproti tomu Worker Role je určená k periodickému spouštění úloh.

Vytvoření Worker Role

Pro vytvoření Worker Role s projektem stačí ve Visual Studio solution kliknout pravým myšítkem na Azure Cloud Service a poté zvolit New Worker Role Project. Pro naše potřeby bude stačit následně zvolit první možnost, tedy Worker Role a tuto roli nějak pojmenovat. Pokud ještě nemáte vytvořený Azure Cloud Service projekt, dá se to udělat přes Add -> New Project přímo na solution.

Možnost nastavení role

Ve složce Roles je nyní naše nově vytvořená role. Opět na ni můžeme kliknout pravým myšítkem a zvolit Properties. Objeví se vizuální průvodce, který umožňuje provést různá nastavení, která se ukládají do csdef a cscfg souborů. Na záložce Configuration je možné nastavit počet instancí, které budou vytvořeny a jejich velikost. Velikost instance (XS, S, M...) v podstatě určuje přidělenou operační paměť a procesor. Pro experimentální účely a nedlouhotrvající rutinní úlohy běžně stačí jedna instance o velikosti ExtraSmall. Další nastavení je možné provést přímo ve správě Azure. Ceny jsou uvedeny na Azure Pricing kalkulačce.

Infrastruktura Worker Role

Worker Role má jednoduchou infrastrukturu, jejíž chování lze snadno oddebugovat ve Visual Studiu pomocí Compute Emulatoru. Pro debugování je nutné zvolit jako startup projekt cloudovou službu, nikoliv klasický webový projekt. Podívejme se ještě, jaký je životní cyklus worker role:

  1. Upload cspkg + cscfg (balíček s aplikací a konfigurace)
  2. Nastavení virtuálního stroje
  3. Spuštění Startup Tasks
  4. Konfigurace IIS (IISConfiguration.exe)
  5. Volání konstruktoru třídy implementující RoleEntryPoint
  6. Volání metody OnStart()
  7. Volání metody Run()
  8. Průběžné checkování spuštěné instance, recyklace (Ready / Recycle)
  9. Volání metody OnStop() při zastavení role

V rámci vývoje Worker Role nás zajímají hlavně body 5-8, které podrobně popíšu níže.

OnStart()

Metoda OnStart() je spuštěna po úspěšném vytvoření stroje a instalaci úloh, eventuelně konfiguraci IIS. Dokud není tato metoda provedena, stav instance můžeme vidět na Azure jako Busy. Pokud dojde k chybě a nepovede se start aplikace, Azure zastavuje instanci a volá se rovnou metoda OnStop(). V případě, že všechno v metodě OnStart() proběhne v pořádku, přechází instance do stavu Ready a role začíná přijímat požadavky od load balanceru. Volá se matoda Run().

Run()

Tato metoda je myšlena jako místo, kde se má provést kód naplánované úlohy. V případě, že metoda Run() vrátí hodnotu, volá se okamžitě OnStop(). Obvykle takové chování není žádoucí. Přejeme si, aby se v metodě provádělo na základě nějakého algoritmu periodické spouštění různých úloh. Jinak bychom takových worker rolí museli mít skutečně mnoho a řešení by nebylo příliš levné. Proto se vnitřní kód implementuje obvykle vytvořením nekonečného cyklu nebo se ukončení záměrně znemožní poté co je nastaveno opakované spouštění úloh.

OnStop()

Metoda OnStop() se volá v případě chyby provádění metody OnStart() nebo obecně při zastavování instance (z jakéhokoliv důvodu). Metoda OnStop() tak může být využita pro různé "úklidové akce". Její vykonání je nicméně časově limitováno, protože veškeré stop operace mají na provedení celkem 5 minut, poté jsou Azurem "zaříznuty". Stav instance je v Azure vidět jako Stopping. Po provedení metody dochází k recyklaci a následně novému spuštění krok za krokem, tak jak jsem popsal výše.

Návrh implementace

Se znalostmi výše se lze pustit do samotné implementace. Řešení je celá řada a vždy záleží na konkrétní situaci. Příklad níže spíše demonstruje princip, na jakém Worker Role funguje a jak jej nastavit právě s plánovačem Quartz.NET. Tato implementace je navržena pouze pro jednu instanci. Pokud bychom chtěli používat více instancí, pak by bylo potřeba ještě vyřešit konkurenci, aby nedocházelo ke konfliktům a úlohy nebyly zpracovány více instancemi v jeden okamžik.

Instalace Quartz.NET

Quartz můžeme přidat do aplikace instalací nuget balíčku:

Install-Package Quartz

Nebudu zde popisovat, jak funguje a jak jej nastavit, protože je to dokonale popsáno v dokumentaci nástroje Quart.NET a příklad níže bude myslím dost srozumitelný.

IWorkerJob.cs

Každá úloha je reprezenzována třídou a měla by realizovat rozhraní IJob. To vyžaduje, aby všechny třídy implementovali metodu Execute(IJobExecutionContext context), která je spouštěna Schedulerem. Pro naše účely si vytvoříme speciální rozhraní, které usnadní plánování a přidáme ještě obslužní metodu Stop().

public interface IWorkerJob : IJob
{
	int ExecutionIntervalInSeconds { get; }
	void Run();
	void Stop();
}

ExampleJob.cs

Následně můžeme připravit ukázkovou úlohu, která bude realizovat naše rozhraní a kterou budeme periodicky spouštět každých 60 sekund. Při zastavení Worker Role se bude spouštět metoda Stop().

public class ExampleJob : IWorkerJob
{
	private readonly DataContext db;

    public int ExecutionIntervalInSeconds
    {
        get { return 5; }
    }

	// tato metoda slouží pro odpálení Schedulerem
    public void Execute(IJobExecutionContext context)
    {
		try {
			Run();
		}
        catch (Exception ex)
        {
            Trace.TraceError(ex.Message);
        }
	}

	public void Run()
	{
		// provede spuštění úlohy
	}

	public void Stop()
	{
		// úklidové akce

        if (db != null)
        {
            db.Dispose();
        }
	}
}

WorkerRole.cs

To nejhlavnější se musí odehrát v implementaci RoleEntryPoint. Naším cílem je:

  • obsloužit metodu OnStart()
  • zajistit spouštění úloh uvnitř Run()
    • získat reference na všechny úlohy
    • naplánovat úlohy pomocí Quartz.NET
    • poté zastavit zpracování Run() metody, aby mohl Quartz bez přerušení "tikat"
  • obsloužit metodu OnStop()
    • projít všechny úlohy a zavolat nad nimy Stop()

OnStart()

Nejprve začneme tímto kódem. Ten zajistí nastavení Scheduleru a nalezení všech úloh na základě interface. To vše se stane uvnitř metody OnStart(). Já pro nalezení úloh používám C# reflexi, nicméně použít se dá i nějaký IoC kontejner.

public class WorkerRole : RoleEntryPoint
{
    private const string JobSuffix = "Job";
    private const string TriggerSuffix = "Trigger";

    private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);
    private readonly List<IWorkerJob> workerJobs = new List<IWorkerJob>();
    private IScheduler scheduler;

    public override bool OnStart()
    {
        SetupQuartz();
        FindWorkers();

        return base.OnStart();
    }

    private void SetupQuartz()
    {
        ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
        scheduler = schedulerFactory.GetScheduler();
        scheduler.Start();
    }

    private void FindWorkers()
    {
        Type interfaceType = typeof(IWorkerJob);

        List<Type> implementations = AppDomain.CurrentDomain.GetAssemblies()
			.SelectMany(s => s.GetTypes())
			.Where(p => interfaceType.IsAssignableFrom(p) && !p.Name.StartsWith(interfaceType.Name)).ToList();

        implementations.ForEach(x => workerJobs.Add((IWorkerJob)Activator.CreateInstance(x)));
    }
}

Run()

Další kód už řeší samotné naplánování úloh uvnitř metody Run() pomocí Quartz.NET. V podstatě se jen vezmou nalezené úlohy a ty se předají v cyklu metodě Schedule(IWorkerJob). Ta vezme typ úlohy, vytvoří na základě něj job, trigger a nakonec provede naplánování.

public override void Run()
{
    foreach (IWorkerJob workerJob in workerJobs)
    {
        try
        {
            Schedule(workerJob);
        }
        catch (Exception ex)
        {
            Trace.TraceError(ex.Message);
        }
    }
	runCompleteEvent.WaitOne();
}

private void Schedule(IWorkerJob workerJob)
{
    Type workerType = workerJob.GetType();

    IJobDetail job = JobBuilder.Create(workerType)
        .WithIdentity(string.Concat(workerType.Name, JobSuffix))
        .Build();

    ITrigger trigger = TriggerBuilder.Create()
        .WithIdentity(string.Concat(workerType.Name, TriggerSuffix))
        .ForJob(job)
        .StartNow()
        .WithSimpleSchedule(
            x =>
                x.RepeatForever()
                .WithIntervalInSeconds(workerJob.ExecutionIntervalInSeconds))
        .Build();

    scheduler.ScheduleJob(job, trigger);
}

OnStop()

Při volání metody OnStop() se začne uklízet. Nejprve se všechny úlohy plánovače zastaví, zavolá se nad nimy metoda Stop() a nakonec se plánovač vypne. Metoda OnStop() se dokončí a probíhá recyklace. Tato část by si ještě zasloužila dopracovat, protože metoda Stop() se v této implementaci volá vždy bez ohledu na to, zda úloha skutečně běží. Záleží samozřejmě na tom, jaká je myšlenka Stop() metod.

public override void OnStop()
{
    scheduler.PauseAll();
    workerJobs.ForEach(x => x.Stop());
    scheduler.Standby();

    runCompleteEvent.Set();

    base.OnStop();
}

Demo

Celý příklad si můžete stáhnout jako ZIP balíček.

Závěr

Realizace spouštění naplánovaných úloh pomocí Worker Role je intuituvní. V praxi bývá často Quartz.NET nahrazen nějakým pozorovatelem, který může úlohy také spouštět. Výhoda Quartz.NET je dle mého názoru v tom, že v metodě Run() WorkerRole.cs je možné pracovat i s datovým úložištěm a úlohy spouštět sofistikovaněji podle různých plánů.

Potřebujete pomoci?

Líbil se Vám článek? Máte dotaz nebo chcete v této oblasti s něčím pomoci? Neváhejte se na mě obrátit.

mirek@miroslavholec.cz

  • Řešení vývojářských problémů
  • Konzultace
  • Firemní školení a workshopy