Jednou za čas se na oficiálních ASP.NET fórech objeví dotaz, jak řešit dlouhotrvající úlohy ve webové aplikaci. Zkuste si někdy koupit letenku po Internetu – zadáte odkud kam chcete letět a pak asi 30 sekund čekáte, než aplikace najde příslušné spojení. Jak tento proces udělat tak, aby byl uživatelsky přívětivý, a zároveň fungoval správně s ohledem na prostředí webové aplikace?
Jak to řešit úplně blbě
Asi nejhorším řešením je samozřejmě provádět takovou věc synchronně, např. přímo v události Button_Click. Není samozřejmě žádoucí, aby HTTP požadavek trval nějak dlouho – jednak tím blokujete vlákna worker procesu, kterých není neomezený počet. V případě, že dojdou a dlouho není nějaké k dispozici, server začne vracet chybu, což není žádoucí.
Druhou nevýhodou je samozřejmě to, že uživateli se stránka načítá půl minuty a většina uživatelů začne nadávat, být nervózní. Velká část uživatelů začne také zuřivě mačkat F5, čímž si zaděláváte na ještě větší problémy – požadavky se budou opakovat a ještě víc zatěžovat server. Takto tedy rozhodně ne.
V jiném vlákně
Jak už mnozí správně tušíte, dlouhotrvající požadavky jest radno obsluhovat v jiném vlákně. Nedělejte to ale takto:
var thread = new Thread(DoTask);
thread.Start();
Tento způsob je totiž tak trochu bezpečnostní díra – představte si, co se stane, když vám někdo zvenku těchto požadavků pošle tisíc najednou. Vytvoření vlákna má totiž nějakou režii a když najednou necháte server vytvořit stovky vláken, tak ho to na chvíli zaměstná, vzroste také spotřeba paměti atd. Tomu se dá sice zabránit nějakým modulem, který hlídá DOS útoky, jenže tady těch požadavků není potřeba zas tolik, aby to DOS filtr zachytil a mohl spolehlivě označit jako DOS útok, a útočník se může vejít do limitu. Navíc v případě distribuovaného útoku z více počítačů stačí 100 počítačů (což není v dnešní době cloudů žádný problém) a každý může poslat jen jednotky či desítky požadavků.
Pro tyto účely existuje ThreadPool. Jak již název napovídá, je to takový bazének na vlákna, kde se čvachtá několik předpřipravených vláken, cákají po sobě, házejí si s míčem, no však to znáte. Jakmile někdo zavolá funkci QueueUserWorkItem, nějaké vlákno z poolu se tohoto úkolu ujme, spustí předanou funkci a jakmile je hotovo, zase se vrátí do bazénku ráchat se s ostatními kamarády. Úkoly se řadí do fronty, takže i když zrovna žádné vlákno není k dispozici, úloha počká, než bude nějaké vlákno se svou prací hotovo. Server tedy nestráví 90% času vytvářením nových vláken.
Možná vás napadne, že IISka má také threadpool a jestli to náhodou není ten samý jako .NETový ThreadPool. Pokud by to tak bylo, mohli bychom opět vyčerpat vlákna, která obsluhují HTTP požadavky. Naštěstí to tak není a IIS má vlastní thread pool. Klidně tedy používejte .NETí ThreadPool, není na tom nic špatného.
Takže v obsluze jednoho HTTP požadavku spustíme úkol v jiném vlákně, uživatel dostane odpověď na požadavek hned (např. stránku s progressbarem) a úkol mezitím běží na serveru. Na stránce, která je vrácena uživateli, může být progressbar, nebo taková ta oblíbená animace “světluška lítá jak potrhlá pořád dokola”. Tato stránka se musí serveru pravidelně tázat, jestli byl úkol již dokončen, a pokud ano, zobrazit uživateli výsledek této operace, pokud nějaký je.
Je potřeba upozornit na dvě věci:
1. Metody spouštěné v jiném vlákně nesmí vyhodit výjimku, jinak dojde ke shození celé aplikace. IIS ji samozřejmě hned restartuje, ale to pár sekund trvá, než se vše zinicializuje a než se třeba spustí kód v Application_Start v Global.asax. Pokud by se to mělo stávat často, tak se bude aplikace neustále restartovat a každý požadavek uživatelů bude trvat několik sekund, jak bude aplikace neustále startovat.
2. IIS občas recykluje aplikační pool, což znamená, že aplikace se shodí a spustí znovu. K recyklaci se přistupuje, když aplikace začne požírat velké množství paměti, anebo může být nastavena například automatická recyklace jednou za den. Při recyklaci by mělo být zaručeno, že se provedou všechny HTTP požadavky a žádný se neztratí, maximálně si uživatel pár sekund počká. Ale vlákna, která si vytvoříte sami, anebo vlákna z ThreadPoolu mohou být kdykoliv ukončena. Musíte s tím tedy u déletrvajících úloh počítat - je to omezení, kterému se dá vyhnout jedině tak, že napíšete klasickou Windows službu oddělenou od IIS. Pokud byla úloha přerušena, tak ji například zkusit spustit znovu, nebo alespoň uživateli oznámit chybu.
Recyklace poolu se při dobře napsané aplikaci a adekvátně nastavené IISce prakticky nevyskytuje, anebo jen výjimečně – není to tedy takový problém. Ale je lepší s ním počítat, minimálně uživateli dát nějak najevo, že to nedoběhlo a že to má zkusit znovu.
Jak na to?
Celý proces má tedy 3 části:
- Spustí se dlouhotrvající úkol a uživateli se vrátí stránka s progressbarem.
- Stránka se jednou za čas zeptá serveru, jestli už úkol byl dokončen.
- Pokud se tak stalo, přejdeme na stránku s dokončením.
Pokud by se náhodou stalo, že se úkol neukončil celý, ale ani dle záznamů neskončil nějakou výjimkou, pravděpodobně byl application pool recyklován a je třeba to dát navenek vědět, aby na to aplikace mohla nějak reagovat, například tak, že spustí úkol znovu.
Arzenál
Stavy aktuálně prováděných úkolů je potřeba někam ukládat. Na to potřebujeme úložiště, které přežije recyklaci worker procesu.
Důrazné upozornění: Speciálně letenkové weby jsou experti na to, nacpat všechny informace do session. Pak si člověk otevře tu samou stránku ve dvou záložkách, aby mohl porovnávat letenky. Ale protože se používá session, která je sdílená, nefunguje to správně. Session nepoužívejte, ještě jsem nenarazil na situaci, kdy by to bez ní nešlo a vždy jsem sám sobě i uživatelům aplikace ušetřil spoustu problémů.
V tomto příkladu použiju SQL Server Compact edition a Entity Framework Code First – ukážeme si jen základní použití, takže se neděste, že tyto technologie neznáte.
Jako jiné úložiště, které přežije recyklaci poolu, je třeba soubor ve filesystému, velký SQL Server, anebo session, pokud máte zapnutý režim State Server nebo SQL. Session ale používat vřele nedoporučuji.
V databázi budeme mít tabulku Tasks, kde budou následující sloupce:
- TaskId – jednoznačný identifikátor úkolu, použijeme Guid
- CreatedDate – datum spuštění úkolu
- IsCompleted – příznak True/False, který určuje, jestli byl úkol ukončen
- ErrorMessage – text výjimky, která nastala při provádění (nebo null, pokud žádná nebyla)
- TaskResult – výsledná hodnota serializovná do XAML
Nalezené výsledky bychom mohli ukládat do druhé tabulky v databázi s vazbou pomocí sloupce TaskId, ale protože na úrovni databáze v hodnotách nepotřebujeme hledat, stačí nám je mít serializované (.NET objekty převedeme na textovou nebo binární reprezentaci – do jednoho dlouhého textu nebo pole varbinary). Já jsem pro to zvolil formát XAML, protože si dobře poradí např. s generickými kolekcemi a obecně mi přijde chytřejší než klasická XML serializace.
Možná si říkáte, že XAML je věc používaná ve Windows Presentation Foundation a v Silverlightu, že s ASP.NET nemá nic společného. XAML je ovšem něco víc – je to obecný formát pro serializaci stromu objektů. Dá se do něj uložit jakákoliv hierarchie vlastních objektů (s pár omezeními) a má to mnoho výhod, uvidíme je za chvíli.
Protože dlouhotrvajících úkolů můžeme mít v aplikaci více, bylo by vhodné je schovat za nějaké rozhraní, abychom k nim mohli přistupovat jednotně. Do aplikace tedy přidáme následující rozhraní:
/// <summary>
/// Reprezentuje rozhraní pro dlouhotrvající úkol.
/// </summary>
public interface ILongRunningTask<TResult>
{
/// <summary>
/// Provede činnost na pozadí a vrátí výslednou hodnotu.
/// </summary>
TResult Execute();
}
Rozhraní je generické, abychom mohli mít návratovou hodnotu silně typovou. ILongRunnintTask<int> tedy bude mít metodu Execute, která vrací hodnotu typu int.
Místo ILongRunningTask by se klidně dala použít i třída Task z Task Parallel Library, která je součástí .NETu.
Práce s databází
Stavy úkolů budeme muset ukládat do databáze, aby tyto informace přežily recyklaci aplikačního poolu. Protože článků o práci s velkým SQL Serverem v ASP.NET zde máme dost, můžeme pro ukázku použít malý SQL Server Compact Edition. Jedná se o malou embedded verzi SQL Serveru, kvůli které není potřeba instalovat žádný databázový server, protože databáze je hostována přímo v procesu, který ji používá. Má to samozřejmě svá omezení – runtime je velmi malý, protože tato je určena mimo jiné pro mobilní zařízení, nemáte tedy k dispozici pokročilé věci jako uložené procedury, triggery atd., a tuto databázi lze používat najednou jen z jednoho procesu – nedá se nijak rozumně sdílet. Od své čtvrté verze je ale SQL Server Compact Edition možné používat u webových aplikací a na malé aplikace to je použitelné.
Abychom se navíc zabývali něčím trochu novým, ukážeme si i použití Entity Frameworku, což je objektově relační mapper, tedy nástroj pro práci s databází, který nám databázi zpřístupní pomocí C# objektů. Protože se nechceme patlat s vytvářením databáze, použijeme přístup Code First, který si na základě deklarací C# tříd vygeneruje databázi sám.
Použití Entity Frameworku nám navíc zaručí, že když se časem rozhodneme přemigrovat aplikaci na velký SQL Server nebo na jinou databázi, nebudeme s tím mít žádný problém – jen změníme connection string a Entity Framework se už postará o to, aby generoval správné SQL dotazy, které se napříč různými servery mohou nepatrně lišit.
Abyste Code First mohli používat, je třeba si do Visual Studia doinstalovat Entity Framework ve verzi 4.1. Asi nejjednodušší je stáhnout si plugin NuGet a přidat do projektu balíček EntityFramework:
NuGet nám do projektu přidá reference na potřebné knihovny a případně doplní konfiguraci tak, abychom mohli novější verzi Entity Frameworku používat.
Nyní budeme chtít popsat strukturu databáze. Máme jen jednu tabulku a protože používáme Code First, stačí napsat třídu, která reprezentuje řádek takové tabulky. Code First ji za nás vygeneruje.
Přidáme tedy třídu LongRunningTaskEntity.
/// <summary>
/// Reprezentace datové entity z tabulky Task
/// </summary>
public class LongRunningTaskEntity
{
[Key]
public Guid TaskId { get; set; }
public DateTime CreatedDate { get; set; }
public bool IsCompleted { get; set; }
public string ErrorMessage { get; set; }
public string Result { get; set; }
}
Sloupce, které v tabulce chceme mít, definujeme pomocí vlastností (s výchozím getterem a setterem). Primární klíč označíme atributem Key.
Dále musíme nadeklarovat tzv. kontext - to je objekt, který spravuje připojení k databázi, obsahuje odkazy na všechny tabulky a stará se o veškeré instance entit z databáze (např. sleduje jejich změny). Pomocí tohoto objektu též načítáme data z databáze a ukládáme provedené změny.
/// <summary>
/// Reprezentace datového modelu
/// </summary>
public class LongRunningTaskContext : DbContext
{
public DbSet<LongRunningTaskEntity> Tasks { get; set; }
}
Kontext musí dědit ze třídy DbContext a pro každou tabulku bude obsahovat vlastnost typu DbSet<typ_entity>. Budeme tedy mít jednu tabulku Tasks plnou entit LongRunningTaskEntity.
Poslední věc, kterou musíme říct, je connection string. U Code First nemusíme databázi vytvářet, runtime si ji vytvoří sám, stačí jen říci, kde bude. Do web.config souboru přidejte connection string, musí mít stejný název jako naše třída, která dědí z DbContext.
<connectionStrings>
<add name="LongRunningTaskContext" providerName="System.Data.SqlServerCe.4.0" connectionString="Data Source=|DataDirectory|LongRunningTasks.sdf"/>
</connectionStrings>
A to je celé. Entity Framework je připraven k použití, při prvním přístupu si vytvoří databázi a v ní potřebnou tabulku, a pak již můžeme vesele číst a zapisovat data.
Načítání a ukládání úkolů
Nyní si napíšeme třídu, která se stará o načítání a ukládání úkolů do databáze, neboli řeší perzistenci. Pojmenujeme ji LongRunningTaskStorage.
/// <summary>
/// Třída, která se stará o načítání a ukládání stavu úloh.
/// </summary>
public class LongRunningTaskStorage
{
/// <summary>
/// Vytvoří instanci databázového kontextu.
/// </summary>
private LongRunningTaskContext CreateContext()
{
return new LongRunningTaskContext();
}
/// <summary>
/// Vrátí úkol z databáze.
/// </summary>
private LongRunningTaskEntity GetTask(LongRunningTaskContext context, Guid taskId)
{
var task = context.Tasks.SingleOrDefault(t => t.TaskId == taskId);
if (task == null)
{
throw new InvalidOperationException(string.Format("The task {0} was not found in database. It may have expired!", taskId));
}
return task;
}
}
Určitě se nám bude hodit metoda CreateContext, která vytvoří novou instanci datového kontextu. Je dobré zapouzdřit new LongRunningTaskContext do separátní funkce, nikdy nevíme, jestli nebudeme později potřebovat na nové instanci nastavit. V ideálním případě by tohle řešil IoC/DI kontejner, ale nebudeme to komplikovat.
Další metoda GetTask má za úkol vrátit objekt reprezentující úkol v databázi s daným ID. context.Tasks reprezentuje tabulku s úkoly a volání SingleOrDefault(t => t.TaskId == taskId) vrátí jeden konkrétní úkol, jehož TaskId je rovno hodnotě v proměnné taskId.
t => t.TaskId == taskId je lambda funkce, jedná se o zkrácený zápis funkce:
bool Funkce(LongRunningTaskEntity t)
{
return t.TaskId == taskId;
}
Žlutě vyznačené je to, co zůstalo v té lambda funkci. Že t je typu LongRunningTaskEntity funkce SingleOrDefault ví, voláme ji na kolekci objektů LongRunningTaskEntity. Název t jsme si pochopitelně zvolili, u lambda funkce se nedeklaruje.
Pokud by context.Tasks byla kolekce v paměti, pak by se pro každý prvek zavolala tato lambda funkce a pokud by pro některý prvek vrátila true, metoda SingleOrDefault vrátí ten prvek. Pokud žádný true vracet nebude, vrátila by null. Pokud by true vrátilo více prvků, SingleOrDefault vyhodí výjimku, vyhovovat podmínce může maximálně jeden.
Protože context.Tasks ale není jen tak obyčejná kolekce, vše, co na ní zavoláme, se převede na SQL dotaz. Tedy správně řešeno vše, co Entity Framework podporuje, a jen do chvíle, než na tom zavoláme foreach, se převede na SQL dotaz. Výše uvedená konstrukce se převede na SELECT * FROM Tasks WHERE TaskId = @taskId a spustí proti databázi. SingleOrDefault pak na výsledku spustí foreach a vrátí jeden prvek, null, pokud žádný nebyl, nebo výjimku, pokud jich bylo více.
Metoda GetTask nám tedy vrátí příslušný úkol z databáze, nebo vyhodí InvalidOperationException, pokud úkol nebyl nalezen.
Dále bude naše třída muset podporovat uložení výsledku prováděného úkolu. Napíšeme tedy následující metody:
/// <summary>
/// Nastaví úkol jako úspěšně dokončený.
/// </summary>
public void SetTaskCompleted<TResult>(Guid taskId, TResult result)
{
using (var context = CreateContext())
{
// získat úkol
var task = GetTask(context, taskId);
// nastavit hodnoty sloupců
task.IsCompleted = true;
task.Result = XamlServices.Save(result);
// uložit změny
context.SaveChanges();
}
}
/// <summary>
/// Nastaví úkol jako neúspěšný.
/// </summary>
public void SetTaskFailed(Guid taskId, string errorMessage)
{
using (var context = CreateContext())
{
// získat úkol
var task = GetTask(context, taskId);
// nastavit hodnoty sloupců
task.IsCompleted = true;
task.ErrorMessage = errorMessage;
// uložit změny
context.SaveChanges();
}
}
Obě metody jsou skoro stejné, vytvoří si kontext, najdou úkol podle daného ID a nastaví mu IsCompleted na true. První metoda pak nastaví do vlastnosti Result předanou výslednou hodnotu, kterou přes XamlServices.Save převedla do XAML reprezentace. Druhá metoda místo toho nastavila předanou chybovou hlášku do vlastnosti ErrorMessage.
Nakonec se na kontextu zavolá SaveChanges, které uloží námi provedené změny. Ve skutečnosti spustí něco jako UPDATE Tasks SET …=… WHERE TaskId = @taskId.
Aby XAML serializace fungovala, je třeba do projektu přidat reference na knihovny System.Xaml, WindowsBase a PresentationCore. Všechny jsou součástí .NET Frameworku. Celá tahle XAML maškaráda je potřeba, protože do databáze samozřejmě nemůžeme uložit libovolný objekt jen tak. Serializací .NET objekt převedeme na textovou (nebo jinou) reprezentaci, abychom pak mohli provést opačný proces – deserializaci. Serializovat pochopitelně nejde všechny objekty, ale výsledek dlouhotrvajícího úkolu bude typicky jen objekt, který drží nějaká data a nemá žádné externí závislosti – např. nějaká kolekce objektů, které obsahují jen primitivní datové typy apod.
Ještě jsme nenapsali metodu, která bude umět do databáze přidat nový úkol. Nyní je ta pravá chvíle.
/// <summary>
/// Uloží do databáze nový úkol.
/// </summary>
public void AddNewTask(Guid taskId)
{
using (var context = CreateContext())
{
// vytvořit úkol
var task = new LongRunningTaskEntity() { TaskId = taskId, CreatedDate = DateTime.UtcNow };
// přidat jej do tabulky
context.Tasks.Add(task);
// uložit
context.SaveChanges();
}
// promazat staré úlohy
DeleteOldTasks();
}
Přidání řádku do tabulky je též velmi jednoduché – vytvoříme objekt LongRunningTaskEntity, nastavíme mu TaskId a datum vytvoření. Přidáme jej do kolekce context.Tasks a uložíme změny.
Po přidání úkolu by bylo ještě radno vymazat staré a již nepotřebné úlohy. To dělá metoda DeleteOldTasks. Dala by se spouštět nějakým plánovačem, ale bohatě stačí mazat úlohy po přidání nové. Metoda obsahuje ještě mechanismus pro úsporu výkonu, bude mazat nejvýše jednou za 2 hodiny, a to úkoly, které jsou starší než 2 hodiny.
private static object deleteOldTasksLocker = new object();
private static DateTime lastDeleteDate = DateTime.MinValue;
/// <summary>
/// Smaže úlohy starší než 2 hodiny.
/// </summary>
private void DeleteOldTasks()
{
// pokud jsme mazali před méně než 2 hodinami, nic nedělat
var minTime = DateTime.UtcNow.AddHours(-2);
if (lastDeleteDate >= minTime) return;
lock (deleteOldTasksLocker)
{
if (lastDeleteDate >= minTime) return;
using (var context = CreateContext())
{
// najít úlohy starší než minimální čas
var tasks = context.Tasks.Where(t => t.CreatedDate < minTime);
foreach (var t in tasks)
{
// smazat je
context.Tasks.Remove(t);
}
// uložit
context.SaveChanges();
}
}
}
Na začátku se spočítá čas od 2 hodiny zpátky, starší úkoly budou smazány. V proměnné lastDeleteDate si pamatujeme čas posledního mazání. Pokud jsme mazali před více než dvěma hodinami, vstoupíme do kritické sekce přes konstrukci lock - to je kvůli tomu, aby nemohlo mazání provádět více vláken najednou. Aby to bylo opravdu thread-safe, je třeba kontrolu na datum provést ještě jednou – je to běžný postup, říká se mu také double-check.
Pomocí dotazu context.Tasks.Where(t => t.CreatedDate < minTime) vytáhneme úkoly, které jsou starší než 2 hodiny. Projdeme je a pro každý zavoláme context.Tasks.Remove, abychom je smazali. Po uložení pomocí SaveChanges se změny provedou.
Zde je krásně vidět jedna konkrétní nevýhoda OR-Mapperů – hromadné UPDATE nebo DELETE příkazy jsou dosti neefektivní, protože nejdříve musíte všechny mazané nebo upravované záznamy načíst, a pak teprve je smazat. ORM pro každý vygeneruje separátní DELETE dotaz, naštěstí se alespoň spouští v jedné transakci, takže se buď smaže vše, nebo nic.
Není to důvod, proč na ORM zanevřít, ale je potřeba s tím počítat a pokud to jde, hromadná mazání a úpravy dělat např. pomocí uložené procedury. SQL Server Compact bohužel storky neumí, takže si musíme vystačit s tímto a počítat s tím, že tam nebudou tisíce úkolů během dvou hodin. Pokud ano, pak je třeba samozřejmě použít velký SQL Server – toto demo je použitelné na malé aplikace.
Dále budeme potřebovat metodu, která vrátí aktuální stav úkolu. Nebylo by ale dobré z ní vracet přímo entitu LongRunningTaskEntity, kdokoliv zvenčí by ji totiž mohl upravovat, navíc po zrušení datového kontextu by entita při přístupu k některým vlastnostem vyhazovala výjimky atd. Napíšeme si tedy třídu, která ponese aktuální stav úkolu.
/// <summary>
/// Reprezentuje výsledek provádění úkolu.
/// </summary>
public class LongRunningTaskState<T>
{
private string errorMessage;
/// <summary>
/// Vrací chybovou zprávu.
/// </summary>
public string ErrorMessage
{
get
{
if (!IsFinished)
{
throw new InvalidOperationException("The task has not been finished yet! Check IsFinished property before accessing ErrorMessage property!");
}
return errorMessage;
}
}
private T result;
/// <summary>
/// Vrací výslednou hodnotu.
/// </summary>
public T Result
{
get
{
if (!IsFinished || HasException)
{
throw new InvalidOperationException("The task has failed or has not been finished yet! Check IsFinished and HasException before accessing the Result property!");
}
return result;
}
}
/// <summary>
/// Vrací, zda-li úkol skončil s chybou.
/// </summary>
public bool HasException
{
get { return !string.IsNullOrEmpty(errorMessage); }
}
/// <summary>
/// Vrací, zda-li je úkol dokončen.
/// </summary>
public bool IsFinished { get; private set; }
/// <summary>
/// Vrací, zda-li byl úkol přerušen.
/// </summary>
public bool WasInterrupted { get; internal set; }
/// <summary>
/// Inicializuje instanci třídy pro dokončený úkol.
/// </summary>
public LongRunningTaskState(T result, string errorMessage)
{
this.result = result;
this.errorMessage = errorMessage;
IsFinished = true;
}
/// <summary>
/// Inicializuje instanci třídy pro nedokončený úkol.
/// </summary>
public LongRunningTaskState(bool isStillRunning)
{
// pokud úkol ještě běží, je to v pořádku - pokud ne, tak musel být přerušen
WasInterrupted = !isStillRunning;
}
}
Máme tam vlastnost ErrorMessage a Result (silně typový, třída je generická). Tyto vlastnosti kontrolují, zda-li je již úkol dokončen, a pokud ne, vyhodí výjimku – je dobré vždy “uživatele” našeho API navést na správné používání – tedy aby nejdříve zkontroloval IsFinished a pak teprve četl výsledek. Máme ještě vlastnost HasException, která vrací, jestli úkol proběhl v pořádku, nebo jestli nastala výjimka. Podmínky if (task.HasException) se mi líbí daleko více než if (task.Exception == null), je to výstižnější.
Poslední vlastností je WasInterrupted, kterou v databázi nemáme – jestli úkol ještě běží bude muset zjistit metoda, která vrací tento stav.
Do třídy LongRunningTaskStorage přidáme poslední metodu, která bude vracet tento aktuální stav úkolu:
/// <summary>
/// Gets the task results.
/// </summary>
public LongRunningTaskState<TResult> GetTaskResult<TResult>(Guid taskId, bool isAlreadyRunning)
{
using (var context = CreateContext())
{
// získat úkol
var task = GetTask(context, taskId);
if (task.IsCompleted)
{
// deserializovat výsledek, pokud nějaký je
TResult value = default(TResult);
if (string.IsNullOrEmpty(task.ErrorMessage))
{
value = (TResult)XamlServices.Parse(task.Result);
}
// vrátit výslednou entitu
return new LongRunningTaskState<TResult>(value, task.ErrorMessage);
}
else
{
// zatím nebyl dokončen
return new LongRunningTaskState<TResult>(isStillRunning);
}
}
}
Opět vytvoříme kontext, vytáhneme úkol s daným ID z databáze. Pokud je úkol již hotov a neskončil chybou, deserializujeme výslednou hodnotu pomocí XamlServices.Parse a vrátíme výsledek. Pokud úkol ještě není hotov, použijeme druhý konstruktor třídy LongRunningTaskState a předáme hodnotu isStillRunning. Pokud úkol již neběží, ale v databázi není informace o tom, že skončil, pak musel být přerušen, a konstruktor tedy nastaví WasInterrupted na true. V opačném případě bude WasInterrupted rovno false a úkol tedy není hotov, ale ještě stále běží.
Tím bychom měli databázovou část hotovou – máme metody, které pracují s úložištěm pro data, a entitu reprezentující stav. Umíme přidat úkol, nastavit jej jako ukončený (úspěšně či neúspěšně), řešíme i promazávání starých úkolů. Umíme zjistit aktuální stav úkolu a vrátit uživateli objekt, z nějž se dozví:
- jestli byl úkol dokončen
- jestli se úkol povedl či ne, lze přečíst i výslednou hodnotu
- jestli byl úkol přerušen a je případně třeba spustit znovu
To je pro tento díl vše. Příště si ukážeme, jak úkoly spouštět, a dokončíme aplikaci, která naši sadu tříd bude používat. Pevně věřím, že se vám tento díl líbil, jakékoliv náměty, postřehy atd. uvítám v komentářích.