Не открою Америки, если скажу что порой требуется выполнить некую времязатратную операцию, результат которой либо не нужен пользователю вовсе (запись в лог, удаление временных файлов и другое обслуживание сервера), либо его можно обмануть и сказать что операция выполнена успешно, а саму операцию выполнить “попозже”. Самым наверное близким всем примером такой операции можно назвать отправку почты – 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 ещё и ещё, но пользователя это уже не должно волновать – у вас его письмо сохранилось и вы его доставите, он вам верит!

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

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

8 thoughts on “Асинхронные задачи в PHP

  • 18.04.2011 at 17:03
    Permalink

    Убогий костыль. Таким людям нельзя писать на php. Никогда.

  • 18.04.2011 at 17:46
    Permalink

    Обоснуй, если таким как ты можно писать.

  • 19.04.2011 at 11:59
    Permalink

    Да, да! Глубже! Быстрее! Сильнее!

    Русским же по белому написал – из пушки по воробьям не стреляют! Для отправки почты не поднимают ни gearmand ни rabbitMQ. Ну точнее я думаю что не поднимают, но похоже с адекватностью в этом мире нынче туго и гостевую книгу сейчас снабжают атомным реактором, блекджеком и шлюхами… Одним словом “ынтырпрайз”.

  • 05.05.2011 at 22:21
    Permalink

    можно еще сдобрить fastcgi_finish_request() если такой есть в наличии

  • 07.10.2011 at 12:54
    Permalink

    Автор+, таким нужно писать код!!!

  • 28.02.2013 at 11:45
    Permalink

    Мне нравится это решение! Костыль или не костыль, поиски подобного развивает мозг. А при достаточном количестве ресурсов кролика и шестерню поставить каждый новичок сможет. А что, если у вас ресурсы ограничены и вы не можете добавлять расширения в систему?!
    автору +

Leave a Reply