Пишем мультипроцессорные приложения на AnyEvent::Fork + Staticperl

Иногда необходимо скомпилировать программу с помощью staticperl и запустить ее на другой машине. Все хорошо, только программа не запускается как вам хочется.

Часть 1. Ошибка Broken pipe

Как описано в документации к AnyEvent::Fork для программ с использованием staticperl необходимо использовать модули AnyEvent::Fork::Early и AnyEvent::Fork::Template.

Возьмем в качестве примера код ниже:

Тестовый код №1 (broken_pipe-01.pl) download
#!perl

use strict;
use warnings;

use AnyEvent::Fork::Early;

# we will use JSON::XS::encode_json as our function
# in forked process.

use JSON::XS;

sub worker_run {
    my ($fh, $txt) = @_;

    printf "%d ==> %s\n",
        $$,
        &JSON::XS::encode_json([$txt]);
}

# again fork process and use code above
# in child.

use AnyEvent::Fork::Template;

# From here starts main program.
use AnyEvent;

my @worker_fhs;
my $cv = AE::cv;

$cv->begin;
for (1..5) {
    $cv->begin;
    $AnyEvent::Fork::Template
        ->fork
        ->send_arg($_)
        ->run("worker_run", sub {
            push @worker_fhs, shift;
            $cv->end;
        })
    ;
}

$cv->end;
$cv->recv;


# wait a little bit...
$cv = AE::cv;
my $t; $t = AE::timer 2, 0, sub {
    undef $t;
    $cv->send;
};

$cv->recv;

При обычном запуске через команду perl вывод будет следующий:

11213 ==> ["1"]
11214 ==> ["2"]
11217 ==> ["5"]
11216 ==> ["4"]
11215 ==> ["3"]

Соберем данный скрипт с помощью staticperl в готовую программу:

~/staticperl mkapp test-staticperl --boot s_ae_fork.pl \
-MCwd -MEnv -Msort.pm -Mvars \
-Mfeature.pm \
-Mutf8 -Mutf8_heavy.pl \
-MConfig -MConfig_heavy.pl \
-MErrno -MFcntl \
-MPOSIX -MSocket \
-MCarp -MEncode -MEncode::Unicode \
-MScalar::Util -MTime::HiRes -MStorable \
-Mcommon::sense -MEV -MGuard -MAnyEvent \
-MAnyEvent::Handle -MAnyEvent::Socket \
-MAnyEvent::Impl::EV -MAnyEvent::Impl::Perl \
-MAnyEvent::Util -MAnyEvent::Log \
-MJSON::XS -MJSON \
-MObject::Accessor \
-MProc::FastSpawn -MIO::FDPass \
-MAnyEvent::Fork \
--usepacklists \
--static

Теперь, чтобы проверить как поведет себя данная программа на другой машине, где скорей всего не будет перла или необходимых модулей (в нашем случае JSON::XS), запустим программу test-staticperl в chroot окружении:

$ mkdir chroot-debian
$ sudo debootstrap --arch amd64 wheezy chroot-debian http://http.debian.net/debian

$ sudo cp test-staticperl chroot-debian/
$ sudo chroot chroot-debian/

root@debian:/# ./test-staticperl 
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = "en_US:en",
        LC_ALL = (unset),
        LANG = "en_US.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
EV: error in callback (ignoring): AnyEvent::Fork: command write failure: Broken pipe at AnyEvent/Fork.pm line 72.
!boot did not return a true value.

Обращаем внимание на ошибку: Broken pipe at AnyEvent/Fork.pm line 72

Если залезть внутрь модуля AnyEvent/Fork.pm и найти строку 72, то это нам ничего не даст, потому что на самом деле ошибка находится в другом месте. А именно в изменении переменной $0 (подробности в perldoc perlvar). К сожалению, ее изменять нельзя, когда используется staticperl. Закомментировав все строчки, где меняется переменная $0 в модуле AnyEvent::Fork ошибка исчезла. Однако, и ожидаемого вывода не последовало. Также необходимо закомментировать строчки и в файле AnyEvent/Fork/Serve.pm. После этого все заработало как положено!

root@debian:/# ./test-staticperl 
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = "en_US:en",
        LC_ALL = (unset),
        LANG = "en_US.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
14768 ==> ["5"]
14767 ==> ["4"]
14765 ==> ["2"]
14766 ==> ["3"]
14764 ==> ["1"]
!boot did not return a true value.

Часть 2. Хотим больше процессов и разных

Использованный пример кода выше удобен, когда у нас имеется всего один процесс. А что делать, когда мы хотим сделать несколько шаблонов процессов с разными модулями? Ответ на этот вопрос нашелся после изучения исходников AnyEvent::Fork (но вам это не нужно:)

Тестовый код №2 (broken_pipe-02.pl) download
#!perl

use strict;
use warnings;

use AnyEvent::Fork::Early;

my $PREFORK_JSON = AnyEvent::Fork->new();
my $PREFORK_DUMP = AnyEvent::Fork->new();

use AnyEvent;
use AnyEvent::Fork::RPC;

printf "parent pid: %d\n", $$;

my $json = $PREFORK_JSON
    ->require('JSON::XS')
    ->AnyEvent::Fork::RPC::run("JSON::XS::decode_json")
;

my $dump = $PREFORK_DUMP
    ->require('List::Util')
    ->AnyEvent::Fork::RPC::run("List::Util::sum")
;

my $cv = AE::cv;

$cv->begin;
$json->('["Perl rulez!"]', sub {
    printf "json returns: @_\n";
    $cv->end;
});

$cv->begin;
$dump->(1..10, sub {
   print "list returns: @_\n";
   $cv->end;
});

$cv->recv;

# wait a little bit...
$cv = AE::cv;
my $dummy = AE::timer 120, 0, sub { $cv->send };
$cv->recv;

Запустим программу как обычно и посмотрим на дерево процессов.

$ ~/staticperl perl test2.pl 
parent pid: 15368
list returns: 55
json returns: ARRAY(0x27ec240)

Пока программа ожидает, в другом терминале:

$ pstree -p 15368
perl(15368)───AnyEvent::Fork:(15373)─┬─JSON::XS::decod(15374)
                                     └─List::Util::sum(15375)

Заключение

Используя AnyEvent::Fork и staticperl можно создавать портируемые программы (все относительно:) и забыть об инсталляции дополнительных модулей на удаленных серверах. Я приложил два патча:

Первый исправляет проблемы в AnyEvent::Fork модуле. Второй касается вычисления команды ncpu из модуля AnyEvent::Fork::Pool.

Удачных компиляций!