Асинхронные задачи в 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

Решения и их последствия.

Разрабатывать продукт в недра которого может влезть “чужой” непросто. Мы придумываем всяческие “неймспейсы”, давая файлам, директориям, переменным и классам псевдо-уникальные префиксы, пытаясь предотвратить коллизии с чужеродной средой.

Однажды, много лет назад, ко мне свалилась задача – размещение некой сложной формы на сторонних ресурсах. Очевидное и годами отработанное решение – iframe было забраковано, т.к. опыт и пятая точкашестое чувство подсказывали – как только владелец сайта разместит форму у себя, его светлую голову немедленно посетит мысль – “а как мне приделать перламутровые пуговицы?!”. Это, признаться, пугало – целыми днями верстать и раскрашивать незнакомым дядям и тётям одну и ту же страницу пятью миллионами способов? Нет, пристрелите меня семеро! Второй способ – разработать и задокументировать API, чтобы владелец ресурса сам разрабатывал себе форму, был отвергнут как трудозатратный, но совершенно нежизнеспособный так как рядовой клиент как правило (тут могло бы быть нечто оскорбительное про уровень интеллекта и радиус кривизны передних конечностей, но цензура не пропустит) не готов своими силами что-то создавать, тогда как продукт конкурента был проще в освоении (работал из коробки). Был выбран третий путь – размещение формы в виде html-разметки, которую мы отдаём заказчику, что называется “в руки”, а данные подгружаем через <script src="http://my-resource.example.com/?parameters"></script>, это позволяло владельцу ресурса до определённой степени контролировать внешний вид полученного документа и избавляло нас от трудозатратной кастомизации сотен инсталляций продукта. Но этот способ потенциально создавал много проблем, т.к. находясь в инородной среде наш документ мог “подхватить” незапланированные свойства, “заразится” чужими переменными и вообще рассыпаться в пыль в руках неуклюжего веб-мастера. Необходимо было тщательно изолировать всё и вся. Одним из способов отделить своё “добро” внутри документа было создание уникального “пространства имён”, например создание атрибутов в виде <div rpz:property="value"></div> – это самое rpz: давало надежду на то, что внешний скрипт(движок сайта или сам вебмастер) “случайно” не создаст аналогичный атрибут с другим value (или вовсе без него), развалив всю эту шаткую конструкцию. Решение оказалось рабочим во всех доступных графических браузерах и популяризировалось мной не только в этом продукте, но и повсеместно в других (печатая эти строки пытаюсь понять – зачем вообще нужны были именно атрибуты и почему их нельзя было заменить на переменные в скрипте, но разумного ответа почему-то не нахожу).

Это неоднозначное решение “выстрелило в ногу” только на днях – появился IE9… Невероятно быстр, ангельски красив, дьявольски умён – блеск, а не браузер На вид такой же унылый как все предыдущие В общем обзор новшеств можно наверное найти на сайте разработчиков, ну а я увидел его первый раз два дня назад, ничего хорошего про него сказать не могу, да и речь совсем не о том. Случись такое совпадение – сайт заказчика оказался настолько весь из себя валиден, что IE9 работал со страницей в своём новом document.documentMode. И чтобы вы думали? Точно! Годами отработанная “технология” дала сбой – “самодельные” атрибуты просто-напросто не видны на такой странице, а моя твёрдая уверенность в том, что они есть запретила здравому смыслу проверять их наличие… так и случаются epic fail-ы, так было и со мной.

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

К слову сказать, не так давно вместо атрибута rpz:property="value", я начал использовать “документированную возможность” – атрибут data-rpz-property="value" и соответствующий вызов в js изменился с jQuery(selector).attr("rpz:property") на jQuery(selector).data("rpz-property") , видимо “шестое чувство” о чём-то подозревало…

Amarok. Следующая остановка – eof?

В последнее время практически не слушаю музыку из локальной коллекции по причине описанной ранее. Но иногда запускаю и Amarok. И вот этот самый amarok начал чудить – останавливает проигрывание после каждого файла. Перерыл все настройки на предмет управление playback-ом, по многочисленным советам с разных убунтофорумов попробовал удалять все конфиги, перерыл багтрекер – все найденные баги были пофиксены несколько версий назад, всё безрезультатно, надежды нет и конец близок. Хоть бери да используй другой плеер…

Решение оказалось неожиданным – потребовалось сменить backend у phonon (кто бы мог подумать?!) с mplayer на xine, чтобы плеер перестал проказничать.

“Такой день.” (ц)

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

PhpStorm

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

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

P.S.

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

28.02.11  |  , ,  | 7 comments

Communication problem with “kded”

Экспериментируя с Plasma сильно-сильно уронил KDE. Роковое стечение обстоятельств – как раз перед этим обновил систему (в т.ч. и kde) и не успел рестартнуть сессию. Итого: утомительная многочасовая битва с мельницами, почти полностью утерянные настройки и эта чудо{вищная} заметка.

Симптомы были следующие: kde не стартует под пользователем, но прекрасно запускаются под root-ом.

startkde: Starting up...
Connecting to deprecated signal QDBusConnectionInterface::serviceOwnerChanged(QString,QString,QString)
kded(4946): Communication problem with  "kded" , it probably crashed.
Error message was:  "org.freedesktop.DBus.Error.ServiceUnknown" : " "The name org.kde.kded was not provided by any .service files" " 

(4944)/ KStartupInfo::createNewStartupId: creating:  "${HOSTNAME};1283501102;250139;4944_TIME0" : "unnamed app"
startkde: Shutting down...
klauncher: Exiting on signal 1
startkde: Running shutdown scripts...
startkde: Done.

Незамедлительно был сделан вывод: что-то “пришло” в обновлённых пакетах, как назло недоступен slackware.com (нет, ну вы подумайте – всё вот так вот разом, а?). Был перелопачен /etc на предмет разного рода изменений в правах доступа. Затем в /dev/null один за одним полетели файлы из $HOME, /tmp и т.п. Удалил весь $HOME, создал девственно чистый – ничего. Начинала свербить мысль – а может остаться под root-ом…. ?!

Решение как всегда оказалось простым и ожиданным. Позабыл удалить /var/tmp/kdecache-$USER. Уже не первый раз сталкиваюсь и не последний раз забываю начать именно с этого.

P.S.
Вот на кой ляд половина хлама падает в /tmp, а другая в /var/tmp ?

05.12.10  |  ,  | 2 comments

Эта музыка будет вечной.

Год назад я нелестно отзывался о социальных сетях и рекламировал доступный способ слушать музыку на last.fm. С тех пор кое-что в жизни изменилось, но музыку я по-прежнему слушаю, постоянно хочется чего-нибудь “новенького”.

Даже тогда, когда я являлся подписчиком last.fm прослушать можно было далеко не все композиции – либо их нет совсем, либо имеются только тридцатисекундные демо-версии. Обидно-досадно, ну да ладно. В какой-то момент подписка закончилась, и я собравшись было продлить её совершенно случайно наткнулся на (more…)

Добавляем действия в контектное меню KDE

Контекстное меню KDEМногие операции вроде “замаунтить флешку”, “скопировать файл” и т.п. ежедневно-рутинные действия я не задумываясь совершаю в консоли (благодаря yakuake она всегда под рукой). Но перед людьми бывает “неудобно” – они видя все эти “магические” манипуляции ещё больше укрепляются в мысли, что “эти ваши линуксы” не для “наших широт”.

Для того чтобы немного размазать негативные впечатления добавил некоторые действия в контекстное меню “проводников”.

Первое что надо сделать: понять ГДЕ необходимо приложить руки.

$ kde4-config --path services
/home/miracle/.kde/share/kde4/services/:/usr/share/kde4/services/

В одной из этих директорий создаём файл my-super-actions.desktop подобного содержания:

[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=video/*;
Actions=CompressMovie4Nokia;CompressMovie4HTC;GetSubtitles;
Encoding=UTF-8

[Desktop Action CompressMovie4Nokia]
Name=Compress for Nokia
Icon=phone
Exec=/bin/sh -c 'cd "`dirname "%f"`" \
&& ffmpeg -y -i "`basename "%f"`" -ac 1 -ar 22050 -vcodec mpeg4 -s 176x144 -r 24 \
-b 118k -ab 32k -aspect 11:9 "nokia-`basename "%f" .avi`.mp4" \
&& kdialog --title "Compress Movie" --passivepopup "Movie `basename "%f"` compressed"'

[Desktop Action CompressMovie4HTC]
Name=Compress for HTC
Icon=pda
Exec=/bin/sh -c 'cd "`dirname "%f"`" \
&& ffmpeg -y -i "`basename "%f"`" -s 320x240 -r 22.5 -ac 2 "htc-`basename "%f" .avi`.avi" \
&& kdialog --title "Compress Movie" --passivepopup "Movie `basename "%f"` compressed"'

[Desktop Action GetSubtitles]
Name=Download subtitles
Icon=draw-text
Exec=/bin/sh -c 'cd "`dirname "%f"`" \
&& subtitles `basename "%f"` -l en \
&& kdialog --title "Subtitles" --passivepopup "Subtitles for movie `basename "%f"` downloaded"'

Затем выполняем:

$ kbuildsycoca4

И наблюдаем свежедобавленные пункты в меню “Actions”.

В коде всё наглядно, отдельного упоминания наверное стоит только тот факт, что /bin/sh нужен лишь для того, чтобы запустить более одной команды, как в моём случае – если команда одна, то запуск шелла будет лишним.

Ссылка по теме: Desktop Entry Specification

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

Performance Optimization WordPress Plugins by W3 EDGE