main

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(), как пишут в мануалах в интернете!
  <...>
}

Идея, думаю, понятна.


  1. ./mojo generate app ↩