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

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
David Grudl
Nette Core | 8227
+
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.

pekelnik
Člen | 462
+
0
-

nice :)

Patrik Votoček
Člen | 2221
+
0
-

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

Honza Kuchař
Člen | 1662
+
0
-

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

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

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ší.
David Grudl
Nette Core | 8227
+
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.

v6ak
Člen | 206
+
0
-

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

Honza Kuchař
Člen | 1662
+
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. :-)

norbe
Backer | 405
+
0
-

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

Majkl578
Moderator | 1364
+
0
-

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

Honza Kuchař
Člen | 1662
+
0
-

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

paranoiq
Člen | 392
+
0
-

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

Honza Kuchař
Člen | 1662
+
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é.

paranoiq
Člen | 392
+
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

paranoiq
Člen | 392
+
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í

David Grudl
Nette Core | 8227
+
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.

David Grudl
Nette Core | 8227
+
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.

Honza Kuchař
Člen | 1662
+
0
-

Tak takto je to dokonalé! :-)

jasir
Člen | 746
+
0
-

Je to pecka. Díky.

paranoiq
Člen | 392
+
0
-

jo taky děkuju :]

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

uestla
Backer | 799
+
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)?

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.

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)

David Grudl
Nette Core | 8227
+
0
-

Tobě to s DummyStorage nefunguje?

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