Alex Efros @powerman
Software Architect, Team Lead, Lead Go Developer
Information
- Rating
- 1,905-th
- Location
- Харьков, Харьковская обл., Украина
- Date of birth
- Registered
- Activity
Specialization
Backend Developer, Software Architect
Lead
From 10,000 $
Designing application architecture
Golang
Linux
Docker
Network security
Modular testing
Mentoring
Development of tech specifications
Software development
High-loaded systems
Я вот программист. И меня максимизация прибыли не интересует. Меня интересует код. Меня интересует разработка нужных, "живых" систем, которыми пользуются, которые решают задачи пользователя, и которые не раздражают пользователя глюками. И от софта, которым я сам пользуюсь, я жду того же: чтобы он решал мои задачи и не глючил.
Для достижения этого эффекта переодически приходится писать с нуля библиотеки, а не использовать готовые (просто потому, что после взгляда в код этих "готовых" начинает тошнить, и ты понимаешь, что код написанный на базе этих библиотек надёжно работать не будет никогда в принципе). Переодически приходится изобретать велосипеды, заточенные под конкретные задачи, вместо использования готовых универсальных решений. Изредка попадается готовое, и при этом достаточно качественное решение - тогда радость моя просто неописуема, т.к. можно немного полениться, поюзать готовое, и получить результат быстрее. Чаще бывает иначе - просто нет никакой возможности переписать всё что нужно, и приходится вынужденно пользоваться тем, что есть.
P.S. Существует мания, когда всё подряд переписывается без веской причины - это имеет смысл только в рамках обучения. Но есть и другая ситуация, когда ты реально можешь написать библиотеку качественнее готовой, причём патчить готовую сложнее написания своей (из-за архитектурных проблем, например), причём в текущем проекте требуется уровен качества выше того, которые предоставляет готовая библиотека - тогда написание своей имеет смысл, хотя и часто не делается из-за экономических причин (повышение качества на X и увеличение времени разработки на Y не оправдывает увеличения расходов на Z). Но этот экономический расчёт зачастую верен только в краткосрочной перспективе, а в долгосрочной это переписывание всё-равно бы окупилось.
Во-первых, как я написал выше, "overquality не нужно нашим заказчикам", по крайней мере абсолютному большинству (бывают и исключения - мой заказчик платит мне уже больше 5-ти лет как раз за то, чтобы я писал насколько качественно, насколько считаю необходимым - но он, и до того как меня нашёл, и после этого, успел поработать с другими программистами, и, в результате, остался со мной - вероятно потому, что мой код таки работает так, как ему нужно).
Во-вторых, лично я не просто готов платить больше за качественный софт, вопрос стоит иначе: я не готов платить ни копейки за некачественный софт. Что касается цен M$, то, как известно, они зарабатывают ГОРАЗДО больше, чем тратят на разработку. Так что, я уверен, они имеют возможность писать гораздо более качественно, сохраняя прежний уровень цен. Просто им это не нужно.
P.S. А по поводу изобретательства велосипедов, я уже когда-то цитировал эту фразу на хабре, но я повторюсь, уж больно хорошо сказано автором JSON:
Я думаю, что этот совет можно применять так, чтобы это не усложняло поддержку кода. Во-первых, можно привыкнуть писать/читать более простой, хоть и не такой краткий код. Тогда удобно будет читать именно его. :) Во-вторых, краткость здесь нужна ради удержания всего проекта в голове, а не ради того, чтобы сложную функцию "запинать" в 3 строки. Поэтому если при нормальной записи кода проект в голове перестаёт умещаться, нужно не код более кратко записывать, а проект дробить на изолированные кусочки, которые не нужно одновременно удерживать в голове.
Иными словами, задачи организаций, и задачи описанных в статье программистов, очень сильно расходятся!
А авторы вышестоящих комментариев, к сожалению, этого не понимают. И поэтому пытаются спорить. Для организаций описанные в статье "способы решения этой проблемы" категорически не подходят (и автор статьи как раз это отлично понимает). Не походят не потому, что проблемы с въезжанием в код у них нет - она есть - а потому, что эта проблема для организации совсем не так важна, как многие другие проблемы - например, проблема взаимозаменяемости людей, проблема сдачи проекта в срок, проблема обеспечения необходимого уровня продаж, проблема минимизации расходов на поддержку проекта, etc. И следование описанным в статье рекомендациям просто не выгодно, ведь задача большинства организаций - "заработать денег", а не "написать максимально качественный код любой ценой".
А вот для описанного в статье программиста, задача стоит "сделать его идеально". И, довольно часто, в определение "идеальности", у программиста не входят такие вещи, как сдача проекта в срок, или облегчение дальнейшей поддержки проекта. Чаще эта "идеальность" относится к внутренней реализации, к элегантности и эффективности кода. А для написания такого кода, и требуется "удерживать его целиком в голове"! Вот для этой ситуации всё описанное в статье абсолютно верно.
Определитесь, в чём Ваша цель, и Вам станет понятно, подходят Вам эти рекомендации, или нет. :)
Теперь Датское королевство переписано надлежащим образом, без перехватов die и вызовов exit. Всё стало очень культурно и аккуратно. И проблема с goto отпала сама собой - сейчас фреймвок сам генерит исключение вместо exit. И никаких вложенных eval-ов в тех местах, где может быть редирект у меня ни в одном проекте нет... и если когда-нить появятся, значит после этого eval необработанные исключения будут передаваться наверх, как и положено.
В общем, ощущения у нас были правильные - изменил архитектуру и проблема исчезла.
К сожалению, я не знаю в чём конкретно заключается это "завершение работы и освобождение ресурсов", может он к моменту вызова блока END ещё ничего такого сделать не успевает.
Что касается утечек - они 100% должны быть, ведь после обработки каждого запроса в память добавляется ещё один блок END, при этом старые блоки из памяти не освобождаются. Другое дело, что много он памяти врядли занимает, и рестарт процесса раз в несколько тысяч запросов эту утечку памяти решит.
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 работать правильно я пока не готов. :( Возможно, дело в том, что мой обрабочик всё-таки не до конца симулирует стандартный обработчик перла. Правда, на множестве разнообразных тестов, которые я прогнал - я разницы между ними не заметил, если не считать вышеописанную.
Что касается пути самурая - честно говоря, качество большинства модулей на 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".
Я не совсем понял, что Вы имели в виду под "только применительно к Perl". Все эти особенности FastCGI к Perl применимы так же, как и к другим языкам. Ничего Perl-специфичного в технологии/протоколе FastCGI нет.
Если это не под NDA, расскажите, пожалуйста, чего именно Вы натерпелись при использовании Perl под FastCGI, и какие библиотеки при этом использовали.
Проблема с этим подходом только одна: на поиск каждого такого бага, обычно в чужом коде, будет уходить куча времени. :(
Приходит запрос в некий 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).
Интересно, чем по сути этот next отличается от того же goto?