Как стать автором
Обновить

Комментарии 31

Exit и die делают в FastCGI то же, что и в CGI - заканчивают работу, и, собственно прибивают FastCGI'шного демона. Надо так

REQ:
while ($query=CGI::Fast->new)
{
# Тело программы
}


Соотвественно там, где перехватываешь надо просто вставлять

next REQ;
Нда.. Любопытно. Про next я как-то не подумал. Никогда не видел, чтобы с помощью next на метку вываливались из нескольких вложенных вызовов процедур и eval-ов.

Интересно, чем по сути этот next отличается от того же goto?
из perldoc -f next:
"next" cannot be used to exit a block which returns a value such as "eval {}", "sub {}" or "do {}", and should not be used to exit a grep() or map() operation.
Так что, увы, next не годится. Либо die, либо goto...
А что мешает на поимке эксепшена сделать next?
Разрешите вопрос в сторону. Зачем делать exit() вообще? Почему нельзя сделать так, чтобы после вывода результата программа завершалась естественным образом?
В топике есть пример. exit делается потому, что выполнение программы может быть прервано в любой момент.

Приходит запрос в некий html-шаблон, передаются параметры. Нормальная работа заключается обычно в том, что выполняется код, связанный с этим шаблоном, и сам шаблон заполняется данными и отдаётся пользователю. В этом случае exit действительно не нужен. Но иногда, в процессе выполнения кода, возникает одна из нескольких исключительных ситуаций: редирект, ошибка либо отправка файла (могут быть и другие примеры, например, при эффективном использовании кеширования может потребоваться отдавать "304 Not Modified", при ручной обработке авторизации может потребоваться "401 Not Authorized", etc.). Иными словами нужно прервать выполнение кода связанного с запрошенным шаблоном, прервать обработку самого шаблона, и вместо всего этого выдать пользователю какой-то специальный ответ.

Так вот, код, который принял решение выдать юзеру редирект или ошибку, может находится на глубине нескольких вложенных функций и eval-ов. И для того, чтобы обойтись без exit, необходимо "всплыть" на самый верх. Это можно сделать либо с помощью goto (ещё, возможно, next), либо через цепочку return-ов, либо через цепочку die. Последние два способа однозначно корректные, но они требуют наличия специального пользовательского когда везде, где он делает eval (в случае die) или вызывает процедуру (в случае return). Вариант с return самый уродливый и сложный, так что его обычно никогда не используют (я его упомянул просто для полноты картины).

И получается, что если не делать exit, и не делать goto/next, то остаётся только die. Причём этот die должен вернуть некую уникальную ошибку, а юзер, после каждого своего eval, должен эту ошибку передавать выше:

...
eval { do_something() };
die if $@ && $@ eq "PLEASE PROPAGATE: NORMAL EXIT\n";
...

Это самый корректный способ, но его сложно гарантировать. Например - использование внешних CPAN-модулей. Они могут принимать от пользователя callback, вызывать его внутри своего eval, и не делать этот die после eval. Полагаться на такое, корректное, поведение чужого когда - это прикапывать грабли, с целью в будущем на них с размаху наступить.

Вот поэтому текущий движок использует exit. Более того, это считается абсолютно нормальным, когда в середине выполнения CGIшка отдаёт юзеру ответ и выходит. И тот же mod_perl считает вызов exit легальным (для чего и перехватывает его превращая в die).
У меня есть ощущение, что что-то не так в Датском королевстве, но обосновать не могу :) Толи в архитекторе, толи в реализации какие-то недоработки, требующие таких подпорок. Или может я так привык к цепочке try .. except, что для меня аварийный выход является нонсенсом.
У меня аналогичное ощущение. Так что если кто-то обоснует - буду только рад.
Возможно, стоит таки потребовать, чтобы необрабатываемые исключения передавались дальше... это, вообще-то, вопрос гигиены. А левые модули с CPAN, которые этого не делают - патчить.

Проблема с этим подходом только одна: на поиск каждого такого бага, обычно в чужом коде, будет уходить куча времени. :(
Кстати, а в какие сторонние модули ты передаешь callback? Мне на ум приходит только парсинг SGML через SAX.
Да ни в какие. Всё это просто было очередным помутнением разума, попыткой опять написать что-то супер надёжное и универсальное.

Теперь Датское королевство переписано надлежащим образом, без перехватов die и вызовов exit. Всё стало очень культурно и аккуратно. И проблема с goto отпала сама собой - сейчас фреймвок сам генерит исключение вместо exit. И никаких вложенных eval-ов в тех местах, где может быть редирект у меня ни в одном проекте нет... и если когда-нить появятся, значит после этого eval необработанные исключения будут передаваться наверх, как и положено.

В общем, ощущения у нас были правильные - изменил архитектуру и проблема исчезла.
На самом деле фреймвок был не так уж плох. Просто ему уже шесть с половиной лет (!), в течение которых он активно использовался практически без изменений... он просто морально устарел за это время, вот и всё.
Рефакторинг - наше все :)
Ну и отлично
Вы абсолютно правы. Не суметь написать код таким образом, чтобы через простую цепочку return'ов откатиться до верхнего уровня вызовов, - это значит, что лучше не браться за такие вещи, как написание своего фреймворка, а тем более под FastCGI, реализация которого в Перле просто ужасна.

Что exit, что goto - это наследние Перла версии 4 (а goto - так и вообще долбаного Бейсика :)). Если уж Вы хотите лечить фреймворк, лучше не костыли сбоку ставить, а отловить все вызовы exit и переработать код.

А вообще - Apache + mod_perl.
Простите, но с этим я согласиться не могу. Причём - по всем пунктам сразу. :)

  1. Цепочки return-ов - это ужасно. Это означает, что каждая функция должна возвращать смесь результата и ошибки - примерно как это делает весь POSIX с его кошмарным кодом возврата -1 и errno. Чтобы этого избежать, как раз и придумали исключения.
  2. goto (он же setjmp/longjmp) - это вполне адекватная вещь, когда используется по назначению. А когда goto используется вместо управления циклами (next/last/redo), выхода из процудур (return), симуляции исключений (die) - вот тогда он неуместен. Например, в perl есть goto на процедуру, который может использоваться для оптимизации tail recursion.
  3. mod_perl. Во-первых он не предназначен для ускорения CGI, он разрабатывался для ковыряния внутренностей апача на perl, в чём ему равных нет. Во-вторых на хостинге mod_perl это одна большая дыра в безопасности. Ну и кроме того у FastCGI есть много других плюсов, по сравнению с mod_perl - если интересно, могу рассказать.
1. Замечательно. Обойдитесь без цепочек return'ов, но предусмотрите во фреймворке хорошие средства генерации исключений. exit - это самое плохое средство. Сделайте класс, который будет отвечать за хранение состояния фреймворка. Посмотрите, в конце концов, как это делает какой-нибудь Catalyst.

2. Согласен, только goto &NAME и можно использовать. Однако разработчики Perl 6 планируют отделить этот синтаксис в отдельную функцию - и правильно, потому что семантика классического goto и goto &NAME
совсем разная.

3. mod_perl предназначен для создания persistent environment плюс тонкого управления веб-сервером - обе задачи как раз и направлены в первую очередь на ускорение веб-приложения. Если речь идёт про виртуальный хостинг, то да, mod_perl тут не подойдёт, но делать высоконагруженный проект на виртуальном хостинге - это, простите, жлобство :), тут уж постарайтесь как-то обеспечить отдельный сервер для проекта, что ли...

Плюсы FastCGI? Расскажите, пожалуйста! Потому что - честное слово! - за полгода работы с ним в Perl (да ещё и в связке с nginx, тьфу!) я натерпелся многого. Будет приятно узнать, что есть что-то хорошее в этой технологии. Но только применительно к Perl, плиз!
  • FastCGI чисто архитектурно значительно лучше альтернатив.
  • В отличие от mod_perl, FastCGI сервер может работать с разными веб-серверами (apache, nginx, lighttpd, etc.).
  • FastCGI лучше масштабируется, т.к. позволяет разнести веб-сервер и CGIшки по разным машинам.
  • По сравнению с mod_perl, FastCGI приложения гораздо удобнее контролировать (запускать, рестартовать, отлаживать, управлять кол-вом одновременно работающих процессов, etc.), и все эти операции не требуют перенастройки и перезапуска веб-сервера.
  • Можно перезапустить веб-сервер не перезапуская CGIшки (удобно при активном использовании кеширования).
  • При использовании апача, запуск FastCGI-приложения от обычного пользовательского аккаунта (не nobody) гораздо проще и безопаснее, чем использование SuExec или CgiWrap.
    Я не совсем понял, что Вы имели в виду под "только применительно к Perl". Все эти особенности FastCGI к Perl применимы так же, как и к другим языкам. Ничего Perl-специфичного в технологии/протоколе FastCGI нет.

    Если это не под NDA, расскажите, пожалуйста, чего именно Вы натерпелись при использовании Perl под FastCGI, и какие библиотеки при этом использовали.
Из перечисленных преимуществ соглашусь только с пунктом про масштабируемость, ну честное слово.

1. Не согласен. И доводов в пользу моей позиции по этому пункту ровно столько, сколько по Вашей :).

2. А оно Вам надо? Единственный качественный веб-сервер - это Apache. nginx - это малыш для отдачи статики, прикручивать к нему сбоку динамику - это извращение. lighttpd - это вообще, насколько я знаю, реврайт Апача.

3. Согласен. Хотя при должном умении можно и веб-приложения на Апаче масштабировать очень изящно.

4. Перезапуск веб-сервера - это такая тяжёлая операция? Ну так выкиньте свой веб-сервер! "Голенький" Апач рестартует моментально.

5. А зачем перезапускать веб-сервер, обслуживающий динамическую часть веб-сайта, без перезапуска самой динамической части? Куда правильнее в этом случае иметь отдельный веб-сервер для статики и отдельный - для динамики. Пресловутая связка nginx + Apache / mod_perl для этого подходит.

6. Не уверен, поскольку не исследовал. Но вопрос безопасности в данном случае - это не столько забота программиста, сколько системного администратора. Веб-сервер нужно стартовать с нужными правами, вот и всё.

Проблема же FastCGI применимо к Перлу в том, что набор специальных модулей очень скуден. Модуль FCGI не состоит в CPAN, а потому толком не поддерживается коммьюнити перловиков, а написан он сыро (что подтверждает и версия модуля). Аналога шикарному модулю Apache::Request в случае с FastCGI нет - есть только уродливый и тяжеловесный CGI.pm и частично совместимые с ним клоны, а CGI::Fast, который упрощает интеграцию CGI.pm и FCGI, даже и клоны эти не поддерживает. Менеджеры процессов для FastCGI-приложений только сейчас начинают появляются на CPAN, буквально полгода назад был только FCGI::ProcManager, болеющий многими болезнями (лень вспоминать все затыки), а самому плодить процессы и управлять ими - это слишком редкое удовольствие, чтобы я мог его оценить. Отладка связки nginx + FastCGI, например, когда-то отняла у меня уйму времени, т.к. nginx в своих логах молчал об ошибках, а FastCGI процессы даже не успевали стартануть. Сама идея отлавливать все сообщения на STDERR, а именно туда пишет свои "телеги" FCGI, - это уже полное отрицание ООП.

Короче говоря, с Apache + mod_perl я просто сажусь и пишу хэндлеры, для меня разработан огромный набор быстрых и хорошо отлаженных модулей, которые покрывают все аспекты тонкой настройки процесса исполнения серьёзного веб-приложения, в то время как FastCGI как задумка, быть может, и хорош, но совершенно не предоставляет инструментов для разработчика. Если Вам не жаль своего времени (а в том числе ради времени разработки многие приходят к Перлу), FastCGI - это Ваш путь (самурая) ;).
Ну что тут сказать... Доводы в пользу архитектуры - это как раз те самые пункты, которые я перечислил, плюс достаточно простой и элегантный протокол - а с Вашей стороны доводов по архитектуре я пока не видел. Ваши ответы из серии "а оно вам надо", "это забота сисадмина" - это странные ответы. Во-первых я и есть сисадмин, и это, действительно, моя забота. Во-вторых, Вы попросили рассказать преимущества FastCGI - я рассказал. Надо ли оно Вам - это другой вопрос, но факт наличия этих преимуществ отрицать бессмысленно. И что касается отсутствия FCGI на CPAN - это шутка?

Что касается пути самурая - честно говоря, качество большинства модулей на CPAN настолько низкое, что путь самурая в этих условиях - пользоваться этими модулями, вместо того чтобы сделать простое решение заточенное под Вашу задачу. Да и отлаживать баги в своём коде значительно проще, чем в цепочке CPAN-модулей.

Что касается Apache::Request и тяжёлого CGI.pm. Да, в принципе я согласен. Но, а Вам не всё равно, насколько тяжёл CGI.pm, в условиях FastCGI? Параметры он возвращает? Аплоады контролирует? Что ещё от него нужно? Не html же через него выводить... :)

Лично для меня, основной довод в пользу именно FastCGI, это безопасность: CGIшки запускаются от пользовательского аккаунта, внутри веб-сервера они не выполняются, никаких сложных механизмов обеспечения безопасности типа SuExec и CgiWrap не используется. Второй довод - поддержка разных веб-серверов. Мы давно хотели отказаться от апача, и использование FastCGI облегчает попытку попробовать другой веб-сервер.

Кстати, на CPAN появился модуль FCGI::Spawn, который использует FCGI как раз для обеспечения безопасности: он не держит в памяти конкретную CGI, а, вместо этого, при каждом запросе подгружает и запускает CGI, указанную параметром $ENV{SCRIPT_FILENAME}. Как там написано: "hot ready for hosting providing".
Простите, не хочу продолжать этот спор. Я только ещё раз подчеркну, что перечисленные Вами пункты - это не преимущества, а сомнительные утверждения, в то время как факт наличия Вашего видения их как преимуществ я отрицать и не собирался :).

CPAN - замечательная коллекция отличных и не очень - так бывает всегда - модулей. А программисты делятся на тех, кто привык пользоваться готовым кодом, экономя время, и тех, кто всегда всё пишет с нуля. Что ж, Вы из второй категории, очевидно. Убеждать ни в чём не хочу, просто я на собственном примере убедился, что испольщовать уже готовое - это куда эффективнее, и в который раз убеждаюсь в этом на нынешней работе.

FCGI::Spawn - это и есть модуль, про который я написал "только сейчас начинают появляться".

Про FCGI на CPAN удивили, честное слово. Попробуйте найти его по запросу FCGI на search.cpan.org - не найдёте. Уж не знаю, в чём хитрость...

И - да, мне не всё равно, насколько тяжёл модуль. В persistent environment'е я всегда указываю даже полный список импортируемых имён, чтобы избежать потерь памяти.

Сможете рассказать мне, как в два притопа написать веб-приложение под FastCGI? Про mod_perl я смогу рассказать Вам.

Для меня главное - одновременно и эффективность кода, и время его разработки.

use strict;
use CGI::Fast;
REQ:
while ($query=CGI::Fast->new)
{
&main_func()
}

sub main_func
{
Собственно программа.
}
1. Как узнать, оборвался ли коннект?
2. Как писать сообщения об ошибках в собственный лог?

А вот mod_perl:

package ApacheTestHandler;
sub handler {
# собственно программа
}
1;
3. Погодите-ка, а менеджер процессов как же? Как Вы запустите несколько FastCGI-процессов? Веб-сервер nginx, в котором своего менеджера процессов FastCGI нет, в отличие от mod_fastcgi для Апача, кстати.
FastCGI программа не должна заканчиваться - она должна начать обрабатывать следующий запрос, иначе оно начинает работать как plain CGI.
YES!!! Чувство жо.. в общем, интуиция меня не подвела и на сей раз. :) Вот он, этот баг с goto:

perl -e '
BEGIN {
  *CORE::GLOBAL::exit = sub {
    goto FASTCGI_NEXT_REQUEST;
  };
}
while (1) {
  eval { that_cgi_script() };
 FASTCGI_NEXT_REQUEST:
  last;
}

sub that_cgi_script {
  local $SIG{__DIE__} = sub { print "<p>error: $_[0]"; exit; print "XXX\n" };
  print "before buggy code\n";
  eval { buggy_code() };
  print "after buggy code\n";
}
sub buggy_code {
  die "error!";
  print "after die\n";
}
'

Этот пример выводит:

before buggy code
<p>error: error! at -e line 20.
after buggy code

т.е. goto не срабатывает как надо, и выполнение продолжается после eval. Фактически exit в обработчике $SIG{__DIE__} срабатывает как return - ведь "print XXX" не срабатывает.

Я нашел как этот баг обойти. Нужно в блоке BEGIN добавить no-op обработчик CORE::GLOBAL::die (который просто симулирует работу системного):

  *CORE::GLOBAL::die = sub {
    if ($SIG{__DIE__}) {
      my $s = $_[0];
      $s .= sprintf " at %s line %d.\n", (caller)[1,2] if $s !~ /\n\z/;
      $SIG{__DIE__}->($s);
    }
  };

и теперь этот тест работает корректно, и выводит:

before buggy code
<p>error: error! at -e line 27.

Впрочем, объяснить почему добавление такого обработчика заставляет goto работать правильно я пока не готов. :( Возможно, дело в том, что мой обрабочик всё-таки не до конца симулирует стандартный обработчик перла. Правда, на множестве разнообразных тестов, которые я прогнал - я разницы между ними не заметил, если не считать вышеописанную.
Самый правильный способ завершить текущий запрос - метод FCGI Finish().

Но это этого нужно иметь возможность обращаться напрямую к обьекту FCGI, что при использовании более высокоуровневых интерфейсов (того же CGI::Fast) может быть затруднено. В этом случае можно для выхода посылать самому себе сигнал. После обработки сигнала происходит выход из цикла accept, обратно можно возвращаться через тот же goto. Выглядит это примерно так:

$SIG{USR1} = sub { $LAST_SIGNAL = shift; ... };

FASTCGI_NEXT_REQUEST:
while (my $q = new CGI::Fast) { ... }

if ($LAST_SIG eq 'USR1') { goto FASTCGI_NEXT_REQUEST}
Это немного не в тему, но вопрос немного в другом - не в том, как прервать accept, а в том, как перехватить exit (вызываемый в коде обрабатывающем запрос) и вернуться в этот цикл.
Вообще вместо переопределения exit можно обернуть цикл accept в функцию, обьявить обработчик END и вызывать эту функцию из него.
Просто END { goto &loop } у меня отрабатывало только 2 запроса (т.е. после первого запроса END срабатывал, после остальных - нет. Интересная особенность, кстати. Нормально заработало вот так:

sub loop {
eval "END { goto &loop }";
while (accept) { .... exit ... }
}

loop();

END { goto &loop; }

Единственное - при таком раскладе выйти по last в accept не получится, но это уже решается элементарно.

И еще - при использовании любого из описанных способов (включая этот) нужно очень внимательно следить за памятью, очень вероятны утечки.
Да, идея с END красивая... но не менее стрёмная, чем с goto. Дело в том, что, насколько я знаю, блоки END начинают выполняться когда perl уже входит в состояние завершения работы, освобождает ресурсы, etc. И тут ему говорят, погоди, поработай ещё, а мы тебе ещё один блок END подсунем. :)

К сожалению, я не знаю в чём конкретно заключается это "завершение работы и освобождение ресурсов", может он к моменту вызова блока END ещё ничего такого сделать не успевает.

Что касается утечек - они 100% должны быть, ведь после обработки каждого запроса в память добавляется ещё один блок END, при этом старые блоки из памяти не освобождаются. Другое дело, что много он памяти врядли занимает, и рестарт процесса раз в несколько тысяч запросов эту утечку памяти решит.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации