Описание
Маленькое расширение для FCGI::ProcManager, позволяющее обращаться к менеджеру fcgi процессов. Для связи сторонней программы с менеджером используется сокет.
Подводные камни
Модуль FCGI::ProcManager используется для порождения обработчиков входящих запросов. Текущий процесс является менеджером. Со старта он порождает обработчиков (n_processes штук), далее он поддерживает их количество, следя за погибшими в бою. Для этих целей он использует wait. Тут и кроется проблемка. После того, как запущены потоки, менеджер, вызывая wait, блокируется. Достучаться до него можно только через сигналы. Исполнять в обработчике сигнала код нужно с умом и аккуратно, гонять там говнокод — нехорошо. А значит необходимо наладить другой канал связи.
Реализация
Для примера попробуем заставить менеджера слушать сокет. Для этого нам надо заставить менеджера не блокироваться. За ожидание в FCGI::ProcManager отвечает функция pm_wait:
sub pm_wait {
my ($this) = self_or_default(@_);
# wait for the next server to die.
next if (my $pid = wait()) < 0;
# notify when one of our servers have died.
delete $this->{PIDS}->{$pid} and
$this->pm_notify("server (pid $pid) exited with status $?");
return $pid;
}
Нужно заменить вызов wait на waitpid и вставить туда наш код. Наш менеджер должен открыть и слушать еще один порт, и в режиме ожидания периодически смотреть за потомками и слушать порт. Напишем свой вариант функции pm_wait и подменим одноименную функцию в модуле FCGI::ProcManager с помощью monkey patch. В качестве реакции на сообщения будем перегружать всех потомков, предварительно обновив бибилиотеки проекта.
use FCGI;
use FCGI::ProcManager;
use IO::Select;
use IO::Socket;
use POSIX ":sys_wait_h";
#use Module::Refresh;
use feature ':5.14';
use strict;
# For fastcgi app
my $socket = FCGI::OpenSocket( ":9019", 5 );
my $request = FCGI::Request( \*STDIN, \*STDOUT, \*STDERR, \%ENV, $socket, FCGI::FAIL_ACCEPT_ON_INTR );
# To communicate with the manager
my $server = IO::Socket::INET->new(LocalPort => '9034',
Type => SOCK_STREAM,
Reuse => 1,
Listen => 1)
or die "Couldn't start messange server $@\n";
my $select = IO::Select->new($server);
# Patch
my $pm_wait = sub {
my ($pm) = @_;
my $pid;
while (1) {
last if ($pid = waitpid(-1, WNOHANG));
foreach my $client ($select->can_read(1)) {
if ($client == $server) {
$client = $server->accept();
$select->add($client);
}
else {
my $msg = <$client>;
chomp $msg;
if ($msg eq 'reload') {
#my $refresher = Module::Refresh->new();
#my @module_list = qw( MyLib1.pm My/Lib2.pm );
#foreach my $module_name (@module_list){
# $refresher->refresh_module($module_name);
#}
$pm->sig_manager('HUP');
}
print $client "done!\n";
$select->remove($client);
close $client;
}
}
}
# notify when one of our servers have died.
delete $pm->{PIDS}->{$pid} and $pm->pm_notify("server (pid $pid) exited with status $?");
return $pid;
};
no strict 'refs';
*{'FCGI::ProcManager::pm_wait'} = $pm_wait;
use strict 'refs';
my $pm = FCGI::ProcManager->new( );
$pm->pm_manage( n_processes => 3, pm_title => 'perl-fcgi-pm', die_timeout => 10 );
while ( $request->Accept() >= 0 ) {
$pm->pm_pre_dispatch();
print "Content-Type: text/plain\n\n";
print "server works";
$pm->pm_post_dispatch();
}
FCGI::CloseSocket($socket);
close $server;
Следом код для теста:
use IO::Socket;
use feature ':5.14';
use strict;
my $socket = new IO::Socket::INET(Proto => "tcp",
Type => SOCK_STREAM,
PeerPort => 9034,
PeerAddr => "127.0.0.1" )
or die "Can't connect: $!";
#my $msg = $ARGV[0];
my $msg = 'reload';
if ($msg) {
print $socket "$msg\n";
my $answer = <$socket>;
say "Answer: ".$answer;
}
Итого
Таким образом мы имеем возможность посылать сообщения (протокол можно придумать самому) менеджеру fastcgi процессов. Это можно использовать для опроса текущей конфигурации приложения. Перезагрузить бибилиотеки проекта (закомментировано в примере). Можно увеличить/уменьшенить количество обработчиков
$pm->{n_processes} = 10;
$pm->sig_manager('HUP');
Ну и такие мелочи как включение/отключение профилирования. Само собой эти задачи можно решить другими способами.
There's more than one way to do it