Асинхронные задачи в PHP

Не открою Америки, если скажу что порой требуется выполнить некую времязатратную операцию, результат которой либо не нужен пользователю вовсе (запись в лог, удаление временных файлов и другое обслуживание сервера), либо его можно обмануть и сказать что операция выполнена успешно, а саму операцию выполнить “попозже”. Самым наверное близким всем примером такой операции можно назвать отправку почты – smtp-cессия может длится довольно долго, особенно если письмо огромное, сервер тормозной (ну да вы сами всё знаете, в реальном мире великое множество острых углов), но зачем пользователю ждать результата? Что ему делать если результат неуспешный? “Попробуйте повторить операцию позже” ? Не смешно! Рядовой пользователь на ваш рядовой ресурс не вернётся, дотошный – свяжется с вами другими способами, так что можно с чистой совестью соврать ему – мол всё путём, всё отправлено, всё доставлено, записано и всё так успешно и идеально, а в это время потихоньку начать на самом деле выполнять задачу.

Конечно если вокруг вас крутятся тысячи серверов, слово “ынтырпрайз” для вас звучит буднично, то для вас уже изобретено много-много buzz-word-ных решений с очередями сообщений, очередями задач и другими полезными решениями, но львиная доля разработчиков всё-таки создают сайты-визитки, поддерживают сайты на хостинг-планах “всё по 20 рублей”, да и вообще на мой взгляд глупо для отправки почты окружать простейший php-скрипт кучей софта вроде gearmand + расширения для работы с ним. Я же хочу показать решение “для бедных”, простое, но удобное в разработке, поддержке и отладке решение.

Итак задача – отправить письмо, не заставляя ждать пользователя.
Имеем:

list($recipient,$subject,$body) = get_vars_from_request();
include 'superpupermailer.php';
$mailer = new SuperPuperMailer($recipient,$subject,$body);
if ($mailer->send()) {
echo "Аллилуя! Мы сделали это, храни нас Великий Байт.";
} else {
echo "О нет, это случилось!!! Быть того не может...но всё же случилось - приходи, милый друг, в другой раз, а сейчас ошибка!";
}

Тут всё понятно – либо отправилось, либо не отправилось – всем приходилось видеть это с разных сторон, те кто видел это со стороны браузера нередко наблюдали не только “ошибочка вышла”, но и другие подробности вроде конкретных строк в скриптах, warning-ов и Fatal error-ов. Но мы уже выяснили ранее – во-первых пользователю совершенно по барабану что у вас произошло с сервером, а во-вторых “пробовать ещё раз” он скорее всего не будет. Поэтому код можно изменить следующим образом:

list($recipient,$subject,$body) = get_vars_from_request();
$async_job = 'send();';
if (file_put_contents('/dir/for/jobs/email.php',$async_job)) {
echo "Ура! Мы сделали этот мир лучше!";
} else {
echo "Увы, мир жесток и безжалостен...";
}

Что это и зачем? Где отправка почты? Каким образом письмо отправится? Больше вопросов чем ответов. Опять-таки остался else, шило поменяли на мыло? В целом да – заменили тёплое мягким, но всё-таки нет – как часто у вас заканчивается неудачей запись на диск? Чаще чем таймаут при коннекте к почтовому серверу?

Теперь о главном – зачем файл? Где отправка почты?

Этим займётся другой скрипт:

if (true === include('/dir/for/jobs/email.php')) {
  unlink('/dir/for/jobs/email.php');
}

Осталось вызвать этот скрипт. Например так:

...
echo 'Ура! Мы сделали этот мир лучше!';
...

Или же добавим в cron задание на вызов job.php каждый час/пять минут/каждую минуту.

Всё. Мы не заставили пользователя ждать, мы отправили сообщение, мы молодцы. А если не отправили? Отправим потом – файл-то остался!

Конечно возникает много закономерных возражений – что будет если send.php (или job.php) будет вызван одновременно двумя пользователями?
Эти вопросы надо обстоятельно решать, задачи создавать с уникальными именами, блокировать одновременный запуск скрипта job.php, в общем работы аж на целых десять минут.

Самое интересное в конце:

...
$job_name = uniqid('mail_');
if (file_put_contents('/dir/for/jobs/'.$job_name.'.php',$async_job)) {
   echo "Ура! Мы сделали этот мир лучше!";
              $job_url = '/job.php?job='.$job_name;
              if (false !== ($fh = @fsockopen($_SERVER['SERVER_ADDR'], $_SERVER['SERVER_PORT'],
    $errno, $errstr, 0.01))) {
                  fputs($fh,
                      "GET $job_url HTTP/1.0\r\n"
                          . "Host: {$_SERVER['HTTP_HOST']}\r\n\r\n"
                  );
                  fgets($fh,32);
                  fclose($fh);
              }

} else {
   echo "Увы, мир жесток и безжалостен...";
}

Что здесь происходит? Мы создали php-файл, при выполнении которого отправится письмо, затем подключились к веб-серверу и запросили скрипт job.php, передав ему параметром имя только что созданного файла. И тут же отключились – ведь нам не важен результат, мы уже солгали пользователю о том, что операция завершилась успешно. На всё-про всё потратили доли секунды. Дальше уже дело техники – job.php захватит lock-файл, проверить наличие файла, имя которого ему передали, выполнит его, удалит в случае успеха, а затем отпустит lock-файл. Конечно надо не забывать, что скрипт может по каким-то причинам не выполнится (почтовый сервер недоступен, или ответит ошибкой, да и мало ли какие напасти происходят в реальности), поэтому следует вызывать job.php ещё и ещё, но пользователя это уже не должно волновать – у вас его письмо сохранилось и вы его доставите, он вам верит!

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

16.04.11  |   | 7 comments

PhpStorm

С недавних пор начал плотно использовать PhpStorm на работе – с появлением в системнике “лишней” памяти она (IDE) стала ну просто космически быстрой, дьявольски умной и невероятно удобной. Одно тяготило меня – не нашёл возможности увидеть вывод отлаживаемого скрипта. Особенно яростно это давит в момент отладки веб-сервисов. И вот сегодня утром IDE предложила написать о себе отзыв. И я не отказал ей в тёплом слове и заодно спросил – где же, чёрт возьми, output?!

Был приятно удивлён скорой реакцией на запрос – приветливый support попытался мне помочь, а затем мы выяснили, что данный функционал ещё в пути и пока не готов. В связи с чем хочу выразить благодарность читателю за то, что он зайдёт на трекер к разработчикам и проголосует за эти фичи: #WI-4323 и #WI-4466 и отдельные “спасибы” раздать Сергею Баранову и Николаю Матвееву за скорую и адекватную помощь и снисхождение к русскоязычной аудитории пользователей.

P.S.

Если вы понятия не имеете о чём идёт речь, но разрабатываете на PHP, вы просто обязаны попробовать PhpStorm в деле – скачать eap-релиз можно на сайте разработчиков.

28.02.11  |  , ,  | 7 comments

commit-jabber


После прочтения статьи о том, как в last.fm используют irc для логирования всего и вся тоже захотелось как-нибудь “выпендриться”.
Мониторить сервера нам ни к чему, да и irc – поди объясни сейчас что это такое и чем оно лучше _______. Но недавно выдалась свободная минутка и я нашёл куда приложить усилия.
Решил сделать post-commit хук в svn-репозитарии, который будет высылать детали о коммите, но не на почту, как это делается в традиционных скриптах, а в jabber (cам jabber достался нам вместе с почтой от гугла) .
По-моему получилось очень удобно и за несколько дней превратилось из игрушки в удобный инструмент для своевременного обновления и обнаружения “ну и зачем ты это сделал” :)

Собственно все внутренности состоят из библиотеки XMPPHP и маленького скриптика, который вешается на post-commit.
Сам скриптик настолько маленький и бесхитростный, что комментировать его не вижу смысла – кладу как есть.
Для функционирования нужно иметь xmpphp в include_path, бинарник svn в PATH и добавить post-commit hook в ваш репозитарий.

В сложнейшем, виндовом случае это будет post-commit.cmd, который лежит в директории hooks репозитария и имеет следующее содержание:

/path/to/php.exe /path/to/svnjabber.php %1 %2

ты не один

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

В рамках работ по выводу одного “неповоротливого асфальтоукладчика” на земную орбиту были проведены ряд его модернизаций. Испытания на тестовом полигоне показали что машина стала ездить на порядок быстрее и было решено доработки включить в работающий прототип(вроде и не первая космическая, но уже есть чем гордиться!). Но на первом же серьёзном показе зверь-машина с чудо{вищным} грохотом рассыпалась у всех на глазах и я сел в лужу…

А теперь тоже самое, но по-русски, с выражением.
Имеются ряд весьма тяжёлых для веба запросов к БД, результат которых условно не меняется в течении небольшого промежутка времени. Кеширование промежуточных результатов в самой БД нагрузку сняло на СУБД, но скрипты продолжали исправно тягать данные и это порой занимало внушительное время. Было решено результаты эти кешировать на стороне веб-сервера. Как хранилище был использован apc.

Первоначальный вариант выглядел примерно так:

if (!$data = cache_get($key)) {
   $data = data_from_db();
   cache_set($key,$data,$expire_time)
}

Без излишеств, просто и со вкусом. Но позже выснилось что “всё не так просто” и в зависимости от некоторых космических характеристик и результатов выборки из базы будет меняется ещё одна переменная (меняется переменная… ого!).

if (!$data = cache_get($key)) {
   $data = data_from_db();
   cache_set($key,$data,$expire_time);
   $other_data = some_function($data,$env);
   cache_set($other_key,$other_data,$expire_time);
} else {
   $other_data = cache_get($other_key);
}

Аляповато, но, чёрт возьми, работает…
Работало…
Должно было по идее работать…
Но, почему-то оказавшись на боевом, изрядно нагруженном, сервере это не сработало. Вернее это работало…, но не всё время… По истечении $expire_time периодически пропадали данные для $other_key. При отключении и/или очистке кеша работоспособность восстанавливалась… Кто виноват? Что делать? Кто за всё это ответит?!? Пришлось срочно пошевелить опилками.

Как мы все наверное догадываемся – реальный сервер обслуживает нереально много клиентов, причём не по очереди. И нет совершенно никаких гарантий на то, что между записью $key и $other_key не встрянет какой-либо маленький, но шустрый процесс (хотя нам это и не особо страшно), так же нет никаких оснований полагать что между выборкой $key и $other_key кто-то умный и большой не очистит кеш (целиком, или только $other_key – в данном эпизоде это не имеет значения).
Оснований не было, но я был почему-то твёрдо уверен, что “это если и происходит, то с кем-то другим”. С пониманием проблемы конечно же пришло и простое решение, но ошибка, согласитесь, совсем не тривиальная.

Так что мораль сей басни: помни – ты не один!

23.09.08  |   | стань первым

firebug для отладки серверного кода


За последние пару лет firebug стал стандартным инструментом для отладки клиентской части веб-приложений у большинства разработчиков.
Тем, у кого это не так – от души сочувствую ;o)
Но многие не догадываются, что с помощью firebug можно отлаживать и серверный код. Не breakpoint-ами конечно. Но как замена унылым print-ам и var_dump-ам очень даже!

Собственно делается всё очень просто. Для того чтобы в консоли firebug-а появилось сообщение, необходимо написать js-код:

console.log('сообщение');
console.group('разные типы сообщений');
console.info('к вашему сведению, мы тут логи пишем...');
console.warn('предупреждаем о разном');
console.error('и информируем об ошибках!!!');
console.groupEnd();
console.dir({сложная: {структура: [1,2,3]}});

Подробнее о возможностях консоли firebug-а можно узнать в документации.

Дело за малым – сформировать эти строки на вашей серверной платформе.
Сложность представляет лишь последнее – json-упаковка сложных структур, но и она уже во многих платформах решена. Сам я пользуюсь функцией, описанной в коментариях к json_encode, т.к. родная слишком консервативна в вопросах кодировок(по стандарту в json можно упаковать только уникод, но браузеры более лояльны нежели писатели стандартов).

Собственно теперь чем это лучше всяких принтов и вар_дампов?
Тем, что не меняют визуально документ, что существенно в современных сложных веб-приложениях.
К тому же это в разы удобнее – все сообщения в одном месте, красиво представлены…

В заключение скажу о якобы готовых решениях для PHP:
Log::Firebug из PEAR – драйвер для довольно распространнённого pear::log
FirePHP – более комплексное решение, плагин к firebug-у и обёртка к серверному коду.

Я ни тем, ни другим не пользуюсь, т.к. первое сделано левой ногой, а второе заворачивает оригинальный документ в страшную multipart-кашу…

06.05.08  |  ,  | 2 comments

чуть ниже нуля

Вот так вот:

<?php
  var_dump(round(-0.1));
?>

29.02.08  |   | 7 comments

Y2K38

Ожидаемый коллапс электронного пространства в y2k так и не состоялся, а следующий намечен на далёкий 2038-ой год.
Но такой ли он далёкий? Буквально сегодня пришлось столкнуться лицом к лицу с нависшей над миром угрозой.
В разрабатываемой системе все манипуляции с датами проводились через преобразование в unix timestamp и последующим его форматированием штатными средствами php (strtotime, mktime, strftime, date) и вся эта хрупкая конструкция разлетелась, когда вдруг возникла дата 01.01.2059. Нет, это не сумашедшие бета-тестеры и не опечатка – это оказался срок годности пива действия паспорта. Казалось выхода нет и человеку надо будет менять паспорт, но решение всё же нашлось: ADOdb Date Time Library – старенькая библиотечка на чистом php, которая позволит вам жить счастливо даже после официального окончания unix-эпохи.
Какие ещё беды настрадал нам Предсказамус?

Update: во какую штуку нашёл, надо бы донести это знание до тех, кому не безразлично…

27.02.08  |   | 2 comments

Performance Optimization WordPress Plugins by W3 EDGE