Případ ucpané roury
„Tenhle job vás naučí, že každý operační systém má svoje kostlivce ve skříni. Linux? Ten se tváří upřímně — open source, jak se tomu říká. Ale Windows? Windows je ta záhadná kráska v koutě baru. Usmívá se, říká, že je všechno v pořádku, a přitom jí ve sklepě hnijou mrtvoly.“
Kapitola 1: Dokonalý zločin
Byl leden 2026, když mi zavolali.
Nette Tester ovládl tajemství vícevláknového testování už v roce 2012. Čtrnáct let spolehlivé služby. Osm vláken v dokonalé souhře by default, jako dirigovaný orchestr. Byl to průkopník — v době, kdy ostatní knihovny pouštěly testy jeden po druhém jako důchodci na poště, Tester už dávno paralelizoval. Šedesát sekund testů stlačených do deseti. Nádhera. Elegance.
Jenže to mělo brzy skončit.
První zprávy přišly od lidí na Windows. Testy, které na Linuxu prosvištěly jak nic, se na Windows vlekly jak šnek po žiletce.
Linux: 3 sekundy. Windows: 21,6 sekundy.
Někdo tu vraždil paralelismus. A dělal to za bílého dne.
Kapitola 2: Podezřelí
Otevřel jsem složku a ponořil se do případu.
Když Tester spustí test, vytvoří nový PHP proces. Tenhle proces běží nezávisle a píše svoje výsledky na standardní výstup — stdout. Jenže jak se k těm výsledkům dostane hlavní program? Přes rouru. Pipe. Neviditelnou trubku mezi procesy, kterou tečou data jako voda.
Na Linuxu je tahle roura chytrá. Když v ní nic není a řeknete „dej mi data“, ona odpoví „nic tu nemám“ a jdete dál. Žádné čekání. Žádné zdržování.
Jenže na Windows?
Zkoušel jsem všechno. První podezřelá byla funkce
stream_set_blocking() — ta měla rouru přepnout do
neblokujícího režimu.
Nefungovala.
Další na řadě byl stream_select() — měla sledovat více
rour najednou a říct, která má data.
Taky mrtvá.
Nakonec jsem zkusil obyčejný fread(). Na Linuxu, když čtete
z prázdné roury, vrátí nic okamžitě. Na Windows? Čeká. Čeká
dokud něco nepřijde. Klidně věčnost.
Tři funkce. Tři mrtvoly. Někdo sprovodil neblokující I/O ze světa už před lety. Zločin tak starý, že křídová silueta dávno vybledla.
Kapitola 3: Blokáda
Představte si tohle:
Osm vláken přijde do hospody. První si objedná — přežene to
s panáky a dá sleep(3). Nic divného. I test si občas
potřebuje zdřímnout.
Jenže pak to začne být zajímavé.
Chcete přečíst výstup toho prvního testu. Sáhnete do roury. Žádný výstup ale není — test spí a nic nevypisuje. A na Windows, když čtete z prázdné roury…
Čekáte.
Čekáte, dokud se test nevzbudí. Zamrzli jste uprostřed pohybu s rukou v rouře. Ostatní vlákna na vás upírají zrak. „Haló? Jsi v pohodě?“ Ale vy jste začali číst a nemůžete přestat. Sedm vláken do vás šťouchá: „Hele, můžem jet? Máme testy k pouštění!“ Jenže vy nemůžete nic. Uvnitř se proklínáte — kdybyste sáhli po jiném testu, mohli jste se teď v baru družit s ostatními, pracovat, být užiteční. Ale ne. Vybrali jste si toho spáče a teď jste jeho rukojmí.
Tři sekundy ticha. Pak se test vzbudí, vyplivne výstup a vy konečně můžete pustit ruku.
Osm vláken. Jedno se zaseklo. Sedm přešlapovalo a čekalo.
Dvacet jedna celá šest sekundy místo tří.
Tohle nebyla vražda. Tohle bylo mučení.
Kapitola 4: Soubory
Mým úkolem je problémy řešit.
Každý detektiv zažije ten moment. Záblesk geniality. Pocit, že to konečně rozlouskl. Můj přišel ve dvě v noci nad vychladlou kávou a zmrazeným kódem.
„Soubory,“ hlesl jsem. „Zapíšeme to do souborů.“
Roura je jako telefonní linka — musíte poslouchat, když někdo mluví, jinak to neslyšíte. Ale soubor? Soubor je jako záznamník. Test do něj napíše, co potřebuje, a vy si to přečtete, až budete mít čas. Žádné čekání. Žádné zamrzání.
proc_open($cmd, [
['pipe', 'r'],
['file', $tempFile, 'w'], // Tady je to!
...
]);
Žádné roury. Žádné záseky. Test píše do souboru, my si ho přečteme, až doběhne. Čisté. Prosté. Geniální.
27 sekund.
Dvacet. Sedm. Sekund.
Proměřil jsem všechno:
proc_open(): 2 ms. Nevinný.proc_get_status(): 4 ms na tisíc volání. Taky čistý.- Čtení a mazání souboru: Pod 10 ms. Nic podezřelého.
A přesto byl výsledek katastrofa.
Zíral jsem na ta čísla, až mi slzely oči. Jednotlivé operace v pohodě. Celek na huntě. Jak je to možné?
Pak mi to došlo. Osmdesát pět testů. Osmdesát pět dočasných souborů. Osmdesát pět vytvoření, zápisů, čtení a smazání. Každá operace rychlá, ale Windows filesystém pracuje jinak než Linux. NTFS žurnálování. Antivir, co každý nový soubor ohmatává jako nervózní celník na hranicích. A ty milisekundy se sčítají.
Smrt po kapkách. Pomalá, ale jistá.
Kapitola 5: Sázka
Docházely mi nápady. A kafe taky.
Pak mě napadlo něco. Něco nebezpečného. Typ nápadu, po kterém detektiv většinou nepřežije třetí dějství.
Co kdybychom prostě nečetli výstup hned?
Myšlenka byla prostá: test běží, my ho necháme být. Nesaháme na rouru, nečekáme, neblokujeme. Až test doběhne — až opustí bar — teprve pak si přečteme, co vypsal. Do té doby se staráme o ostatní vlákna. Pracujeme. Žijeme.
if ($status['running']) {
if (PHP_OS_FAMILY !== 'Windows') {
// Na Linuxu čteme průběžně, tam to funguje
$this->test->stdout .= stream_get_contents($this->stdout);
}
// Na Windows? Nečteme. Nešaháme. Nedýcháme směrem k rouře.
return true;
}
Implementoval jsem to. Zatajil dech. Spustil testy.
3 sekundy.
Tři. Sekundy.
Jako na Linuxu. Jako za starých dobrých časů. Chtělo se mi brečet štěstím. Tančit. Vyběhnout na střechu a—
Zazvonil telefon.
„Máme problém,“ ozval se hlas. „Některé testy prostě nedoběhnou. Visí tam jak prádlo.“
Zastavilo se mi srdce.
„Které?“
„Ty, co hodně vypisují. Spousta echo. Píšou a píšou, a pak — nic. Ticho. Navždy.“
Zavřel jsem oči. Jasně. Jak jsem na to mohl zapomenout.
Buffer roury. Čtyři kilobajty na Windows. Roura není bezedná — je to trubka s omezenou kapacitou. Když test píše a píše a nikdo nečte, buffer se naplní. A když je buffer plný, test zamrzne uprostřed zápisu. Čeká, až někdo uvolní místo. Jenže my nečteme. My čekáme, až test doběhne. On čeká na nás.
Deadlock.
Klasická past. Dva lidé, co na sebe vzájemně čekají u dveří: „Po vás.“ — „Ne, po vás.“ — Navěky.
Vyměnili jsme vraha za vraha.
Kapitola 6: Poslední pokusy
Dalších čtyřicet osm hodin bylo ve znamení kofeinu a čím dál šílenějších nápadů.
Timeout:
stream_set_timeout($this->stdout, 0, 1000); // 1ms timeout
$this->test->stdout .= fread($this->stdout, 8192);
Výsledek: 18,5 sekundy. Lepší. Ale pořád se to zasekává. Timeout byl jenom slušná prosba, se kterou si Windows vytřel.
Kontrola metadat:
$meta = stream_get_meta_data($this->stdout);
if (!empty($meta['unread_bytes'])) {
// Číst, jen když něco je!
}
Výsledek: unread_bytes byla vždycky nula. Vždycky. I když
tam data byla. I když tam byly megabajty. Windows nám lhal přímo
do očí.
Praštil jsem do stolu.
Každá stopa vedla do zdi. Každý důkaz se rozpadl v rukou. Každé řešení jeden problém vyřešilo a druhý vytvořilo. Jako bych hrál šachy se soupeřem, který po každém mém tahu překreslí šachovnici.
Kapitola 7: Pravda
Na detektivní škole vám tohle neřeknou:
Někdy vraha nedopadnete.
Někdy je vrah samotný systém. Zakódovaný v základech. Rozhodnutí, které někdo udělal v Redmondu před dvaceti lety. Člověk, který nikdy nepočítal s tím, že PHP procesy budou chtít komunikovat bez čekání.
Windows prostě nepodporuje neblokující I/O na anonymních rourách. Tečka. Hotovo. Šlus.
Jediná cesta ven vedla přes sockety. TCP sockety. Protože
stream_select() na sockety funguje. I na Windows.
┌─────────────────────┐ ┌─────────────────────┐
│ Test Runner │ TCP localhost:N │ Testovací proces │
│ │◄────────────────────►│ │
│ stream_select() │ │ Výstup jde do │
│ TADY FUNGUJE │ │ socketu, ne stdout │
└─────────────────────┘ └─────────────────────┘
Fungovalo by to. Jsem si jistý.
Jenže by to znamenalo přepsat všechno. Změnit, jak každý test vypisuje data. Překopat hlavní smyčku runneru. Operace na otevřeném srdci pacienta, který čtrnáct let běhá maratony.
Epilog: Hořký konec
Zapálil jsem si cigaretu, kterou nekouřím, a díval se z okna na déšť, který nepršel.
Složka leží na stole. Nevyřešená. Současná implementace — nečíst během běhu — je rychlá jako blesk, ale v kapse nosí bombu. Každý test, který se opováží vypsat víc než 4 KB, zamrzne navěky. Uvízne v digitálním limbu vlastní výroby.
Zavřel jsem složku.
Ne každý případ má šťastný konec. Ne každý vrah skončí za mřížemi. A některé operační systémy prostě jsou, jaké jsou — bez ohledu na to, kolik nocí strávíte na Stack Overflow nebo kolik inženýrů z Microsoftu proklejete.
Paralelismus na Windows je mrtvý. Ať žije paralelismus.
Případ uzavřen. Vrah je pořád venku. Dobrou noc.
Viděl jsem věci, kterým byste nevěřili. Hořící lodě u Orionu. Sledoval jsem, jak se třpytí C-paprsky v temnotě u brány Tannhäuser. A viděl jsem, jak se PHP snaží o neblokující I/O na windowsových rourách. Všechny tyhle vzpomínky se ztratí. Jako slzy v dešti.
— Roy Batty, kdyby dělal v PHP
Chcete-li odeslat komentář, přihlaste se