среда, 22 октября 2008 г.

Background execution of subroutines in child processes

При работе с распределенной базой данных иногда возникает необходимость выполнить один и тот-же запрос на каждом сегменте базы. По-очереди - долго, хочется параллельно.

Для решения этой и подобных задач написал маленький модуль BGS - Background execution of subroutines in child processes. Модуль позволяет упростить выполнение подпрограмм в дочерних процессах, ожидание их завершения и возврат результатов работы подпрограмм из дочерних процессов в основной.

Пример использования:

use BGS;

my @foo;

foreach my $i (1 .. 2) {

bgs_call {
# child process
return "Start $i";
}

bgs_back {
# callback subroutine
my $r = shift;
push @foo, "End $i. Result: '$r'.\n";
};

}

bgs_wait();

print foreach @foo;

Код, который будет выполняться в дочерних процессах, помещается в блок bgs_call.

Код, который выполниться в основном процессе, при завершении bgs_call, находиться в блоке bgs_back. Ответ bgs_call (скаляр или ссылка, но не список) передается в bgs_back в качестве аргумента.

Механизм запускается командой bgs_wait. Не забудьте перед созданием дочерних процессов, закрыть все соединения к базе. Почему это желательно сделать, смотрите в заметке "Отцы и дети или perl, fork и деструкторы".

Внимание, код, который будет вызываться из bgs_call, на данный момент ничего не должен печатать на STDOUT! Надо, все таки, собраться и устранить этот недостаток. А может еще добавить timeout ожидания?

Кстати, на CPAN недавно обнаружил модуль Parallel::SubFork с похожим функционалом. Судя по датам BGS и Parallel::SubFork писались приблизительно в одно время. :-)

вторник, 21 октября 2008 г.

Cooking Plain Old Documentation

Недокументированный код - это, обычно, одноразовый код.
А плохая документация - хуже, чем ее отсутствие.
Следовательно, надо писать хорошую документацию.
А хорошую документацию хочется читать в хороших условиях.

Я, например, предпочитаю читать не с экрана монитора, а с бумажной копии, сидя в уютном кресле с чашечкой ароматного зеленого чая.

Документацию пишу в Plain Old Documentation.
Конечно, если необходимы математические формулы или графика, то привлекаю более мощные средства, но сейчас разговор не о них. Кстати, не забывайте о podchecker, а то некоторые трансляторы с POD слишком снисходительны.

Но вернемся к подготовке POD к печати.
Под Windows самым простым способом является преобразование POD в HTML и печать его из под MS Office. А чтобы документ был более красивым, то можно использовать свой шаблон со стилями.

Под UNIX, как все знают, MS Office нет :-), а Open Office тогда еще не было.
Да и изучать Open Office нет желания, так как считаю не рациональным знать и MS Office, и Open Office, тем более, что есть альтернативы. Например, pod2man или pod2latex.

Вариант с man и groff мне как-то не пришелся по душе, поэтому я даже не пытался его использовать. А вот вариант с LaTeX применял, но все таки ставить TeX систему ради одного POD - это, по моему, перебор, тем более, что есть крошка lout.

Lout - система форматирования текста, подобная TeX и производящая PostScript файл.
http://lout.wiki.sourceforge.net В lout входит также программа prg2lout, предназначенная для трансляции исходников в lout разметку. Среди поддерживаемых языков имеется Perl и POD.

Например, чтобы распечатать документацию по модулю Foo, я набираю следующие команды:

podselect Foo.pm > Foo.pod
prg2lout -l pod -n Foo.pod | lout -s | ps2pdf - > Foo.pdf
lpr Foo.pdf

Кончено, я это делаю не вручную, а при помощи make (кроме вызова lpr, разумеется).

Но листы формата A4 иногда не очень мне нравятся...
Какой я все таки бываю капризным: "хочу книжечку читать и точка!"

В таком случаете, я использую вот такой конвейер команд:

prg2lout -l pod -n Foo.pod | lout -s | psbook | psnup -2 | ps2pdf - > Foo.pdf

Несколько сгибов, две скрепки - и у меня имеется красивая брошюра формата А5!
Осталось только сделать чай. :-)

А как вы "готовите" POD?


P.S.
Когда необходимо сделать документацию в виде набора HTML документов с перекрестными ссылками,
более всех понравился мне модуль Pod::Simple::HTML. Его просто использовать для пакетной обработки, создания индекса и кастомизации.

понедельник, 13 октября 2008 г.

Perl: Асинхронный конвейер HTTP клиентов

Как-то давным давно почувствовал я на работе запах дыма. Думал - пожар, но выглянув в коридор, увидел, что дым не так уж и велик, и валит из офиса SEO (search engine optimization).

Хотя было вернуться к себе на рабочие место, но природное любопытство пересилило и я решил пойти посмотреть, что твориться у оптимизаторов. Оказалось, все просто - на экранах мельтешили окна, все были ужасно заняты, а дым шел от клавиатур - плавилась пластмасса от такой нагрузки!

Когда мне наконец-то удалось привлечь к себе внимание, выяснил, что для какого-то исследования оптимизатором надо было закачать несколько десятков тысяч web страниц. Что за исследования и где они взяли этот список url я уточнять не стал, просто пожелал им плодотворной работы, а сам запомнил где лежит этот список url.

Да, придется ликвидировать причину дымa, - подумал я на обратной дороге. Нет, оптимизаторов я ликвидировать не собирался, просто решил им сделать скриптик, которых закачает все эти странички, тем более, что у меня уже был опыт работы с модулем LWP::Parallel::UserAgent.

Задача сводилась к тому, чтобы по мере обработки читать с файла новые url, асинхронно запрашивать страницы и записывать HTTP ответы в файлы. Конечно можно было все сделать последовательно, но уж больно много времени потребовалось бы на это.

Как оказалось, для организации такого конвейера более подходит модуль HTTP::Async, а не LWP::Parallel::UserAgent.

Вот примерный код, который использовался для этого (работа с файлами опущена):

use HTTP::Message 1.57;
use HTTP::Request;
use HTTP::Async;

my @urls = (
'http://www.perl.com',
'http://www.perl.org',
'http://perlmonks.org',
'http://www.pm.org',
'http://kiev.pm.org',
'http://www.parrot.org',
'http://www.parrotcode.ks.ua',
);

my $async = HTTP::Async->new;

my $max_connects = 3;
my $cnt = @urls > $max_connects ? $max_connects : @urls;
add_request() foreach (1 .. $cnt);


while ( my $res = $async->wait_for_next_response ) {
if($res->is_success()) {
print "Succeeded for '", $res->request->url, "'\n";
# print $res->content, "\n";
}
add_request();
}


sub add_request {
my $url = shift @urls or return;
my $req = HTTP::Request->new(GET => $url,
['User-Agent' => "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)"]);
$async->add($req);
}

В это пример первоначально создается $max_connects запросов и по мере их завершения, создаются все новые и новые.

Все очень просто. Но опять у меня проснулась любопытство: а нельзя это все таки для такой конвейерной обработки приспособить модуль LWP::Parallel::UserAgent?

Оказалось можно. Для этого просто нужно переопределить события on_return и on_failure:

$SIG{PIPE} = 'IGNORE';
use LWP::Parallel::UserAgent;

my @urls = (
'http://www.perl.com',
'http://www.perl.org',
'http://perlmonks.org',
'http://www.pm.org',
'http://kiev.pm.org',
'http://www.parrot.org',
'http://www.parrotcode.ks.ua',
);

my $ua = LWP::Parallel::UserAgent->new();
$ua->agent("Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)");
$ua->nonblock(1);

my $max_connects = 3;
my $cnt = @urls > $max_connects ? $max_connects : @urls;
add_request() foreach (1 .. $cnt);


sub add_request {
my $url = shift @urls or return;
my $req = HTTP::Request->new(GET => $url);
$ua->register($req);
}


{
no warnings;
sub LWP::Parallel::UserAgent::on_return {
my ($self, $request, $response, $entry) = @_;
if($response->is_success()) {
print "Succeeded for '", $response->request->url, "'\n";
}
add_request();
}

sub LWP::Parallel::UserAgent::on_failure { add_request() }
}

my $entries = $ua->wait(3);

Что ж не так красиво, как при использовании HTTP::Async, но свою задачу этот код выполняет. Разве что очень не понравилась необходимость установить $SIG{PIPE} = 'IGNORE';

Ну вот и все история... :-) Да... давно это было... Но тем не-менее, кому-то может и пригодиться этот опыт и сейчас.

Копия: http://kiev.pm.org/node/253