Jednoduchý scheduler v .NETu

Václav Dajbych       03.09.2015       C#, Azure, .NET       15333 zobrazení

Asi to znáte – máte nějaký složitější systém na zpracování velkého objemu dat a čas od času potřebujete vykovat nějakou automatizovanou údržbu – typicky smazat všechny položky starší než několika dní. Možností, jak toho dosáhnout, je hodně. Snažil jsem se vymyslet něco jednoduchého a efektivního.

Než začnu popisovat, jakým způsobem funguje můj scheduler, se kterým jsem vešel do 40 řádků kódu, popíšu, jaké jiné možnosti jsou a proč jsem je neshledal jako vhodné. Univerzální nejlepší řešení neexistuje, vše samozřejmě záleží na konkrétním projektu. Tento scheduler používám v Azure Cloud Service.

Alternativní přístupy

Naplánovaná úloha

Pokud se má ve Windows periodicky spouštět nějaký kód, je nesmysl dělat na to proces, který bude 99% času čekat a ve zbylém čase něco dělat. K tomu slouží Plánovač úloh. Jelikož jsem ale potřeboval běh v Azure Cloud Service, hledal jsem řešení čistě v prostředí .NET.

Azure Scheduler a Storage Queue

Dobrá volba je nakonfigurovat si Job Action ve Scheduleru, který periodicky vkládá zprávy do Storage Queue. Tyto zprávy pak odebírá Worker Role a vykoná kýženou úlohu.

Je velká škoda, že se automaticky nenastaví životnost zprávy na délku periody, se kterou se úloha vykonává. Když je totiž životnost zprávy delší než perioda, mohou se ve frontě vyskytnout dvě zprávy, které spustí ihned po sobě tutéž úlohu, a to nechceme. Pokud je perioda delší než 1 týden, což je výchozí životnost zprávy, nevadí to. Pokud je ale kratší, třeba jedna minuta, úloha se po updatu, který zabere čtvrt hodiny, vykoná 15 krát.

Výhoda Sheduleru spočívá v tom, že si drží přehled o tom, kolik úloh se vykovalo úspěšně a kolik neúspěšně. Dalším plusem je integrace retry policy.

System.Threading.Thread.Sleep

Spustit vlákno, vykonat úlohu, počkat, vykovat úlohu, zase počkat a tak pořád ve smyčce není ničím, co by vysloveně nefungovalo. Pokud ale chci úlohu vykovat každý den ve 3 hodiny ráno, nezávisle na tom, kdy se program spustí, už to chce nějakou logiku navíc.

System.Threading.Timer

Timer je užitečný pomocník, ale pokud se úloha vykovává déle, než je jeho perioda, zahltí nakonec celý virtuální stroj. Ne, že by se s tím nedalo něco dělat, ale opět to vyžaduje logiku navíc.

Požadavky

Co vlastně po takovém scheduleru chceme?

  • Opakované spouštění asynchronní úlohy.
  • Perioda nebude delší, než 24 hodin.
  • Spouštění úlohy v konkrétním čase (např. vždy ve 3 hodiny ráno).
  • Jistotu, že se další úloha nespustí dříve, než se předešlá dokončí.
  • Zrušení naplánovaných úloh přes CancellationToken.
  • Závislost pouze na platformě .NET.

Zdrojový kód

Scheduler je postavený na třídě Timer. Celý um spočívá ve dvou věcech.

  • Podle toho, v kolik hodin se má úloha spustit, si spočítáme, za jak dlouho se má Timer vzbudit.
  • Naplánujeme vždy jen jedno probuzení. Další naplánujeme až poté, co úloha skončí.
public class Scheduler {

    private readonly Timer timer;
    private readonly Func<Task> work;
    private readonly CancellationToken cancellationToken;
    private readonly TimeSpan period;
    private readonly TimeSpan utcStartupTime;

    public Scheduler(Func<Task> work, TimeSpan period, TimeSpan utcStartupTime, CancellationToken cancellationToken) {
        if (work == null) throw new ArgumentNullException(nameof(work));
        if (utcStartupTime.Ticks < 0 || utcStartupTime.Ticks > TimeSpan.TicksPerDay) throw new ArgumentOutOfRangeException(nameof(utcStartupTime));
        if (period.Ticks < 0 || period.Ticks > TimeSpan.TicksPerDay) throw new ArgumentOutOfRangeException(nameof(period));
        if (cancellationToken == null) throw new ArgumentNullException(nameof(cancellationToken));
        this.work = work;
        this.cancellationToken = cancellationToken;
        this.period = period;
        this.utcStartupTime = utcStartupTime;
        timer = new Timer(Callback);
        ScheduleTimeout();
        ThreadPool.RegisterWaitForSingleObject(cancellationToken.WaitHandle, Cancellation, null, -1, true);
    }

    private void Callback(object state) {
        work().Wait();
        ScheduleTimeout();
    }

    private void ScheduleTimeout() {
        if (cancellationToken.IsCancellationRequested) return;
        var utcTimeOfDay = DateTime.UtcNow.TimeOfDay;
        var dueTime = TimeSpan.FromTicks(((TimeSpan.TicksPerDay + utcStartupTime.Ticks - utcTimeOfDay.Ticks) % TimeSpan.TicksPerDay) % period.Ticks);
        timer.Change(dueTime, Timeout.InfiniteTimeSpan);
    }

    private void Cancellation(object state, bool timedOut) {
        timer.Change(Timeout.Infinite, Timeout.Infinite);
    }

}

Použití

A jak se Scheduler používá? Předáme mu, co se má spouštět, jak často, v kolik hodin a kdy to celé má skončit. Můžeme si to vyzkoušet v konzolové aplikaci:

private static CancellationTokenSource cts = new CancellationTokenSource();
private static int i = 0;

public static void Main(string[] args) {
    var sch = new Scheduler(Work, TimeSpan.FromSeconds(2), new TimeSpan(12, 0, 0), cts.Token);
    cts.Token.WaitHandle.WaitOne();
}

private static async Task Work() {
    Console.WriteLine($"{DateTime.Now.TimeOfDay} Work started.");
    Interlocked.Increment(ref i);
    Console.WriteLine(i);
    await Task.Delay(TimeSpan.FromSeconds(5));
    if (i == 5) cts.Cancel();
    Console.WriteLine($"{DateTime.Now.TimeOfDay} Work completed.");
}

Úloha se má spouštět každé 2 sekundy ale tak, aby se spustila v poledne (což v tomto konkrétním případě znamená, že se bude spouštět v každé sudé sekundě). Trvá ale 5 sekund, takže se bude spouštět každých 6. Po 5. spuštění vyvolá signál, který zruší další spuštění a ukončí celou aplikaci.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

 

 

Nový příspěvek

 

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

Vhodnost použití

Ahoj, díky za příspěvek. Nicméně vidím v něm několik problémů a proto budu trochu prudit.

1. absolutně chybí ošetřování výjimek (chyba se nepropíše, nenaplánuje se další spuštění)

2. jedno vlákno je zabité zbytečně čekáním na cancellation (WaitForCancellation) - lze použít například https://msdn.microsoft.com/en-us/library... (čekání na WaitHandle cancellation tokenu)

3. proč psát něco, co už existuje? https://github.com/quartznet/quartznet

nahlásit spamnahlásit spam 0 / 2 odpovědětodpovědět

Já bych jen navázal na bod 3... začátkem roku jsem shodou okolností napsal článek s demem o použití Quartz.NET společně s Worker Rolí na MS Azure. https://www.miroslavholec.cz/blog/azure-...

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

S Quartz .NET mám také relativně dobré zkušenosti. Často používám také Hangfire.io

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

Konkrétní implementace se odvíjí od konkrétní aplikace.

1. Předpokládám, že metoda work ošetřené výjimky má a tím pádem work() nikdy výjimku nevyhodí.

2. To je pravda. Stálo by za to ten kód ještě vylepšit.

3. Protože je někdy rychlejší si jednoduché věci napsat sám, než hledat knihovny, které umí přesně to, co člověk potřebuje. Také nemám rád knihovny přepsané z Javy (výjimku tvoří Bouncy Castle). Nevyužívají plně všech vlastností jazyka C#.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

TIRO

cituji: "typicky smazat všechny položky starší než několika dní."

Pokud se jedná například o MS SQL server? Na to by stačilo napsat nějaký JOB a ten spouštět taky periodicky.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět

To ano, na vše jde také napsat konzolovka a normálním schedulerem ji pouštět. Jsou ale případy, kdy například chcete dynamicky měnit naplánování, případně přenastavit využití threadů, podsunout jiná data a to vše například v reakci na nějaký event, například na základě toho, co udělal jiný naplánovaný job.

Sšeho jste schopen docílit i pomocí X konzolových aplikací, Thread.sleepu, modifikace externích konfiguračních souborů, pomocí nějaké knihovny na ovládání windows schedulera atd. Bylo by to tedy velmi pracné a komplikované. A co pokud chcete tu práci nějak distribuovat? Pokud chcete například 2 tyto úlohy synchronizovat, že například nemohou běžet současně, nebo že musí čekat 3. na dokončení těch 2 (případně že nemůže běžet, pokud běží 1 a současně 2)?

Zkrátím to,... nějaký framework typu Quartz .NET je jen jedna z možností, někde je to ohromný overkill (vzpomeňte na případ konzolovka + periodický scheduler), jinda je to skvělá věc. Je to tedy případ od případu.

nahlásit spamnahlásit spam -1 / 1 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

Příspěvky zaslané pod tento článek se neobjeví hned, ale až po schválení administrátorem.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback