Mojo, hypnotoad и DBI
http://bash.im/quote/393352 // ...вместо традиионного афоризма.
Здесь описана история моих боданий с гипножабой по поводу коннектов к базе.
Прелюдия
Работать с голым DBI - удовольствие так себе. Хочется:
- не писать каждый раз лапшу из prepare/execute/fetch
- соблюдать DRY
Вывод - нам нужна собственная высокоуровневая библиотека, куда будем дописывать методы по мере надобности. А вот внутри - всё тот же DBI и хелперы.
Этап нулевой - скрипт
Костяк библиотеки:
package MyDB;
use strict;
use warnings;
use DBI;
sub new {
my ($class, $opts) = @_;
my $dbh = DBI->connect($opts->{dsn}, $opts->{user}, $opts->{pass}, {
RaiseError => 1, AutoCommit => 1, <..more..>
});
my $self = { dbh => $dbh };
return bless($self, $class);
}
sub get_stats
{
my ($self) = @_;
<..code..>
}
# больше методов!
1;
Типовой скрипт:
#!/usr/bin/perl
use strict;
use warnings;
use MyDB;
my $db = MyDB->new;
$db->get_stats();
В таком виде - работает безукоризненно, идём дальше.
Этап первый - morbo
Далее мы хотим, чтобы скрипт работал постоянно и показывал нам статистику с браузере по запросу.
Берём mojolicious, делаем типовое приложение1.
Как проще всего девелоперу прикрутить туда наш модуль? Да очень просто:
package MyAPP;
use strict;
use warnings;
use Mojo::Base 'Mojolicious';
use MyDB;
sub startup {
my $self = shift;
<...>
my $db = MyDB->new({user => 'user', ...});
my $self->app->{db} = $db;
<...>
$self->app->{db}->init();
<...>
}
Фреймворк поставляется с 2 штатными серверами - morbo (для разработки), и hypnotoad (для "ынтерпрайзного продакшона").
Проблема: не обрабатывается перезагрузка сервера БД.
Это может быть решено таким кодом в нашей библиотеке:
sub new {
my ($class, $opts) = @_;
my $self = { dbh => $dbh, %$opts };
return bless($self, $class);
}
sub conn {
my ($self) = @_;
my $dbh = DBI->connect_cached($self->{dsn}, <...>);
return $dbh;
}
Т.е. вместо подключения при вызове new(), мы просто сохраняем параметры подключения. И в дальнейшем работаем через метод $db->conn.
Здесь есть одна засада: всё это хорошо, но не работает совместно с транзакциями. connect_cached МОЖЕТ вернуть другой handle. В результате часть запросов пойдёт в одно соединение, оно на середине отключится, остальная - в новое и в результате теряется сам смысл использования транзакций.
Этап второй - hypnotoad
Ну хорошо, всё работает, пробуем запустить наше приложение на другом сервере и обламываемся. Почему? Потому что fork().
sub startup {
<...>
$self->app->{db}->init();
<...>
}
Вот этот код у нас создаст соединение ДО форка, и worker'ы будут работать с устаревшим хэндлом. Что самое интересное - connect_cached тут не поможет. Соединение не просто невалидно, оно было отключено по всем правилам при вызове DESTROY'я родителя.
Если вы видите в логе "has gone away" и динозаврика в браузере сразу после запуска - это оно.
Лечится это таким кодом:
sub startup {
my $self = shift;
<...>
$self->app->attr(db => sub {
my $db = MyDB->new({user => 'user', ...});
return $db;
});
<...>
$self->app->{db}->init();
<...>
}
attr() - это метод Mojo::Base. Теперь каждый отдельный процесс получит по копии модуля со своим собственным хэндлом.
Этап третий - alltogether
И на закуску - вопрос: что будет, если оставить такое приложение запущенным на сутки и не трогать? Ба, да это же наш старый знакомец - динозаврик! А всё потому, что по мнению сервера БД - наш хэндл протух из-за неактивности.
Ну бывает, да, что делать-то будем? Будем костылять, чего же ещё.
<...>
use Mojo::IOLoop;
sub startup {
my $self = shift;
<...>
my $loop = Mojo::IOLoop->singleton;
$loop->recurring(300 => sub {
$self->app->db->ping;
});
# не вызывайте здесь $loop->start(), как пишут в мануалах в интернете!
<...>
}
Идея, думаю, понятна.
./mojo generate app ↩