Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

Atomické operace ještě jednou

Od té doby, co jsem tu před pár dny publikoval článek o atomických operacích se soubory, se hodně změnilo. Udělal jsem značný pokrok a představím vám nyní naprostou novinku.

Možná ještě krátce, proč se této problematice tolik věnuji. Obecně totiž platí, že pokud váš PHP skript vytváří nebo píše do souborů, je nutné toto řešit! Už pouhý jeden zápis je kritický! V opačném případě musíte počítat se ztrátou dat a vznikem těžko odhalitelných chyb.

Tedy věnujte tomu prosím pozornost.

Kde nestačí databáze ani NSafeFile

Určitě víte, že atomického čtení, zápisu a mazání lze dosáhnout použitím databází. Tedy místo ukládání dat ve formě souborů je uložíme jako záznamy do SQlite apod. Nepříjemné je, že třeba takový konfigurační INI soubor se jakožto záznam databáze hůř edituje.

Minule jsem tu dal k dispozici třídu, která uměla atomicky načíst, zapsat a smazat soubor (skript prošel určitým vývojem, o tom později). Pomocí ní můžeme onen INI soubor bezpečně zapsat a přečíst z disku – editace tedy bude pohodlnější.

Jenže ouha! Je tu problém. Soubor chci číst funkcí parse_ini_file(), která zámky nepoužívá. Čímž jde celá snaha o atomicitu do kytek. Databáze samozřejmě nepomůže už vůbec. Co teď?

Nette\IO\SafeStream

Trklo mě to včera večer a mám pocit, že to je fakt bomba :-) Ne, vážně, tohle řešení se mi prostě líbí. Nette\IO\SafeStream (dříve NSafeStream) registruje „bezpečný stream“, pomocí něhož můžeme atomicky manipulovat se soubory prostřednictvím standardních funkcí. Stačí jen uvést protokol „safe://“. Příklad:

// před jméno souboru přidáme safe://
$handle = fopen('safe://test.txt', 'x');
// ve skutečnosti se vytvořil dočasný soubor

fwrite($handle, 'La Trine');

fclose($handle);
// a teprve teď se přejmenoval na test.txt

// můžeme soubor smazat
unlink('safe://test.txt');

// a vůbec používat všechny známé funkce
file_put_contents('safe://test.txt', $content);

$ini = parse_ini_file('safe://autoload.ini');

Parádička, ne?

Jak to funguje?

V podstatě se jedná o přepis původního NSafeFile do podoby Stream Wrapper. Nette\IO\SafeFile je mimochodem několikrát překopaný skript, původní verze totiž fungovala na špatném předpokladu, na což mě v diskuzi upozornil mka. Jo, taková interakce se mi vážně líbí. Vlastně jsem rád, že tam ta chyba byla, protože se mi celou věc podařilo zvládnout bez lockfiles. Tedy je to velice výkonné.

Podívejme se na magii řešení:

Čtení

Otevřu soubor v módu r a pokusím se získat zámek pro čtení (neboli shared lock, LOCK_SH). Poté je možné soubor volně číst. Zámek se uvolní automaticky s uzavřením souboru – fclose() nebo při ukončení skriptu.

Zápis

Otevřu soubor v módu r+. Tím zjistím, zda existuje (budeme přepisovat) nebo je ho třeba vytvořit.

Zápis do existujícího souboru

Soubor je tedy otevřen v módu r+. Získáme zámek pro zápis (neboli exclusive lock, LOCK_EX). Obsah vymažeme funkcí ftruncate() a pak můžeme do souboru volně psát, až do uzavření a uvolnění zámku.

Zápis do neexistujícího souboru

Není možné vytvořit nový soubor, protože než bych získal zámek, mohl by s ním pracovat jiný thread. Proto vytvoříme dočasný (temporary) soubor v módu x a získáme exkluzivní zámek. Poté do něj volně zapisujeme. V okamžiku uzavření souboru jej zkusíme přejmenovat na požadovaný název. Pokud se přejmenování nezdaří (jiný thread mezitím tento soubor vytvořil), dočasný soubor smažeme.

Zápis v módu append

Protože není možné soubory otevírat v režimech a nebo w, neboť vytvoření nového souboru je nežádoucí akce, otevřeme jej v módu r+ a posuneme ukazatel na konec via fseek().

Mazání souboru

Soubor prostě smažeme funkcí unlink(). Že má soubor otevřený jiný thread, ať už pro čtení nebo zápis, nám nemusí vadit. Ve Windows totiž otevřený soubor vůbec smazat nelze a unlink selže. Naopak v Unixu se smaže, jakožto položka adresáře, ale s jeho obsahem je možné nadále bezpečně pracovat.

Tak a to je celé. Ono je to vlastně docela jednoduché, že? :-) Tak můžete testovat a připomínky i nápady jsou vítány!

Download

Nette\SafeStream

Komentáře

  1. Borek http://borber.com/blog/ #1

    Nápad to je dobrý, mám jen dvě pragmatické otázky:

    1. K čemu to je? Jak jsi správně poznamenal, k ukládání dat slouží databáze, a pokud je potřeba mít např. konfigurační soubor snadno editovatelný, většinou u něj není potřeba řešit problém současného čtení a zápisu, protože se úpravy dělají ručně a jen zřídka.
    2. U podobně sofistikovaných řešení si vždycky kladu otázku, jakto že se rozšířené a nesmírně zatěžované systémy typu Drupal nebo Wordpress bez podobného řešení v pohodě obejdou.

    Jinými slovy, řešení je to určitě účinné, je ale i účelné?

    před 11 lety | reagoval [2] David Grudl
  2. David Grudl http://davidgrudl.com #2

    avatar

    #1 Borku, Databáze je dobré úložiště dat, ale INI soubor z ní nenaparsuji, také ukládat do ní fotky serializované v SQL příkazu je cesta do pekel. Tedy jsou situace, kdy musíme operovat se soubory na disku. A potom je threadsafe přístup nezbytností.

    ad 2) A víš jistě, že tyto aplikace zapisují soubory na disk a přitom žádné bezpečnostní prvky nepoužívají?

    Mimochodem, ta úvaha je vůbec zcestná. Stavíš na tom, že WordPress vše řeší dokonale a je třeba si z něj brát příklad. Ale proč? Protože je tak populární mezi uživateli? Existuje snad souvislost mezi popularitou a kvalitou kódu? Osobně třeba WordPress považuju za zbastlený kus kódu…

    Jinými slovy, řešení je to určitě účinné, je ale i účelné?

    Vyzkoušej si ten příklad, co jsem uváděl minule, je i součástí NSafeStream. Při 1000 opakování mi udělá 5–10 chyb. Každá stá operace tedy končí nečekaným narušením či ztrátou dat. Je to málo?

    před 11 lety | reagoval [9] Borek
  3. KLoK http://www.kl0k.net #3

    Pokud se přejmenování nezdaří (jiný thread mezitím tento soubor vytvořil), dočasný soubor smažeme.

    Mozna by nebylo od veci dat uzivateli sanci obsah toho souboru zrestaurovat/zapsat pod jinym jmenem. resp. co kdyz je zadouci aby obsah souboru byl ten ktrery byl smazan

    před 11 lety
  4. pkm #4

    A spinlock je opravdu spinlock = aktivní čekání v cyklu? Něco takového používat pro čekání na diskové operace je v podstatě zhůvěřilost možná asi jen v PHP.

    Spinlock se používá jen v jádrech operačního systému, kdy je očekávané získání zámku kratší než přepnutí kontextu. Všude jinde by se mělo používat pasivní čekání (semafory, mutexy, monitory).

    před 11 lety | reagoval [5] johno [7] hvge
  5. johno http://johno.jsmf.net/ #5

    avatar

    #4 pkme, Ano, ale v PHP je to problém lebo štandardne semafóry nemáme.

    před 11 lety
  6. Keff http://www.tomaskafka.com #6

    avatar

    Nsafe je vážná věc, ne že budete někdo dělat fórky! :) I DGX to říká, tak tož to už musí být pravda, né?

    před 11 lety
  7. hvge http://hvge.sk #7

    #4 pkme, Spinlock sa pouziva aj v aplikaciach, nielen v OS. Zvlast s prichodom viac jadrovych procesorov to nabera na vyzname. Napriklad kvalitne navrhnute memory alokatory pouzivaju spinlock :)

    před 11 lety
  8. pkm #8

    Ok, u memory allocatoru to jistě má smysl. Tam právě platí to, že zámek je získan rychle (jedna alokace je krátká záležitost).

    Ale u čekání na práci s diskem, když to může trvat stovky a stovky milisekund?-)

    To, o co se dgx pokouší je „vážnější práce“ a PHP je IMHO pro vážnější práci nevhodné.

    před 11 lety
  9. Borek http://borber.com/blog/ #9

    #2 Davide Grudle,

    Databáze je dobré úložiště dat, ale INI soubor z ní nenaparsuji, také ukládat do ní fotky serializované v SQL příkazu je cesta do pekel.

    Souhlas, ale právě v těchto případech se jedná o soubory typu „jednou uložit a potom tisíckrát číst“. Tvoje řešení je užitečné pro často čtená a zapisovaná data, kde hrozí, že k oběma operacím dojde současně. A já říkám, že taková data patří do databáze.

    A víš jistě, že tyto aplikace zapisují soubory na disk a přitom žádné bezpečnostní prvky nepoužívají?

    Jistě to nevím, to je fakt. Vycházel jsem pouze z toho, že před vývojem NSafeStream jsi se určitě podíval, jestli vůbec a případně jak je to vyřešeno v současných systémech. (K nakouknutí by asi byla ideální filesystémově orientovaná DokuWiki, ale sám jsem ji nezkoumal.)

    Mimochodem, ta úvaha je vůbec zcestná. Stavíš na tom, že WordPress vše řeší dokonale a je třeba si z něj brát příklad.

    To jsem nikde neřek. Omlouvám se, jestli to tak vyznělo.

    Existuje snad souvislost mezi popularitou a kvalitou kódu?

    Podle mě ano. Popularita → široké nasazení → reportování chyb → oprava chyb.

    Osobně třeba WordPress považuju za zbastlený kus kódu…

    Já docela taky.

    Abych pravdu řekl, úplně jsi mé pochybnosti nerozptýlil. Uznávám, že pokud někdo chce dělat souborově orientovanou aplikaci (třeba ukládat data do XML), tvoje práce se bezpochyby hodí. Onen člověk si však asi špatně rozmyslel, k čemu se používají soubory a k čemu databáze (sám jsem se jednou takhle špatně rozhodl).

    Toť můj pocit.

    před 11 lety | reagoval [13] David Grudl
  10. Hever #10

    Dá se nějak konkrétně říct, k čemu by měl v praxi budovaný kód sloužit (reaguji v návaznosti na diskuzi, kde myslím většině není příliš jasné použití)?

    před 11 lety | reagoval [12] Borek
  11. llook http://llook.wz.cz/weblog/ #11

    avatar

    můžeme soubor smazat

    Jo, taky už jsem PHP4 zavrhnul.

    před 11 lety
  12. Borek http://borber.com/blog/ #12

    před 11 lety
  13. David Grudl http://davidgrudl.com #13

    avatar

    #9 Borku,

    Souhlas, ale právě v těchto případech se jedná o soubory typu „jednou uložit a potom tisíckrát číst“.

    Ano, a já právě dávám k dispozici funkci, která to umí jednou uložit a tisíckrát číst. Použít běžné fce je sebevražda. Samozřejmě pokud se bavíme na top-profi úrovni ;)

    Tvoje řešení je užitečné pro často čtená a zapisovaná data, kde hrozí, že k oběma operacím dojde současně.

    Nikoliv. Už jedno uložení je kritické. To je právě riziko, na které se snažím upozornit.

    Vycházel jsem pouze z toho, že před vývojem NSafeStream jsi se určitě podíval, jestli vůbec a případně jak je to vyřešeno v současných systémech.

    Nedíval. Znáš to, co si neuděláš sám… Mohl jsem buď den Googlit a hledat řešení, u něhož bych velmi těžko ověřoval, jestli je skutečně, ale skutečně ok. Nebo obětovat 2× tolik času, ponořit se do problému, sebevzdělat se v nové oblasti a pokusit se na něco přijít. A zároveň si být jist, že to je bezchybné. A nakonec i velmi univerzální a přínosné pro všechny.

    Každopádně děkuju všem co pomohli, zejména Johnovi!

    před 11 lety | reagoval [15] Borek
  14. David Grudl http://davidgrudl.com #14

    avatar

    Podíval jsem se, jak to dělá Dokuwiki, a dělá to naprosto špatně. Co z toho soudit?

    před 11 lety
  15. Borek http://borber.com/blog/ #15

    #13 Davide Grudle, Není mi jasné, proč je i jedno uložení kritické.

    Mimochodem, nevíš, proč nějaké thread-safe řešení není přímo součástí PHP? Tvoje práce je bezpochyby za hranicí schopností většiny PHP programátorů.

    před 11 lety | reagoval [18] David Grudl
  16. johno http://johno.jsmf.net/ #16

    avatar

    DGX: Páči sa mi to. Toto už hej.

    před 11 lety
  17. Jozef Izso http://www.izsak.net/weblog #17

    avatar

    V PHP sú veľmi obľúbené guestbooky realizované pomocou súboru a nie databázy. A tam dochádza k častému read/write.

    Cachovanie v Texty je tiež súborové a aj LucidCMS som si upravil na súború cache a tam by som uvítal tento safe spôsob zápisu.

    před 11 lety | reagoval [18] David Grudl
  18. David Grudl http://davidgrudl.com #18

    avatar

    #15 Borku, Zápis není atomická operace, takže může dojít k situaci, kdy ještě není soubor celý zapsán a už se jej snaží někdo číst. Pak přečte nesmysl. Pokud se takto načítá třeba tabulka uživatelských práv, může dojít k závažné bezpečností díře.

    NSafeStream to řeší tak, že obsah píše do dočasného souboru, a jakmile je úspěšně zapsán, přejmenuje ho.

    ad úspěšně zapsán: zmíněné Dokuwiki dokonce ani nekontroluje, zda se podařilo soubor zapsat celý! Už jsem na hostingu několikrát narazil na nedostatek místa na disku. Bojím se, že mít tam Dokuwiki, tak můžu nenávratně přijít o všechna data doslova na pár kliknutí na „edit“. Má skepse vůči Open Source projektům se tím jen potvrzuje…

    #17 Jozefe Izso, Do pluginů Texy teď samozřejmě NSafeStream budu implementovat – respektive budu ho používat všude, kde má své opodstatnění.

    před 11 lety
  19. hvge http://hvge.sk #19

    Dokuwiki to ma riesene uplne nahovno. Chvilu som sa v tom vrtal a okrem toho io_save.. tam ma este dalsie zvrhlosti. Napriklad sa spyta, ‚je subor niekym editovany?‘ a ak nie je, ‚tak ho edituj‘. Hazard ako z prirucky :)

    před 11 lety
  20. Vilém Málek http://interval.cz #20

    Rád bych se zeptal, jak je to se zotavováním z chyb skriptu v průběhu zapisování. Jak se systém zachová, pokud během generování nebo i zapisování dat dojde k nějakému problému? Vznikne mi soubor s polovinou dat nebo systém „pozná“, že je něco v nepořádku a vadný soubor zahodí?

    před 11 lety
  21. Ondrej Ivanic #21

    Preco? lebo napisa nieco aby bolo thread safe nie je trivialne. ZE2 je thread safe, kopa extenzii je thread safe, ide len o to vybrat spravny mix a potom aj to php bude thread safe :)

    Thread safe mozem okaslat, radsej by som uvital daky aplikacny server pre php… Celkom je to poznat ked pri kazdom requeste je potrebne nacitat stale rovnake 30kb XML a nieco snim porobit. Niektore veci sa daju poriesit napr. pomocou APC, ale vacsina bohuzial nie.

    před 11 lety
  22. hakim #22

    avatar

    chtěl jsem se optat, jak to řeší problém kdy edituji/natahuji soubor a někdo se ho mezitim snaží smazat.

    před 10 lety
  23. 2ge http://www.opensubtitles.org #23

    dik, urcite to vyskusam na cachovanie, dufam, ze to pojde pekne :)

    před 10 lety
  24. emme #24

    čau, NSafeStream nejde stáhnout, mohl bys ho dát zpět? díky

    před 9 lety | reagoval [25] David Grudl
  25. David Grudl http://davidgrudl.com #25

    avatar

    #24 emme, opraveno

    před 9 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.