Bug v Nette cache – FileStorage.php

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Honza Kuchař
Člen | 1662
+
0
-

Ahoj, dneska jsem si hrál s keší a narazil jsem na takový problémek. Následující k´d mi nechal v tempech prázdný soubor (0bytů):

$cache = Environment::getCache("Test");
$cache["test"] ="test";
unset($cache["test"]);

Po odstranění zavináče v FileStorage.php:255 jsem se dozvěděl „Permision deined“. Ale já jsem pod Win a Apache běží pod uživatelem system. Takže může všude. Hmmm. Ve funkci readMeta() se soubor uzamkne přečte vrátí. Jenomže není uzavřený a uzamčení pořád trvá. Odemčít se měl ten soubor až v remove() na řádku 256. Jenomže o řádek před tím se zamčený soubor pokouším smazat.

Tzn. upravená a funkční verze remove() je pouze s přehozenými řádky:

	/**
	 * Removes item from the cache.
	 * @param  string key
	 * @return bool  TRUE if no problem
	 */
	public function remove($key)
	{
		$cacheFile = $this->getCacheFile($key);
		$meta = $this->readMeta($cacheFile, LOCK_EX);
		if (!$meta) return TRUE;

		ftruncate($meta[self::HANDLE], 0);
		fclose($meta[self::HANDLE]);
		@unlink($cacheFile); // intentionally @
		return TRUE;
	}
David Grudl
Nette Core | 8228
+
0
-

Ano, FileStorage pod Windows má problém kvůli nemožnosti mazat uzamčený soubor. Pokud jej odemkneš a pak teprve smažeš, není to atomické, tj. je tu šance, že během té doby došlo ke změně obsahu souboru a maže se něco novějšího. Z toho důvodu je tam to truncate(), aby bylo možné soubor se zámkem pod Windows vůbec zlikvidovat.

Samozřejmě je otázka, jestli zmíněná neatomičnost může mít nějaký negativní vedlejší efekt. Kromě toho, že se smaže něco jiného.

Honza Kuchař
Člen | 1662
+
0
-

A co tam dát podmínku že pokud velikost souboru == 0?

No vlastně stále je to neatomické. :/ Tak co udělat kompromis: tuto malinkatou neatomičnost vyvolat pouze pokud je skript spuštěn pod Windows. Já mám server na Windows a dohromady s tím MiltipleFileUploaderem by to vytvářelo opravdu hodně souborů.

Nebo by šly soubory uzamykat pomocí druhého souboru .lock. Ale to už je dost práce na víc.


Ještě dotaz, když mám atomickou cache: 2 procesy současně začnou zapisovat, tak jeden musí počkat, nebo se jeden prostě nezapíše? Nebo se vyhodí chyba?

Honza Kuchař
Člen | 1662
+
0
-

Asi to udělám jednoduše: v použití MultipleFileUploaderu je tak asi 1/10000000000, že vůbec trefí 2× po sobě stejný název položky v cache. Takže tam napíšu upravený FileStorage, na který se cache přepne, pokud jede pod Windows.

Honza Kuchař
Člen | 1662
+
0
-

Jinak zdá se, že už tento nápad někdo měl: http://cz2.php.net/…on.flock.php#…

Zatím budu používat klasické filestorage. Až bude troška času, zkusím to přepsat tímto stylem. CO vy na to?

A v tomto stylu zapisování a čtení cache ( http://cz2.php.net/…on.flock.php#… ):

<?php
        $fp = fopen($logFileName, 'a');
        $canWrite = false;
        //Waiting until file will be locked for writing
        while (!$canWrite) {
          $canWrite = flock($fp, LOCK_EX);
          //Sleep for 0 - 2000 miliseconds, to avoid colision
          $miliSeconds = rand(0, 20); //1 u = 100 miliseconds
          usleep(round($miliSeconds*100000));
        }
        //file was locked so now we can store information
        fwrite($fp, $toSave);
        fclose($fp);
?>

Jednoduché a účinné.

Editoval honzakuchar (20. 8. 2009 8:34)

David Grudl
Nette Core | 8228
+
0
-

Celý ten cyklus se dá napsat jednodušeji: flock($fp, LOCK_EX); :-) (ano, flock čeká automaticky). Ale hlavně to nesouvisí s titulním problémem.

Tak co udělat kompromis: tuto malinkatou neatomičnost vyvolat pouze pokud je skript spuštěn pod Windows.

No, tohle není úplně 100%, protože problém se týká NTFS (a asi i FAT), nikoliv přímo Windows. Sice v drtivé většině případů platí Windows = NTFS, Linux = ext3, ale je to hra s pravděpodobností.

David Grudl
Nette Core | 8228
+
0
-

Zkusil jsem updatnout FileStorage. Můžeš to prosím důkladně otestovat? Případně i na Linuxu, jestli máš možnost.

Honza Kuchař
Člen | 1662
+
0
-

Na Linuxu možnost nemám. Na php.net jsem se dočetl, že FAT vůbec nepodporuje uzamykání souborů. (vždy vrátí false)

Testovat jdu hnedka ;)

Honza Kuchař
Člen | 1662
+
0
-

Cache se zdá být v pořádku, ale já mám v MultipleFileUploaderu totální neatomičnost. :( Ale nevím jak to vyřešit.

Soubory jako takové se na server (zdá se dostanou v pořádku). Ale musím od nich ještě chvíli skladovat meta informace – serializovaný HttpUploadedFile.

Dělám to následujícím způsobem (upozorňuji, že následující řešení je úplně špatně a proto ho prosím nikdo nepoužívejte!)

$store = self::getData($token); // vrátí pole vytažené z cache tzn. volá něco jako return $cache[$token]
$store[] = new HttpUploadedFile(xxx); // Přidám tam informace
self::saveData($token); // zase zapíše do cache.

Jenomže pokud je mi známo, tak se soubor v cache odemkne hned po tom co si ty data vytáhnu. Ale já je pořebuji mít uzamčené po celou dobu až dokud neskoční to načtení – přidání – uložení. Tzn. jak v cache atomicky upravovat data? Jak se tak dívám ono by klidně stačilo kdybych byl schopný získat včechny cachované položky z jednoho namespace.

h4kuna
Backer | 740
+
0
-
<?php
$cache = Environment::getCache("Test");
$cache["test"] ="test";
unset($cache["test"]);
?>

David Grudl napsal(a):
No, tohle není úplně 100%, protože problém se týká NTFS (a asi i FAT), nikoliv přímo Windows. Sice v drtivé většině případů platí Windows = NTFS, Linux = ext3, ale je to hra s pravděpodobností.

Ja osobne jedu na ReiserFS a vsak to se chova nejspise stejne jako ext3.

PHP 5.2.10–2

Nette 5ecb8df

Otestoval jsem to v temp/c-Test mi nic nezustalo pro ReiserFS TRUE

honzakuchar napsal(a):
Na Linuxu možnost nemám. Na php.net jsem se dočetl, že FAT vůbec nepodporuje uzamykání souborů. (vždy vrátí false)

ani FAT pod linuxem nefunguje v tempu zustal soubor „_test“ (flashdisk) FALSE

NTFS pod linuxem v pohode, asi to ale nebude smerodatny pac mi to fachalo i u predchozi revize 8f16ab2 TRUE

Doufam ze to pomohlo

Editoval matata (20. 8. 2009 14:37)

David Grudl
Nette Core | 8228
+
0
-

Podle mých stress testů to na Win/NTFS a Linux/ext3 taky valí dobře.

honzakuchar napsal(a):

Cache se zdá být v pořádku, ale já mám v MultipleFileUploaderu totální neatomičnost. :( Ale nevím jak to vyřešit.

Používáš cache na něco, na co se nehodí. U cache totiž platí jedno pravidlo – všechno, co tam uložíš, může být v následující milisekundě klidně smazáno. Proto existuje i úložiště DummyStorage – správně napsaná aplikace s ním musí fungovat.

To, co hledáš, je session.

Honza Kuchař
Člen | 1662
+
0
-

Jenomže session s Flashem použít nemůžu. Kdyby to šlo, tak bych to už dávno udělal. Viz http://bugs.adobe.com/…browse/FP-78 . Ale tohle není až zase takový problém, to bych obešel, že bych si ten identifikátor předal někde v datech, ale potom je jako výchozí v Nette Session zapnuto, že se ověřuje prohlížeč jazyk a ještě cosi. Tzn. odhlásí mě to.


Existuje v Nette něco jako Storage dat, které je přístupné od všech klientů naráz? (tedy jakoby globální – to co teď nahrazuji cachí?) Nebo by bylo něco takového potřeba napsat?

Editoval honzakuchar (20. 8. 2009 18:38)

David Grudl
Nette Core | 8228
+
0
-

To ověřování v session lze vypnout, pokud je problém jen v tomto.

Mechanismus pro ukládání cache by v podstatě šlo použít pro ukládání trvalejších dat. Ale určitě bych to šoupl do extra adresáře, řekněme třeba uploadData. Aby zámek byl trvalý, šlo by použít TemplateCacheStorage:

$cache = new Cache(new TemplateCacheStorage(__DIR__ . '/uploadData/'));
$cache[$key] = $data;

...

$dataAccess = $cache[$key]; // POZOR! Zde je trik - $dataAccess je pole s prvkem `handle`, tedy handle souboru se zámkem
$cache[$key] = $newData; // zapíšu nová data
fclose($dataAccess['handle']);

píšu to z hlavy, ale mohlo by to tak nějak fungovat.

Honza Kuchař
Člen | 1662
+
0
-

To ověřování v session lze vypnout, pokud je problém jen v tomto.

Vím, ale připadí mi nevhodné kvůli odesílacímu políčku snižovat bezpečnost celé aplikace.

Mechanismus pro ukládání cache by v podstatě šlo použít pro ukládání trvalejších dat.

Přesně to jsem myslel. Nepsat to znova jen upravit.

šlo by použít TemplateCacheStorage

Děkuji za tip. Zítra vyzkouším. Popřípadě se zkusím tuto funkcionalitu nějak hezky zaobalit. Protože jak tak vidím, tak cache používám úplně špatně. Zatím jsem vždy cache používal jako session společnou pro všechny uživatele. Tzn. pokud bych použil DummyStorage rozhodně by to nefungovalo. Davide moc děkuji za ten příklad s tím DummyStorage už je mi přesně jasné, kdy a jak cache využít. ;) Možná tu větu přidat do dokumentace? :)

David Grudl
Nette Core | 8228
+
0
-

honzakuchar napsal(a):

Vím, ale připadí mi nevhodné kvůli odesílacímu políčku snižovat bezpečnost celé aplikace.

Přemýšlím, jestli by to nešlo vypnout jen pro ty Flash requesty…

Honza Kuchař
Člen | 1662
+
0
-

Nevím, nezkoušel jsem. Někde tady na fóru jsem se dočetl, že se to musí vypnout pro celou app. (ale nevím jestli je to ještě pořád pravda…) Ale i tak, je session thread-safe? Myslím, že ne. Tzn. když budu mí od jednoho uživatele 3 fronty, každou na 2 vláknech, tak to dopadne špatně. Protože teď má každá fronta alespoň vlastní soubor, takže pokud jede na jednom vlákně, tak to jede perfektně…

David Grudl
Nette Core | 8228
+
0
-

Session se na začátku požadavku zamkne a je dostupná jen jednomu vláknu, takže funguje přesně jak potřebuješ.

Honza Kuchař
Člen | 1662
+
0
-

Mno tak to by stálo za zvážení… Ale chtělo by to nějak vyřešit to vypínání toho ověřování prohlížeče a spol. jenom na tom jednom requestu. (nezkoušel jsem to) Nebo už to tak funguje?