WP_Cron

Důležité pro pochopení akcí naplánovaných pomocí WP_Cron je to, že WP_Cron není cron takový, jaký známe ze světa serverů – spolehlivý pracovník, který v daný čas udělá to, co mu řeknete. V případě WP_Cron se nelze spoléhat na to, že se naplánovaná akce spustí v čas. Nicméně se můžete spolehnout na to, že se nespustí dříve.

Je to dáno tím, že WordPressu se snaží veškerou svou funkcionalitu poskytnout i na tom nejlevnějším sdíleném hostingu ihned po světoznámé 5 minutové instalaci.

WP_Cron je systém postaven čistě na záznamu v MySQL databázi, systému zámků využívajícího Transients API (tedy buď databázi či objektovou cache), HTTP požadavcích a PHP. V takovém setupu jsou proto stále přítomny timeouty, PHP memory limit či max-execution time.

Základ pro funkcionalitu, kterou WP_Cron nabízí tedy není zcela ideální, ale i tak je WP_Cron užitečným pomocníkem – pokud tedy víme jak se k němu chovat a co od něj očekávat. To vše se pokusím aspoň nastínit v tomto článku.

Jak naplánovat akci ze svého kódu

Na webu prakticky okamžitě naleznete obstojný tutorial o tom, jak nějakou událost naplánovat. Já budu v dalším výkladu spoléhat na to, že jste si tu práci již dali a níže uvádím jen kód, který by měl být výsledkem takových tutoriálů. Poté přejdu rovnou k popisu mechanismu, který tento kód spouští. Začínáme tedy tam, kde tutoriály končí.

//následující funkce je spouštěna nějakou uživatelskou akcí, typicky odesláním formuláře
function my_example_function_called_as_a_result_of_user_action( ... ) {
    $post_id = 2; //typicky dynamicky získaná proměnná.
    $variable = 'Hello WP_Cron World!';
    wp_schedule_single_event( time(), 'trigger_my_cron_function', array( $post_id, $variable ) );
}

add_action( 'trigger_my_cron_function', 'run_my_cron_function', 20, 2 );

function run_my_cron_function( $post_id, $variable ) {
    update_post_meta( intval( $post_id ), 'cron_set_variable', sanitize_text_field( $variable ) );
}

Pokud vám ukázka kódu nic neříká, je nejvyšší čas si přečíst některý z tutoriálů, či se podívat do kodexu

Jak WordPress plánuje události

Pokaždé, když je z kódu zavolána funkce wp_schedule_single_event, dojde k aktualizaci záznamu v tabulce wp_options. Jedním ze záznamů v této tabulce jsou naplánované události – uložené jsou všechny pěkně pohromadě v option s názvem cron podobně serializovaného pole s údajem o čase na místě klíče a naplánovaných volání funkce v hodnotě záznamu pole. Zde je podoba cron option na mé lokální instalaci založené na VVV získaná pomocí WP_CLI a příkazu shell

$ wp shell
wp> get_option( 'cron' );
array(4) {
  [1451331840]=>
  array(1) {
    ["wp_maybe_auto_update"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
  }
  [1451332812]=>
  array(1) {
    ["wp_scheduled_delete"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(5) "daily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(86400)
      }
    }
  }
  [1451333427]=>
  array(3) {
    ["wp_version_check"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
    ["wp_update_plugins"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
    ["wp_update_themes"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
  }
  ["version"]=>
  int(2)
}

Ukládání do wp_options, namísto použití transients API či jiného dočasného mechanismu je zvoleno proto, že tyto záznamy musí zůstat zachovány až do té doby, dokud nedojde k jejich zpracování. V případě, že by na webu byla použita externí cache využívaly by zmíněné transients tuto cache a ta nám nikdy negarantuje minimální dobu, po kterou je záznam dostupný, pouze maximální – tedy tu, od kdy už záznam dostupný nebude.

autoload options, cron a object cache

Skutečnost, že WP_Cron využívá wp_options má ještě minimálně jeden háček, který je nutné brát v potaz. WordPress využívá options pro skladování informací o svém nastavení, uživatelských rolí, nastavení šablony, pro transients a stejná tabulka také slouží často jako úložiště dat pro pluginy. Protože některá z těchto nastavení jsou důležitá při každém načtění WordPressu a některá nikoli, WordPress využívá sloupeček autoload pro uchovávání informace o tom, které options mají být automaticky z databáze načteny v samotném začátku načítání aplikace. WP_Cron je jednou z takových autoloaded options:

$ wp shell
wp> global $wpdb
wp> $wpdb->get_results( "SELECT * FROM {$wpdb->options} WHERE option_name = 'cron'" );
array(1) {
  [0]=>
  object(stdClass)#82 (4) {
    ["option_id"]=>
    string(3) "103"
    ["option_name"]=>
    string(4) "cron"
    ["option_value"]=>
    string(828) "a:4:{i:1451331840;a:1:{s:20:"wp_maybe_auto_update";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}}i:1451332812;a:1:{s:19:"wp_scheduled_delete";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:5:"daily";s:4:"args";a:0:{}s:8:"interval";i:86400;}}}i:1451333427;a:3:{s:16:"wp_version_check";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}s:17:"wp_update_plugins";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}s:16:"wp_update_themes";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}}s:7:"version";i:2;}"
    ["autoload"]=>
    string(3) "yes"
  }
}

Proč to říkám. Říkám to proto, že funkce wp_load_alloptions využívá WP_Cache a ta může být poháněná pomocí externí objektové cache a to může mít svá úskalí. Například defaultní maximální velikost objektu skladovaného v Memcache je 1M. Může se tak velice snadno stát, že tenhle 1M záznamy WP_Cron zaplníte a externí objektová cache v případě alloptions přestane fungovat. A přitom je alloptions často jedním z prvních důvodů, proč se člověk k Memcache či jiné objektové cache dostane.

Kdy WordPress vykoná naplánovanou událost

Jak jsem již zmínil, WP_Cron je jakýsi kvazi cron, nikoli cron v pravém slova smyslu. To, jakým způsobem je realizován v rámci WordPressu není nic neobvyklého a vlastně dost lidí překvapí, že využívá stejný mechanismu, jako vše ostatní – filtry a akce.

Vezměme si modelový problém. Není to problém čistě akademický, ale něco, co skutečně vývojáře trápí.

Naplánujeme publikaci článku na půlnoc. Klikneme tedy na tlačítko “Schedule”. Záznam o této budoucí akci je zapsán do tabulky wp_options a WordPress vás přesměruje zpět na výpis článků. To je vše, nic dalšího se nekoná a WordPress i server spí.

A spí tak dlouho, dokud jej nikdo nenavštíví. A dokud WordPress spí, nemůže vykonávat žádné akce. WordPress vzbudíte tím, že navštívíte nějakou stránku – ať již na frontendu či v administraci. Ale pozor, v případě frontendu se musí jednat o necachovanou stránku. Pokud tedy web pro nepřihlášené uživatele cachujete pomocí nějaké full page cache, je nutné se přihlásit.

No a pokud vy, ani nikdo jiný web kolem půlnoci nenavštíví (a nebo navštíví, ale je mu předložena cachovaná verze), může se dost dobře stát, že druhý den ráno si vzpomenete, že jste na půlnoc měli naplánovanou publikaci článku a rádi byste se podívali na komentáře.

Ale jelikož jste prvním návštěvníkem webu od doby, kdy jste dokončili článek a naplánovali jej, žádné komentáře nečekejte. WordPress se právě probudil a hodlá dohnat vše, co zameškal. Včetně publikace vašeho článku, která nastane až krátce poté, co jste web navštívli. Již bychom měli mít představu o tom, proč nedošlo k publikaci článku o půlnoci – protože se na web nikdo nepodíval nebo se podíval na cachovaný frontend a WordPress samotný tedy spal. Proč ale nedojde k publikaci článku ihned ve chvíli, kdy se přihlásím do administrace?

WP_Cron a výkon

Jelikož WP_Cron není odbavován přesně v čas, kdy to bylo naplánováno, může se stát, že na pořadu je v danou dobu více akcí, které již měly proběhnout, nicméně kvůli tomu, že web nikdo nenavštívil, neproběhly. Kdyby se WordPress snažil okamžitě vše zpracovat, došlo by k tomu, že by takové načtení stránky trvalo nesnesitelně dlouho a dost pravdědopodobně by došlo k timeoutu požadavku. Tomu ale WordPress předchází.

Asynchronní PHP

Asynchronní PHP je něco, co by mnoho vývojářů velmi ocenilo. Ovšem ještě stále nejsme tak daleko, abychom jej mohli plně využívat na libovolném hostingu. A tak WP_Cron využívá techniky, která spoléhá na HTTP requesty.

Nejspíš jste si všimli, že v rootu vaší WordPress instalace naleznete soubor wp-cron.php. Ten hraje klíčovou roli v tom, jak WordPress spouští cron tak, aby neblokoval načítání stránky pro daného návštěvníka.

Vždy když navštívíte necachovanou verzi stránky,

// WP Cron
if ( !defined( 'DOING_CRON' ) )
	add_action( 'init', 'wp_cron' );

WordPress vyšle HTTP požadavek na http://example.com/wp-cron.php?doing_wp_cron a přidá k tomu nějaké parametry.

Relevantní kód: https://github.com/WordPress/WordPress/blob/77e365efbf2e499e2ed11d29c101ea466cf1ceed/wp-includes/cron.php#L352

Důležité je, že se jedná o tzv. non-blocking HTTP request. Tedy WordPress odešle požadavek, ale již nečeká na odpověď. Ta jej vůbec nezajímá a načítání vaší stránky může nerušeně pokračovat.

Jak WordPress vykonává naplánované akce

WordPress vykonává naplánované akce tak, že vybere již zmíněný záznam z tabulky wp_options (cron) obsahující serializované pole z databáze a prochází celé pole, položku po položce a vždy porovnává časový záznam s aktuálním časem. Pokud je naplánovaný čas v minulosti je tato položka z pole odebrána a zavolá se funkce, která je vedle času v dané položce uložena a předají se ji uložené argumenty.

Teoreticky se tedy může stát, že mezi odstraněním položky z cronu a vykonáním akce dojde k timeoutu či vyčerpání PHP memory limitu. V takovém případě je akce navždy ztracena a již se nikdy nevykoná. Je to ovšem lepší varianta, než aby se naplánovaná akce spustila opakovaně.

Vždy běží jen jeden proces

Jak jsem zmínil, vždy, když někdo navštíví necachovanou stránku, je odeslán dotaz na wp-cron.php. Nebýt systému zámků, docházelo by k tomu, že vedle sebe poběží dva a více konkurenčních procesů zpracovávajících záznam o naplánovaných akcích. To by opět vedlo v problémům – ať již výkonnostním, tak i funkčním, jelikož by se některé akce vykonávali vícekrát, než bylo původně myšleno.

Systém zámků jsem již nastínil v úvodu. WP_Cron využívá transients API (ať jsou již ukládané do databáze, či do object-cache). Pokud existuje specifický transients záznam, wp-cron.php svou činnost ukončí a nepustí se do zpracování naplánovaných akcí. Čili velké množství requestů na http://example.com/wp-cron.php?doing_wp_cron končí velmi záhy.

Relevantní kód:

Nasazení podpory opravdového cronu

Nicméně pokud je váš web hojně navštěvován, může i velké množství HTTP requestů spouštějícíh wp-cron.php způsobit problémy na straně serveru. A zde se již dostáváme k technice, která umožňuje zajistit, že WP_Cron se bude vykonávat periodicky i tehdy, pokud web nikdo nenavštíví (nebo je vhodně a silně cachován) a navíc zajistí, že návštěva necachované stránky nevytvoří nový HTTP požadavek na wp-cron.php, tedy na server.

WordPress umožňuje mechanismus HTTP requestů vypnout pomocí konstanty DISABLE_WP_CRON.

//file: wp-config.php
define(‘DISABLE_WP_CRON’, true);

Pokud je definována s hodnotou true ve vašem wp-config.php, žádné požadavky na wp-crong.php se neprovádí. To ovšem znamená, že se žádná z naplánovaných akcí nikdy neprovede.

Je totiž třeba dalšího kroku. A sice nastavení opravdového cronu tak, aby odesílal HTTP požadavky na wp-cron.php:

wget -q -O - http://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Podrobnější návod jak na to určitě naleznete na googlu.

Závěrem

Tento článek by měl pomoci vysvětlit mechanismus stojícím za WP_Cron a to, co tato konkrétní implementace způsobuje. Určitě jste se všichni někdy setkali s “Missed Schedule” hláškou v administraci označující příspěvek, který nebyl doposud publikován, ačkoli měl být. Důvody pro existenci tohoto fenoménu by vám nyní již měly být jasné.

Určitě doporučuji podívat se nyní do souboru wp-cron.php. To se v něm odehrává by vám nyní mělo být jasnější.

Pokud je pro vás pravidelnost publikování příspěvků či provádění některých akcí, zkuste popřemýšlet o podpoře WP_Cronu skrze reálný cron. U více navštěvovaných webů tak navíc můžete ulehčit vašim serverům.

3 thoughts on “WP_Cron”

  1. Ahoj Davide, moc pekne sepsano. Pokud clovek pochopi, jak WP Cron funguje, stava se z nej velice uzitecny nastroj. Spravuji pomerne velky cluster 1x load balancer, 8x aplikacni server (Varnish, Apache, PHP-FPM, synchronizace souboru pomoci lsyncd) + 2x DB server (master – slave, pouzivam hyperDB). Vse monitoruji mimo jine pomoci new relic a mohu potvrdit, ze nahrazeni WP Cronu server side cronem znamena celkem vyznamnou usporu.

    Externi objektova cache je tez pomerne fajn vec. Nedavno jsem zacal experimentovat s memcached jako back-endem pro cache a zda se, ze tento krok docela pekne ulevil databazovym serverum. Nicmene prave diky faktu, ze WP Cron vyuziva object cache jsem narazil na problem, kdy dochazelo k tomu, ze nektere cron tasky se nespustily a proste zmizely. Toto bylo podle me zpusobeno nekonzistentnim stavem cache. Vcera jsem zkusil implementovat reseni nastinene v tomto vlakne https://core.trac.wordpress.org/ticket/31245 a po ranni kontrole logu se zda byt vse funkcni.

    Liked by 1 person

    1. Ahoj Petře, předně děkuji za komentář – schválil bych jej hned, nebýt toho, že jsem byl mimo civilizaci. Velice mě zaujal popisovaný cluster. Dovol několik otázek 🙂

      8x aplikacni server (Varnish, Apache, PHP-FPM …

      8 aplikačních serverů je, dle mých zkušeností, víc, než je běžně třeba. Zvláště potom, když tam běží Varnish, PHP-FPM a stačí jedna dvojice master-slave databázových serverů. Je to tak, že web (weby?) navštěvuje hodně přihlášených uživatelů? V takovém případě by nejspíš jedna dvojice master-slave databázových serverů byla málo. Je tedy “problém” někde jinde?

      Proč Apache a ne Nginx? Neříkám, že Nginx je tou správnou odpovědí na všechno, nicméně poslední dobou bývá právě Nginx jasnou volbou, pokud jde o produkční servery, co mají něco vydržet.

      Jakým způsobem provádíš deploy na těch 8 aplikačních serverů. Pomocí zmíněného lsyncd, nebo to je jen pro obsah wp-content/uploads? Pokud takto synchronizuješ uploads, není to zbytečné plýtvání místem – mít všechny uploads na všech serverech?

      Nicmene prave diky faktu, ze WP Cron vyuziva object cache jsem narazil na problem, kdy dochazelo k tomu, ze nektere cron tasky se nespustily a proste zmizely.

      Pouštíš WP Cron na všech 8 aplikačních serverech nebo jen na jednom vybraném? Spuštět cron na všech je, imho, zbytečné. Já bych si vybral jeden a remote request pro spouštění cronu nehnal přes load balancer, ale přímo na IP vybraného serveru – mělo by být poté jednodušší daný server ohlídat.

      Like

      1. Ahoj Davide, predpokladam, ze jsi mimo civilizaci slavil prichod noveho roku 🙂 Tak je to spravny. Obcas je rozhodne fajn byt delsi dobu off-line…:-) Jinak k tvym dotazum:

        1. 8x aplikacni server

        Kouknul jsem ted na konfiguraci a v soucasne dobe mame na serveru vytvoreno cca 50 virtual hostu. Jedna se spise o weby zpravodajskeho typu, takze vetsina uzivatelu je spise neprihlasenych. Pro tvoji informaci, na nejvetsim webu mam rekord temer 300.000 uzivatelu za den (k dnesnimu dni 78.334 publikovanych clanku, 141.604 registrovanych uzivatelu). Pak je tam par webu nekde v rozmezi 50k – 100k navstev za den a zbytek uz je mensi. Nevim presne, musel bych trosku prolezt statistiky. Celkove je to cele spise takove narazove. Traffic je vetsinou typu – vyjde clanek, nahrnou se uzivatele. Tohle je takova varianta v pohode a i pri nekolika tisisich uzivatelich on-line se servery relativne flakaji. Nicmene pak je zde nekolik duvodu, proc je dobre to mit cele trosku predimenzovane. Prave na tom nejvetsim webu mame takovou featuru, ktera umoznuje sledovat fotbalove zapasy on-line. Nejjednodusii bude asi kouknout na link takove stranky zapasu (napr. http://www.footballfancast.com/match/803354), kde se krome deni na hristi zobrazuje i rozestaveni tymu na hristi, aktualizuji se statistiky. Zaroven se casto pripojuje i liveblog, takze oficialni komentare jsou na casove ose mixovany s komentari ze socialnich siti (pouzivame Scribblelive) Ted si vem, ze tech zapasu se pres den hraje nekolik, vetsinou ve stejnou dobu a obsah musi byt aktualizovan co nejvice realtime. Tohle uz byva trosku zahul :-). Pak jsou tady dalsi duvody, proc je dobre mit predimenzovano – napriklad ruzne typy utoku. Nechci zakriknout, ale vetsina uz se mi dari pomerne dobre odrazet. Pri mnozstvi obsahu, ktery mame v cele siti publikovan, je obcas docela i problem navsteva agresivnejsich robotu. To da serverum take docela dobre zakourit. Ale tady si myslim, ze celkem efektivni reseni tez mam. Soude na zaklade toho, ze po nasazeni jsem po delsi dobu podobny problem nezazil. No takze to asi tak ve strucnosti. Bylo by to urcite na delsi dobu :-).

        2. proc Apache ne Nginx

        Kdyz jsme tuto architekturu davali cca pred dvema lety dohromady, tez jsem puvodne pozadoval Nginx, ale hosi, co nam poskytuji serverove zazemi (https://www.wirehive.net/) mi to rozmluvili. Jestli si dobre vzpominam v pouziti Nginx nespatrovali zadnou vyraznou vyhodu. Taky si myslim, ze maji vetsi zkusenosti spise s konfiguraci Apache nez Nginx.

        3. Deployment etc.

        Deployment probiha pouze na jeden server (rikejme mu master) a zbytek se replikuje pomoci lsyncd. Plytvani mistem to trosku je, ale mame ho dost 🙂 a myslim si, ze kdyby bylo potreba pridat, moc to stat nebude. Tento master je zaroven dedikovan pro /wp-admin a cron tasky se samozrejme spousteji pouze na tomto jednom uzlu.

        Snad jsem nic nevynechal…:-) Uprimne, pokud by jsi mel chut, docela rad by jsem s tebou nekdy dal rec na tema webu s vysokou navstevnosti. V mnoha ohledech totiz plno veci vyzaduje dosti individualni pristup a plno standardnich postupu selhava – viz napriklad tvuj predchozi clanek…:-)

        Liked by 1 person

Leave a comment