Mnozí z vás se jistě již setkali s problémem, jak spustit nějaký skript, vykonat nějakou práci – úlohu – v určitý čas. V tomto článku si shrneme některé možnosti řešení.
Potřeba vykonat nějakou činnost v určený čas je u dnešních webových dynamických aplikací velice častá. Jako příklad můžeme uvést vyčištění cache, pročištění session přihlášení uživatelů, provedení zálohy, v dnešní době, kdy jsou v módě „webové hry“ (hromadné většinou tahové většinou strategické hry, k jejichž hraní stačí pouze internetový prohlížeč), taky tzv. „přepočty“. Ve všech prozatím jmenovaných příkladech se jedná o opakující se činnosti, ale taky se zde samozřejmě mohou vyskytnou i „jednorázovky“– kupř. naplánované vydání článku v určitou hodinu. Nazývejme takové činnosti „úlohami“. (Pozn.: Nejpřesněji by tyto činnosti asi vystihoval termín „naplánované úlohy“, ale ten si bohužel už zabraly Windows, proto pouze „úlohy“.)
Prvně tedy můžeme tyto úlohy rozdělit na opakující se a jednorázové. Opakující se, jak plyne z názvu, v pravidelných intervalech či časech vyvolávají znova a znova. Jejich využití je tedy hlavně na úlohy, jejichž časy spouštění na začátku naplánujeme, necháme je, aby dělaly svou práci, a staráme se o ně většinou jen tehdy, nedělají-li to, co se od nich očekává. Na druhou stranu, u jednorázových se většinou jedná o to, že v daný čas bychom nemohli danou činnost vykonat. Nebo prostě slouží jako ulehčení, abychom se nemuseli činností již dále zabývat – určíme její čas provedení a ona v ten čas bude provedena.
Ovšem pokud si uvědomíme, že webové aplikace jsou postaveny na architektuře klient-server, zjistíme, že úloha vlastně nemusí být provedena přesně v daný čas. Naše aplikace běží na vzdáleném serveru a do té doby, než přijde nějaký požadavek, je nečinná – což znamená, že její stav se nemůže nijak změnit. Změny se dočkáme teprve tehdy, když nějaký ten požadavek přijde.
Zde se možnosti vykonávaní úloh rozdělují na „bezprostředně vykonávané úlohy“ a „odložené úlohy“. První typ potřebuje, aby ke klientu a serveru přibyla ještě další entita, která si bude hlídat čas a spustí úlohu přesně v čas, kdy je potřeba. K tomuto způsobu vykonávání úloh se řadí cron a webcron, kdy každý je založen na trochu jiném principu, ale v základě jsou stejné.
Odložené úlohy naopak využívají faktu, že stav není potřeba měnit, dokud na aplikace nepřijde nějaký požadavek. Po příchodu požadavku aplikace nejdříve zjistí, jestli v čase mezi předchozím požadavkem a tím nynějším měly být vykonány nějaké úlohy a jestli ano, vykoná je. Teprve potom se zabývá zpracováním samotného požadavku. V obou případech přístupu k úlohám je zajištěno, že požadavek bude zpracován teprve po tom, co byly vykonány všechny úlohy, které předcházely požadavku. Jediné, co se bude lišit, je čas samotného vykonání úloh.
Cron
Cron je démon používaný v systémech unixového typu ke spouštění shellových příkazů v určitý čas. Ve Windows se nepoužívá cron, ale tzv. „Naplánované úlohy“. První cron byl velice jednoduchý program. Byl spouštěn při vstupu systému do víceuživatelského režimu. Celý program sestával z nekonečné smyčky:
- přečti konfigurační soubor (tzv. „crontab“)
- zjisti, jestli je v tento čas nějaký skript ke spuštění, případně takový spusť
- uspi se na jednu minutu
- a všechno zase od začátku
Jelikož většina serverů je založena právě na nějakém unixovém systému, je cron, pokud ho hosting podporuje, dobrou volbou pro spouštění úloh. Ovšem je zde problém, že asi jen na málokterém (nejspíš na žádném) hostingu vám nedovolí přímý zápis a možnost editace crontabu, proto jen těžko jde spravit vlastní zadávání úloh. Záleží, jaké možnosti pro editaci úloh v crontabu hoster dovolí. Řešení vlastního hostingu také nemusí být ideální. Ale výhodou je, že skripty, které serverový cron spouští, vůbec nemusí být přístupné pro vnější svět.
Webcron
Webcronem je míněna služba, která poskytuje možnost, jak z jiného umístění nechat vykonat nějaký skript. Prakticky je to většinou realizované tou nejjednodušší cestou – na serveru webcronu je cron, který v daný čas vyšle HTTP požadavek na zadanou adresu úlohy. Jak je vidět, opět se jedná o možnost, jak vykonávat úlohy, jak na ně má přijít čas.
Problémem webcronu ale je, že úlohy musí být přístupné přes HTTP, což nemusí být zrovna bezpečné – každý si může vyslat požadavek serveru, čímž aktivuje úlohu. Dá se tomu předcházet např. kontrolou dat předaných v požadavku, ale ani to není 100% – dají se napodobit, podstrčit. U některých úloh třebas nemusí vadit, že se vykonají předčasně či vícekrát (např. u toho vyčištění cache by to nemuselo být zas tak hrozné), u jiných to už může být problém (např. ty „přepočty“ u webových her). Také je zde problém s tím, že pokud server poskytovatele webcronu spadne, úlohy nebudou vykonány. A pokud webcron neposkytuje nějaké API, které by přidávání úloh zjednodušovalo, zůstává to prakticky stejné jako u normálního cronu.
Odložené úlohy
Jak již bylo popsáno, odložené úlohy se vykonávají teprve až přijde na aplikaci nějaký požadavek. Mezitím mohou někde ležet. Většinou jako takovéto úložiště slouží databáze, ale mohou to být i soubory, může to být třeba i vlastní crontab.
Odložené úlohy mají oproti cronu i webcronu výhodu, že nepotřebují, aby je někdo spouštěl. Spustí se samy, pokud přijde nějaký požadavek (lépe řečeno při každém požadavku musíme kontrolovat, jestli nejsou nějaké úlohy k vykonání a případně je spustit; ale tato činnost se dá dosti zautomatizovat). Mají tedy výhodu v tom, že se aplikace nemusí spoléhat na nic dalšího. Háček tkví v tom, že server dnes snad vždy postaven konkurenčně – může obsluhovat více požadavků naráz. Pokud na aplikaci přijde více požadavků najednou, může dojít k tomu, že by se některá úloha mohla provést vícekrát (což hrozí hlavně u „dlouhotrvajících úloh“ a/nebo velkém náporu návštěvníků).
Ukázka implementace odložených úloh
Abychom nezůstali jen u suché teorie, ukážeme si jednu možnost implementace odložených úloh. Bude se jednat o jednoduchou třídu zastřešující práci s úlohami a jejich inicializaci, k vytváření vlastních úloh, které budou dělat něco prospěšného, budeme naši třídu specializovat (neboli z ní budou konkrétní úlohy dědit). K ukládání bude sloužit databáze, ke které se budeme připojovat pomocí PDO. Stáhněte si zdrojové kódy s příklady.
Pro začátek si napíšeme nějakou jednoduchou úlohu, např. na promazávání cache:
<?php
final class ClearCache extends Task
{
protected function execute()
{
list($cache_dir) = $this->getParameters();
$mode = fileperms($cache_dir);
$this->deleteDirectory($cache_dir);
mkdir($cache_dir, $mode);
}
private function deleteDirectory($dir)
{
foreach (glob($dir . '/*') as $item) {
if (is_dir($item)) {
$this->deleteDirectory($item);
} else {
unlink($item);
}
}
rmdir($dir);
}
protected function areParametersValid()
{
$params = $this->getParameters();
if (count($params) == 1 && is_dir($params[0])) {
return true;
}
return false;
}
}
Řekl bych, že kód je samovysvětlujicí. Metoda execute()
je v rodičovské třídě deklarovaná jako abstraktní – dědící úlohy pak tuto metodu implementují svým funkčním kódem. Zde je to jasné – smaže se celá složka s cacheí a poté se vytvoří nově.
Další metodou, kterou zde můžeme vidět, je areParametersValid()
, ve které, jak název napovídá, je umístěn kód pro zkontrolování platnosti předaných parametrů. My chceme pouze jeden a chceme, aby to byla složka.
Úlohu pak můžeme vytvořit jednoduše pomocí:
<?php
$task = new ClearCache(strtotime('+1 minute'), 'cache');
Ovšem problém je, že takto vytvořená úloha prostě vznikne a zanikne, nikam se neuloží, nikdy se neprovede. Proto musíme vytvořit pro úlohy nějaký úložný prostor (je potřeba vytvořit nejdříve tabulku v databázi) a nově vytvořenou úlohu tam uložit. Uděláme to jednoduše pomocí:
<?php
Task::setStorageWrapper(new PDOStorageWrapper(DB_DSN, DB_USER, DB_PASSWD));
$task->store();
Úloha je vytvořena i uložena. V aplikaci pak můžeme říct, aby se úlohy vykonaly:
<?php
Task::processUndone();
Tak a to je všechno, nic víc potřeba dělat není. Pro více příkladů se podívejte do archivu se zdrojovými kódy. Vylepšení by se avšak dalo udělat mnoho. Největší problémy určitě dělá ta synchronizace při více přístupech naráz. Také by určitě stálo za zvážení použít nějaký lepší způsob sdílení init() funkcí (viz zdrojové kódy a příklady). Hodila by se jistě podpora opakovaných úloh (ano, ta chybí). Teď se to dá řešit tak, že úloha ve svém těle (v metodě execute()
) vždycky vytvoří znovu sama sebe (samozřejmě s jiným časem), ale určitě by to šlo vyřešit lépe.
Shrnutí
Způsobů, jak v PHP spouštět úlohy, je více a výběr toho správného záleží na dané situaci. Možná je také kombinace všech způsobů dohromady. Na opakující se úlohy se nejvíce hodí cron – je na to prostě stavěný, navíc nabízí velice široké možnosti nastavení opakování (např. se může určit, že úloha se spustí vždycky první pátek v měsíci, či každý pátek třináctého atp.). Hlavní využití odložených úloh bych viděl v těch „jednorázovkách“. Ale problém je, pokud se takové úlohy nahromadí. Když se nastaví čas provedení více úloh na nějakou noční hodinu, tak to odnese první návštěvník, který si bude muset ráno počkat trochu déle, než se všechny provedou. Tady může pomoci webcron, který v pravidelných intervalech (např. přes noc každou jednu hodinu) provede na aplikaci HTTP požadavek, čímž se úlohy vykonají.