Описание
Маленькое расширение для 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