[2010–02–11] Cache a ukládání „callbacků“ kvůli dramatickému zrychlení

před 9 lety

David Grudl
Nette Core | 6770
+
0
-

Smazání cache je běžná operace po nahrání nové verze aplikace na server. V tu chvíli ovšem dostane server docela zabrat, protože musí vybudovat cache novou. Získání některých dat může být dost náročné, příkladem je třeba cache RobotLoaderu. A pokud navíc v krátkém okamžiku přijde třeba 30 požadavků a všechny se snaží o totéž, náročnost ještě násobně roste.

Řešením by bylo upravit chování aplikace tak, aby data vytvářelo vždy jen jedno vlákno a ostatní čekala. Což je docela implementační oříšek. Mám dobrou zprávu, oříšek našel docela jednoduché řešení! V poslední revizi Nette stačí postup

$result = buildData(); // náročná operace
$cache->save($key, $result); // uložení do cache

napsat jako

$result = $cache->save($key, function() { // nebo callback(...)
    return buildData(); // náročná operace
});

a to je vše, buildData() bude volat jen jedno vlákno a ostatní budou čekat. Pokud vlákno z nějakého důvodu selže, dostane šanci jiné.

Toto chování jsem naučil RobotLoader. Výsledky jsou úžasné, při simulaci 30 vláken se ukázalo, že vytížení serveru klesá na pár procent vytížení původního.

před 9 lety

pekelnik
Člen | 468
+
0
-

nice :)

před 9 lety

Patrik Votoček
Člen | 2249
+
0
-

wow tak to je mazec! musím kouknout jak je to implementováno porotože to je hodně cool hračička…

před 9 lety

Honza Kuchař
Backer | 1648
+
0
-

super! Přesně takhle jsem si to představoval! :-)

Editoval honzakuchar (13. 2. 2010 16:23)

před 9 lety

v6ak
Člen | 206
+
0
-

Rozhodování, zda jde o data nebo callback, může být trochu zvláštní.

  • „Do keše lze ukládat jakékoliv struktury, nemusí to být jen řetězec.“ ( https://doc.nette.org/cs/caching )
  • Do callbacků může patřit i array(‚Foo‘, ‚bar‘) a další.

před 9 lety

David Grudl
Nette Core | 6770
+
0
-

Callback musí být buď objekt Closure (pro PHP 5.3, který ani nelze serializovat) nebo Nette\Callback (pro 5.2, 5.3), takže array se vždy uloží jako obyčejné pole.

před 9 lety

v6ak
Člen | 206
+
0
-

Aha, to je ale celkem důležité zmínit, protože jinak je to celkem WTF chování.

před 9 lety

Honza Kuchař
Backer | 1648
+
0
-

Mám trošku problém s tím, jak vlastně ty callbacky zapisovat.

callback(function(){
    ...
});

nebo jen

function(){...}

ale co potom napsat do PHPDoc? Píšu to pokaždé jinak a už mě to docela štve. Jak to zapsat správně? Resp. nejsprávněji. :-)

před 9 lety

norbe
Backer | 397
+
0
-

Obalovat closuru callbackem mi přijde poněkud zbytečný…

před 9 lety

Majkl578
Moderator | 1379
+
0
-

Closuru určitě nedávat do callback(), nemá to význam.

před 9 lety

Honza Kuchař
Backer | 1648
+
0
-

Teď mně tak napadá, toto asi nebude fungovat s DummyStorage? Nebo se mýlím?

před 9 lety

paranoiq
Člen | 388
+
0
-

nemýlíš. nefunguje. asi bude třeba místo ní udělat nějaké dočasné úložiště (prostě pole)

před 9 lety

Honza Kuchař
Backer | 1648
+
0
-

Hlásí se někdo dobrovolně o vytvoření ArrayDummyStorage? Na druhou stranu, přes takovýto storage už nezjistíš, co se stane, pokud aplikaci někdo pod rukama smaže tempy. Ale pravda, je to dost málo pravděpodobné.

před 9 lety

paranoiq
Člen | 388
+
0
-

vlastně bude úplně stačit, když se současné DummyStorage upraví, aby si pamatovalo callbacky (a nic jiného). večer pošlu patch

před 9 lety

paranoiq
Člen | 388
+
0
-

ke zprovoznění postačí i tohle (proti verzi 0.9.4 pro PHP 5.2):

diff --git a/Caching/DummyStorage.php b/Caching/DummyStorage.php
index 9f4d0b5..6cbb885 100644
--- a/Caching/DummyStorage.php
+++ b/Caching/DummyStorage.php
@@ -21,6 +21,11 @@
 class DummyStorage extends Object implements ICacheStorage
 {

+   /** @var array */
+   private $data = array();
+
+
+
    /**
     * Read from cache.
     * @param  string key
@@ -28,6 +33,8 @@ class DummyStorage extends Object implements ICacheStorage
     */
    public function read($key)
    {
+       if (isset($this->data[$key])) return $this->data[$key];
+
        return NULL;
    }

@@ -42,6 +49,16 @@ class DummyStorage extends Object implements ICacheStorage
     */
    public function write($key, $data, array $dp)
    {
+       if (!empty($dp[Cache::TAGS]) || isset($dp[Cache::PRIORITY]) || !empty($dp[Cache::ITEMS])
+           || !empty($dp[Cache::EXPIRE]) || !empty($dp[Cache::SLIDING])) {
+           throw new NotSupportedException('Tags, priority, dependent items, expiration and sliding expiration are not supported by DummyStorage.');
+       }
+
+       if ($data instanceof Callback || $data instanceof Closure) {
+           $data = $data->__invoke();
+       }
+
+       $this->data[$key] = $data;
    }


@@ -53,6 +70,7 @@ class DummyStorage extends Object implements ICacheStorage
     */
    public function remove($key)
    {
+       unset($this->data[$key]);
    }


@@ -64,6 +82,7 @@ class DummyStorage extends Object implements ICacheStorage
     */
    public function clean(array $conds)
    {
+       $this->data = array();
    }

 }

stejně se mi to ale celé nelíbí. je to hack a porušení SRP. úložiště se teď musí starat nejen o ukládání dat, ale i o jejich vytváření

před 9 lety

David Grudl
Nette Core | 6770
+
0
-

paranoiq napsal(a):
stejně se mi to ale celé nelíbí. je to hack a porušení SRP. úložiště se teď musí starat nejen o ukládání dat, ale i o jejich vytváření

Souhlas, přepíšu to.

před 9 lety

David Grudl
Nette Core | 6770
+
0
-

Aktualizoval jsem první příspěvek.

Zavolání callbacku se přesunulo mimo Storage, takže je v režii třídy Cache. Zároveň metoda save() vrací uložená data, tak se k nim dá dostat i bez nutnosti z cache číst.

před 9 lety

Honza Kuchař
Backer | 1648
+
0
-

Tak takto je to dokonalé! :-)

před 9 lety

jasir
Člen | 748
+
0
-

Je to pecka. Díky.

před 8 lety

paranoiq
Člen | 388
+
0
-

jo taky děkuju :]

mimochodem, http://cz.php.net/…-through.php

před 8 lety

uestla
Backer | 739
+
0
-

Dovolím si dotaz ohledně callbacku – většinou mám metody modelu v podobném stylu:

class ArticleModel
{
    /** @var Cache */
    protected $cache;

    private $table = 'clanky';



    // inicializace cache, apod.



    public function getArticle($id)
    {
        $key = 'article-' . $id;

        if (!isset($this->cache[$key])) {
            $article = $this->connection
                    ->select('*')
                    ->from($this->table)
                        ->where("[id] = %i", $id)
                    ->fetch();

            if ($article) {
                $this->cache->save($key, $article, array(
                    Cache::EXPIRE => time() + Tools::WEEK,
                    Cache::SLIDING => TRUE,
                ));
            }

        } else {
            $article = $this->cache[$key];
        }

        return $article;
    }

    // ...
}

Tzn., že kdybych chtěl tento způsob nahradit novým, výše doporučovaným, musel bych nějakým způsobem callbacku předat parametr $id, na kterém metoda lpí… Existuje nějaké řešení (nevím jak pro closures, ale osobně vyvíjím na PHP 5.2)?

před 8 lety

v6ak
Člen | 206
+
0
-

Existuje, ale je to složitější. Jako callback lze totiž použít array(instance, ‚názevNetody‘), jen ho musíš přeložit.

před 8 lety

wdolek
Člen | 331
+
0
-

ten patch se zatim nejak neobjevil nikde, ne? (stahl jsem si 0.9.5 i 1.0 pro PHP3 a kde nic tu nic… ani v api dokumentaci)

před 8 lety

David Grudl
Nette Core | 6770
+
0
-

Tobě to s DummyStorage nefunguje?

před 8 lety

wdolek
Člen | 331
+
0
-

David Grudl napsal(a):
Tobě to s DummyStorage nefunguje?

abych pravdu rekl, nezda se mi, ze to neco dela… pouzivam to s callbacky a mam takovy pocit, ze se callback ani nespusti, cimz nedojde ani k vytvoreni nejakych dat… takze:

  1. nic se mi necachuje – coz je u DummyStorage cilem
  2. bohuzel se mi data z callbacku neposlou do aplikace – protoze se asi ani zadne nevyrobi

… ale musim se priznat, ze jsem to do hloubky nestudoval, mel sem na spech, tak sem DummyStorage simuloval neustalym restartovanim memcached