Как я могу запустить PHP-скрипт в фоновом режиме после отправки формы?

105

Проблема
У меня есть форма, которая при отправке запускает базовый код для обработки отправленной информации и вставки ее в базу данных для отображения на веб-сайте уведомлений. Кроме того, у меня есть список людей, которые подписались на получение этих уведомлений по электронной почте и SMS. На данный момент этот список тривиален (всего около 150), однако его достаточно, чтобы циклически перебрать всю таблицу подписчиков и отправить более 150 электронных писем. (Электронные письма отправляются индивидуально по запросу системных администраторов нашего почтового сервера из-за политики массовой рассылки.)

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

  1. Во-первых, автор может подумать, что сервер отстает, и снова нажать кнопку «Отправить», в результате чего скрипт запустится заново или будет запущен дважды. Я мог бы решить эту проблему, используя JavaScript, чтобы отключить кнопку и заменить текст, чтобы сказать что-то вроде «Обработка ...», однако это далеко не идеально, потому что пользователь все равно будет застрять на странице на время выполнения скрипта. (Кроме того, если JavaScript отключен, эта проблема все еще существует.)

  2. Во-вторых, автор может преждевременно закрыть вкладку или браузер после отправки формы. Сценарий будет продолжать работать на сервере до тех пор, пока он не попытается выполнить обратную запись в браузер, однако, если пользователь затем перейдет на любую страницу в нашем домене (пока скрипт все еще запущен), браузер зависнет при загрузке страницы, пока скрипт не завершится. . (Это происходит только тогда, когда закрывается вкладка или окно браузера, а не все приложение браузера.) Тем не менее, это далеко не идеально.

(Возможное) Решение.
Я решил, что хочу выделить часть сценария «электронная почта» в отдельный файл, который я могу вызвать после публикации уведомления. Первоначально я думал разместить это на странице подтверждения после того, как уведомление было успешно отправлено. Однако пользователь не узнает, что этот сценарий запущен, и ему не будут очевидны какие-либо аномалии; Этот сценарий не может потерпеть неудачу.

Но что, если я могу запустить этот сценарий в фоновом режиме? Итак, мой вопрос таков: как я могу выполнить скрипт PHP для запуска в качестве фоновой службы и полностью независимо от того, что пользователь сделал на уровне формы?

РЕДАКТИРОВАТЬ: это нельзя cron'ed. Он должен запускаться в момент отправки формы. Это уведомления с высоким приоритетом. Кроме того, системные администраторы, работающие на наших серверах, запрещают запускать crons чаще, чем 5 минут.

Майкл Иригойен
источник
Запустите задание cron и предупредите пользователя на другой странице о том, что его запрос обрабатывается, или что-то в этом роде.
Эван Мулавски
1
Вы хотите запускать его регулярно (каждый день или каждый час) или при отправке? Думаю, вы могли бы использовать php, exec()но я никогда этого не пробовал.
Саймон
2
Я просто использую ajaxи вставляю sleep()с интервалом времени внутри цикла (в моем случае я использую foreach) для запуска фонового процесса. Он отлично работает на моем сервере. Не уверен насчет других. Я также добавляю ignore_user_abort()и set_time_limit()(с расчетным временем окончания) перед циклом, чтобы убедиться, что скрипт не будет остановлен сервером, который я использую, даже в соответствии с моим тестом, скрипт завершает задачу без ignore_user_abort()и set_time_limit().
Ари
Я видел, что вы уже нашли решение. Я использую gearmanphp для решения фоновых задач. Он идеально подходит для масштабирования, если вы столкнулись с большой обработкой и большими нагрузками. Вам стоит взглянуть на это.
Андре Гарсия
Следуйте этому ответу на [stackoverflow] ( stackoverflow.com/a/46617107/6417678 )
Джоши Фрэнсис

Ответы:

89

Делая некоторые эксперименты с execи shell_execя обнаружил решение , которое работало отлично! Я предпочитаю использовать, shell_execчтобы я мог регистрировать каждый процесс уведомления, который происходит (или не происходит). ( shell_execвозвращается в виде строки, и это было проще, чем использовать exec, назначать вывод переменной и затем открывать файл для записи.)

Я использую следующую строку для вызова сценария электронной почты:

shell_exec("/path/to/php /path/to/send_notifications.php '".$post_id."' 'alert' >> /path/to/alert_log/paging.log &");

Важно замечать &в конце команды (как отметил @netcoder). Эта команда UNIX запускает процесс в фоновом режиме.

Дополнительные переменные, заключенные в одинарные кавычки после пути к сценарию, задаются как $_SERVER['argv']переменные, которые я могу вызывать в моем сценарии.

Затем сценарий электронной почты выводит в мой файл журнала с помощью >>и выводит что-то вроде этого:

[2011-01-07 11:01:26] Alert Notifications Sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 38.71 seconds)
[2011-01-07 11:01:34] CRITICAL ERROR: Alert Notifications NOT sent for http://alerts.illinoisstate.edu/2049 (SCRIPT: 23.12 seconds)
Майкл Иригойен
источник
Спасибо. Это спасло мне жизнь.
Лиам Бейли
Что это $post_idпосле пути к php-скрипту?
Perocat
@Perocat - это переменная, которую вы отправляете в скрипт. В этом случае он отправляет идентификатор, который будет параметром вызываемого скрипта.
Chique
Я сомневаюсь , что это действительно работает отлично, см stackoverflow.com/questions/2212635/...
symcbean
1
Есть отложенное предложение редактирования, чтобы избежать $post_id; Я предпочел бы бросить его прямо на номер: (int) $post_id. (Взывая к вашему вниманию, чтобы решить, что лучше.)
Ал.Г.
19

На серверах Linux / Unix вы можете выполнять задание в фоновом режиме с помощью proc_open :

$descriptorspec = array(
   array('pipe', 'r'),               // stdin
   array('file', 'myfile.txt', 'a'), // stdout
   array('pipe', 'w'),               // stderr
);

$proc = proc_open('php email_script.php &', $descriptorspec, $pipes);

&Является важным здесь немного. Скрипт будет продолжен, даже если исходный скрипт закончился.

сетевой кодер
источник
Это работает, когда я не звоню, proc_closeкогда задача выполнена. proc_closeзаставляет скрипт зависать от 15 до 25 секунд. Мне нужно будет вести журнал, используя информацию о канале, поэтому мне нужно открыть файл, в который нужно записать, закрыть его, а затем закрыть соединение proc. Так что, к сожалению, у меня это решение не работает.
Майкл Иригойен
3
Вы, конечно, не звоните proc_close, в том-то и дело! И да, вы можете подключиться к файлу с расширением proc_open. См. Обновленный ответ.
netcoder 07
@netcoder: что, если я не хочу сохранять результат ни в каком файле. какие изменения требуются в stdout?
naggarwal11
9

Из всех ответов ни один не рассматривал до смешного простую функцию fastcgi_finish_request , которая при вызове сбрасывает весь оставшийся вывод в браузер и закрывает сессию Fastcgi и HTTP-соединение, позволяя скрипту работать в фоновом режиме.

Пример:

<?php
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
fastcgi_finish_request(); // The user is now disconnected from the script

// do stuff with received data,
Даногентили
источник
это именно то, что я искал, в shell_exec мне нужно каким-то образом передать опубликованные данные в исполняемый скрипт, что само по себе является трудоемкой задачей в моем случае, а также очень усложняет ситуацию.
Аурангзеб
7

PHP exec("php script.php")может это сделать.

Из руководства :

Если программа запускается с этой функцией, для продолжения работы в фоновом режиме вывод программы должен быть перенаправлен в файл или другой поток вывода. В противном случае PHP зависнет до завершения выполнения программы.

Поэтому, если вы перенаправляете вывод в файл журнала (что в любом случае является хорошей идеей), ваш вызывающий скрипт не зависнет, а ваш почтовый скрипт будет работать в bg.

Саймон
источник
1
Спасибо, но это не сработало. Скрипт по-прежнему "зависает", пока обрабатывает второй файл.
Майкл Иригойен
Посмотрите на commend php.net/manual/en/function.exec.php#80582 Похоже, это потому, что Safe_mode включен. В unix есть обходной путь.
Саймон
К сожалению, в нашей установке не включен безопасный режим. Странно, что он все еще висит.
Майкл Иригойен
6

А почему бы не сделать HTTP-запрос к скрипту и не проигнорировать ответ?

http://php.net/manual/en/function.httprequest-send.php

Если вы сделаете запрос в скрипте, который вам нужно вызвать, ваш веб-сервер запустит его в фоновом режиме, и вы можете (в своем основном скрипте) показать сообщение, сообщающее пользователю, что скрипт запущен.

Твистер
источник
4

Как насчет этого?

  1. Ваш PHP-скрипт, содержащий форму, сохраняет флаг или какое-либо значение в базе данных или файле.
  2. Второй сценарий PHP периодически опрашивает это значение и, если он установлен, запускает сценарий электронной почты синхронным образом.

Этот второй скрипт PHP должен быть настроен на запуск как cron.

Адаршр
источник
Спасибо, но я обновил исходный вопрос. Кроны в этом случае не подходят.
Майкл Иригойен
4

Насколько я знаю, вы не можете сделать это простым способом (см. Fork exec и т. Д. (Не работают под окнами)), возможно, вы можете отменить подход, использовать фон браузера, отправляющего форму в ajax, поэтому, если сообщение все еще работа у тебя нет времени ждать.
Это может помочь, даже если вам придется долго дорабатывать.

При отправке почты всегда предлагается использовать спулер, может быть локальный и быстрый сервер smtp, который принимает ваши запросы и передает их в реальный MTA или помещает все в БД, чем использовать cron, который буферизует очередь.
Cron может находиться на другом компьютере, вызывая диспетчер очереди печати как внешний URL:

* * * * * wget -O /dev/null http://www.example.com/spooler.php
Иван Буттинони
источник
3

Фоновая работа cron - хорошая идея для этого.

Вам понадобится ssh-доступ к машине, чтобы запустить скрипт как cron.

$ php scriptname.php запустить его.

Ян
источник
1
нет необходимости в ssh. Многие хосты не поддерживают ssh, но имеют поддержку cron.
Teson
Спасибо, но я обновил исходный вопрос. Кроны в этом случае не подходят.
Майкл Иригойен
3

Если вы можете получить доступ к серверу через ssh и запускать свои собственные сценарии, вы можете создать простой сервер fifo с использованием php (хотя вам придется перекомпилировать php с posixподдержкой fork).

Сервер действительно может быть написан на чем угодно, вы, вероятно, легко сможете сделать это на python.

Или самым простым решением было бы отправить HttpRequest и не читать возвращаемые данные, но сервер может уничтожить скрипт до того, как он завершит обработку.

Пример сервера:

<?php
define('FIFO_PATH', '/home/user/input.queue');
define('FORK_COUNT', 10);

if(file_exists(FIFO_PATH)) {
    die(FIFO_PATH . ' exists, please delete it and try again.' . "\n");
}

if(!file_exists(FIFO_PATH) && !posix_mkfifo(FIFO_PATH, 0666)){
    die('Couldn\'t create the listening fifo.' . "\n");
}

$pids = array();
$fp = fopen(FIFO_PATH, 'r+');
for($i = 0; $i < FORK_COUNT; ++$i) {
    $pids[$i] = pcntl_fork();
    if(!$pids[$i]) {
        echo "process(" . posix_getpid() . ", id=$i)\n";
        while(true) {
            $line = chop(fgets($fp));
            if($line == 'quit' || $line === false) break;
            echo "processing (" . posix_getpid() . ", id=$i) :: $line\n";
        //  $data = json_decode($line);
        //  processData($data);
        }
        exit();
    }
}
fclose($fp);
foreach($pids as $pid){
    pcntl_waitpid($pid, $status);
}
unlink(FIFO_PATH);
?>

Пример клиента:

<?php
define('FIFO_PATH', '/home/user/input.queue');
if(!file_exists(FIFO_PATH)) {
    die(FIFO_PATH . ' doesn\'t exist, please make sure the fifo server is running.' . "\n");
}

function postToQueue($data) {
    $fp = fopen(FIFO_PATH, 'w+');
    stream_set_blocking($fp, false); //don't block
    $data = json_encode($data) . "\n";
    if(fwrite($fp, $data) != strlen($data)) {
        echo "Couldn't the server might be dead or there's a bug somewhere\n";
    }
    fclose($fp);
}
$i = 1000;
while(--$i) {
    postToQueue(array('xx'=>21, 'yy' => array(1,2,3)));
}
?>
OneOfOne
источник
3

Более простой способ запустить PHP-скрипт в фоновом режиме -

php script.php >/dev/null &

Скрипт будет работать в фоновом режиме, и страница также быстрее достигнет страницы действия.

Сандип Дханда
источник
2

Предполагая, что вы работаете на платформе * nix, используйте cronи phpисполняемый файл.

РЕДАКТИРОВАТЬ:

Уже есть ряд вопросов, касающихся «запуска php без cron» на SO. Вот один:

Планирование сценариев без использования CRON

Тем не менее, ответ exec () выше звучит очень многообещающе :)

Колючий Норман
источник
Спасибо, но я обновил исходный вопрос. Кроны в этом случае не подходят.
Майкл Иригойен
2

Если вы используете Windows, поищите proc_openили popen...

Но если мы находимся на одном сервере "Linux" с cpanel, то это правильный подход:

#!/usr/bin/php 
<?php
$pid = shell_exec("nohup nice php -f            
'path/to/your/script.php' /dev/null 2>&1 & echo $!");
While(exec("ps $pid"))
{ //you can also have a streamer here like fprintf,        
 // or fgets
}
?>

Не используйте fork()или curlесли вы сомневаетесь, что справитесь с ними, это похоже на злоупотребление вашим сервером

Наконец, script.phpобратите внимание на файл, который указан выше, убедитесь, что вы написали:

<?php
ignore_user_abort(TRUE);
set_time_limit(0);
ob_start();
// <-- really optional but this is pure php

//Code to be tested on background

ob_flush(); flush(); 
//this two do the output process if you need some.        
//then to make all the logic possible


str_repeat(" ",1500); 
//.for progress bars or loading images

sleep(2); //standard limit

?>
я АрбЗ
источник
Прошу прощения за его последующее редактирование, потому что я использую свой мобильный телефон, и действительно, это намного сложнее, чем писать скрипт exec (). Lol;)
я ArbZ 09
2

для фонового работника я думаю, вам следует попробовать этот метод, он поможет вызвать столько страниц, сколько вам нравится, все страницы будут запускаться одновременно независимо, не дожидаясь ответа каждой страницы как асинхронного.

form_action_page.php

     <?php

    post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
    //post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
    //post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
    //call as many as pages you like all pages will run at once //independently without waiting for each page response as asynchronous.

  //your form db insertion or other code goes here do what ever you want //above code will work as background job this line will direct hit before //above lines response     
                ?>
                <?php

                /*
                 * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
                 *  
                 */
                function post_async($url,$params)
                {

                    $post_string = $params;

                    $parts=parse_url($url);

                    $fp = fsockopen($parts['host'],
                        isset($parts['port'])?$parts['port']:80,
                        $errno, $errstr, 30);

                    $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                    $out.= "Host: ".$parts['host']."\r\n";
                    $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                    $out.= "Content-Length: ".strlen($post_string)."\r\n";
                    $out.= "Connection: Close\r\n\r\n";
                    fwrite($fp, $out);
                    fclose($fp);
                }
                ?>

testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
//here do your background operations it will not halt main page
    ?>

PS: если вы хотите отправить параметры URL-адреса в виде цикла, следуйте этому ответу: https://stackoverflow.com/a/41225209/6295712

Хасан Саид
источник
0

В моем случае у меня есть 3 параметра, один из них - строка (mensaje):

exec("C:\wamp\bin\php\php5.5.12\php.exe C:/test/N/trunk/api/v1/Process.php $idTest2 $idTest3 \"$mensaje\" >> c:/log.log &");

В моем Process.php у меня есть этот код:

if (!isset($argv[1]) || !isset($argv[2]) || !isset($argv[3]))
{   
    die("Error.");
} 

$idCurso = $argv[1];
$idDestino = $argv[2];
$mensaje = $argv[3];
Эрнальдо Гонсалес
источник
0

Это работает для меня. тыр это

exec(“php asyn.php”.” > /dev/null 2>/dev/null &“);
Радж
источник
что, если у меня есть значения постов? из формы, и я отправляю его через ajax и serialize()функцию. например, у меня есть index.php форма, и я отправляю значение через ajax в index2.php. Я хочу выполнить отправку данных в index2.php в фоновом режиме, что вы предлагаете? спасибо
khalid JA
0

Используйте Amphp для параллельного и асинхронного выполнения заданий.

Установить библиотеку

composer require amphp/parallel-functions

Пример кода

<?php

require "vendor/autoload.php";

use Amp\Promise;
use Amp\ParallelFunctions;


echo 'started</br>';

$promises[1] = ParallelFunctions\parallel(function (){
    // Send Email
})();

$promises[2] = ParallelFunctions\parallel(function (){
    // Send SMS
})();


Promise\wait(Promise\all($promises));

echo 'finished';

Для вашего варианта использования вы можете сделать что-то вроде ниже

<?php

use function Amp\ParallelFunctions\parallelMap;
use function Amp\Promise\wait;

$responses = wait(parallelMap([
    'a@example.com',
    'b@example.com',
    'c@example.com',
], function ($to) {
    return send_mail($to);
}));
SkyRar
источник