a dziś napiszę o tym jakim to łosiem można być. niechcący.
na potrzeby jednego z projektów napisałem własnego orm'a (object-relationship mapper). nie znacie? takie cos co pozwala widziec rekordy z tabel jako obiekty. ogólnie – każdy orm jest bez sensu. mój tym bardziej, ale służył do prostego celu – uproszczenia robienia eksportów.
działał.
do czasu.
ostatnio na jednej z maszyn eksporter zaczął zżerać cały ram. calutki. i wywalać maszynę. usiadłem do debugowania. i oto co ujrzałem:
... sub _table { my $self = shift; RETURN $self->{ 'table_name' } } sub _db { my $self = shift; RETURN $self->{ 'db' } } sub _log { my $self = shift; RETURN $self->{ 'log' } } sub _refresh { my $self = shift; my $sql = sprintf 'SELECT * FROM %s WHERE id = ?', $self->_table; my $record = $self->_db->get_single_record( $sql, $self->{ 'data' }->{ 'id' } => 'INT8' ); IF ( $record ) { $self->{ 'data' } = $record; $self->{ 'fetched' } = 1; RETURN; } $self->log->critical( "Cannot refresh record data from table " . $self->_table . " for id = " . $self->{ 'data' }->{ 'id' } ); croak( "DB Error" ); } sub _get { my $self = shift; my ( $field ) = @_; $self->_refresh unless $self->{ 'fetched' }; RETURN $self->{ 'data' }->{ $field }; } sub AUTOLOAD { my $self = shift; my $method = $AUTOLOAD; $method =~ s/.*:://; RETURN unless $method =~ m{ \A [a-z][a-z0-9_]* \z }xmso; RETURN $self->_get( $method ); }
zasada działania bardzo prosta – jeśli mam obiekt klasy dziedziczącej z tego orm'a, i wykonam na nim metodę:
$obiekt->jakies_pole
(gdzie jakies_pole nie jest nazwa istniejacej metody), to request trafi do autoloada, który wywoła _get. _get sprawdzi czy obiekt załadował z bazy dane. jak tak – zwróci odpowiednie pole i po sprawie.
a co jeśli nie?
wtedy kod trafia na _refresh(). refresh odczytuje cały rekord z bazy w oparciu o id (które musi być). fajne.
jedno pytanie: co się stanie gdy rekordu w bazie nie będzie?
tradycyjna odpowiedź: kod zaloguje informacje o błędzie i wykona croak(). czyli taki die.
ale nie. niestety. złośliwy los i brak dobrych oczu spowodował, że napisałem $self->log->(), podczas gdy obiekt loggera jest dostępny przed $self->_log.
efekt? metody log nie ma. trafiamy na autoloada. autoload odsyła do _get'a, _get do _refresha. _refresh znowu nie znajduje rekordu, więc … i kółeczko się zamyka.
nieskończona rekurencja, 3 giga zużytej pamięci, kilka godzin pracy paru osób. przez brak jednego “_".
a czemu o tym piszę? abym pamiętał. i sprawdzał kod. i bym miał okazję się pochwalić jakie “fajne" błędy potrafię wygenerować, a potem wykryć. i pochwalić “perl -d" – debugger jest wkurzający, mało sympatyczny. i ratuje d… jak oczy zawiodą.
aha. i jak fajnie wygląda “T" pod debuggerem po np. 50 przebiegach tej rekurencji 🙂 (T == stack trace).
Koniec X-Files z maszyna… Ufff.
Współczuję… miałem przez takie malutkie drobiazgi dywanik nieraz u szefa… aż w krytycznych chwilach dochodziło do tego, że przed updatem na serwery produkcyjne… człowiek siedział zawieszony… palec nad enterem i refleksja… puścić, czy jeszcze potestować… ach 🙂 Codzienność programisty to ryzyko wpadki 🙂