Pracuję od jakiegoś czasu nad projektami, w których istotną część stanowią (autorskie) konsolowe narzędzia do zarządzania wewnętrznymi procesami aplikacji. Aktualizacja danych, wysyłka maili, itp. – każdej operacji odpowiada polecenie i odpowiedni zestaw argumentów, dzięki czemu zamiast pisania kodu w pocie czoła mogę ograniczyć się jedynie do sprawdzenia pliku logu. Nie znaczy to, że jestem „bezrobotny”, ale przynajmniej mam dużo czasu na „kreatywną” część programowania, zamiast tej odtwórczej. Wszystko fajne, dopóki działa – ostatnio jedno z poleceń zaczęło ni stąd ni zowąd wyrzucać błędy, i to BłędyNieByleJakie™. Zapraszam do lektury artykułu.

<reklama> Na wstępie chciałbym naprawdę szczerze polecić komponent Console Symfony2 do pisania jakichkolwiek poleceń konsolowych w PHP. Nie trzeba importować całego frameworka, wystarczy pojedyncza zależność w composer.json (symfony/console: 2.*), composer install i „samo się robi”. </reklama>

W ramach wprowadzenia powiem, że wymienione wyżej polecenie ma za zadanie ściągnąć kod HTML podanego adresu URL, wyciągnąć z kodu wszystkie obrazki, ściągnąć je i jeśli spełniają pewne warunki (wymiary, rozmiar pliku) zapisać wspomniane informacje w bazie.

Wszystkie te operacje wykonują się poprawnie, poza ostatnią, najważniejszą – zapisem w bazie. Analizując kod i izolując obszar występowania potencjalnych błędów udało mi się jedynie odbić od ściany (przynajmniej po stronie PHP), ponieważ segfault dzieje się dokładnie w momencie wykonywania metody $em->flush() na obiekcie EntityManagera. Sam proces występowania błędu jest też ciekawy, za każdym razem objawia się on jednym z przypadków:

  • końcem skryptu, kontrola konsoli po prostu wraca do promptu,
  • komunikatem „Segmentation fault”,
  • komunikatem „zend_mm_heap corrupted”.

Gdybym nie miał w swojej karierze przygód z C / C++ (BTW bardzo przyjemnie wspominam te czasy :)), ten wpis mógłby się zakończyć w tym momencie, ale na szczęście tak nie było i możemy drążyć dalej temat. Odkopałem trochę wiedzę sprzed lat i żeby przynajmniej mieć dalej co analizować wykonałem jeszcze raz problematyczne polecenie z prefiksem:

LD_PRELOAD=/lib/i386-linux-gnu/libSegFault.so php app/console [command] [args]

Trik z LD_PRELOAD (dokumentacja ld) polega w (wielkim skrócie) na włączeniu w runtime aplikacji (w tym przypadku interpretera PHP) biblioteki (plik libSegFault.so) przed wszystkimi innymi, która zamiast krótkiego „przykro mi” wyrzuci na ekran pełne dane dotyczące środowiska, w którym wystąpił błąd. Ścieżkę do biblioteki musicie sobie niestety znaleźć sami, chyba, że tak jak ja korzystacie z Ubuntu 12.04, wtedy macie łatwiej. Interesujące fragmenty wyniku:

*** Segmentation fault
Register dump:
(...)
Backtrace:
php(zend_hash_destroy+0x16)[0x832b556]
php(_zval_dtor_func+0x56)[0x831c3c6]
php[0x834360a]
php[0x83806fb]
php[0x8381043]
php[0x834b0ca]
php(execute+0x1be)[0x8345d3e]
/usr/lib/php5/20090626+lfs/xdebug.so(xdebug_execute+0xcdc)[0xb6cca123]
php[0x83963c1]

Memory map:
(...)
Segmentation fault (core dumped)

Zamiast LD_PRELOAD możecie skorzystać z narzędzia catchsegv, które jednak w tym przypadku nie chciało współpracować. Mówi się trudno. Wracając do tematu: nie będę udawał mądrego, szukałem jakichkolwiek skojarzeń z czymś, co mogłoby mi się skojarzyć z czymkolwiek innym. :D W tym przypadku jednak miałem trochę szczęścia i w kombinacji ze skonsolidowanymi informacjami z zakamarków Internetu wydedukowałem, że „tradycyjnie” pamięć po czymś została zwolniona, a coś innego i tak próbowało ją odczytać, no i zonk. Błąd, rzekłoby się, licealny, ale jednak – nie działa.

Oczywiście nie posądzam o nic programistów interpretera, bo robią naprawdę dobrą robotę, ciężko też posądzać o fuszerkę Fabiena i całe Symfony2, czy też Benjamina i całe Doctrine2. Jak mówi stare programistyczne porzekadło:

„jeśli nie możesz znaleźć źródła błędu, spójrz w lustro”

Tym razem jednak to (na szczęście) nie była moja wina – analizując zdobyte na różne sposoby stack trace’y i var_dump()’y z kodu doszedłem do finalnego wniosku, że Doctrine sam łapie się w pułapkę swojej złożoności i w kombinacji z zewnętrznymi mechanizmami garbage collectora (który okazał się być głównym winnym) PHP wysiada podczas sprawdzania, czy wszystkie dane, które na pewno są poprawne, na pewno są poprawne.

W PHP 5.3 został wprowadzony mechanizm garbage collectora, który w inteligentny sposób zajmuje się usuwaniem nieużywanych referencji (cyklicznych referencji) do zmiennych. Domyślnie jest włączony, ale korzystając z funkcji gc_enable() i gc_disable() można dynamicznie (w trakcie działania skryptu) sterować jego działaniem. Wstawiłem więc na początku metody execute() klasy polecenia instrukcję:

gc_disable();

i problem zniknął. Okazało się, że Doctrine tak się „zamotał” z „cyklicznymi referencjami”, że już nie było dla niego ratunku. Tym, którzy nie pracują z tą biblioteką warto wyjaśnić, że każdy pobrany rekord jest przechowywany w jednym unikalnym w skali runtime obiekcie (tj. mamy gwarancję, że zawsze operujemy na tym samym kawałku pamięci). Problem powstaje wtedy, kiedy mechanizm garbage collectora PHP „zwinie” nam rzeczony obiekt sprzed nosa, a my się potem do niego odwołujemy w kodzie.

Jak zawsze, jestem otwarty na propozycje lepszych rozwiązań tudzież porady, jak można to lepiej zdebugować. Do usłyszenia!