¿Cómo puedo ejecutar un script PHP en segundo plano después de enviar un formulario?

104

Problema
Tengo un formulario que, cuando se envía, ejecutará un código básico para procesar la información enviada e insertarla en una base de datos para mostrarla en un sitio web de notificaciones. Además, tengo una lista de personas que se han registrado para recibir estas notificaciones por correo electrónico y mensaje SMS. Esta lista es trivial como el momento (solo empuja alrededor de 150), sin embargo, es suficiente para que se tarde más de un minuto en recorrer toda la tabla de suscriptores y enviar más de 150 correos electrónicos. (Los correos electrónicos se envían individualmente según lo solicitado por los administradores del sistema de nuestro servidor de correo electrónico debido a las políticas de correo electrónico masivo).

Durante este tiempo, la persona que publicó la alerta se sentará en la última página del formulario durante casi un minuto sin ningún refuerzo positivo de que se está publicando su notificación. Esto conduce a otros problemas potenciales, todos los que tienen posibles soluciones que creo que no son ideales.

  1. Primero, el cartel puede pensar que el servidor está retrasado y hacer clic en el botón 'Enviar' nuevamente, lo que hace que el script comience de nuevo o se ejecute dos veces. Podría resolver esto usando JavaScript para deshabilitar el botón y reemplazar el texto para decir algo como 'Procesando ...', sin embargo, esto es menos que ideal porque el usuario seguirá atascado en la página durante la ejecución del script. (Además, si JavaScript está deshabilitado, este problema aún existe).

  2. En segundo lugar, es posible que el autor cierre la pestaña o el navegador antes de tiempo después de enviar el formulario. La secuencia de comandos seguirá ejecutándose en el servidor hasta que intente volver a escribir en el navegador, sin embargo, si el usuario navega a cualquier página dentro de nuestro dominio (mientras la secuencia de comandos aún se está ejecutando), el navegador se cuelga cargando la página hasta que finaliza la secuencia de comandos. . (Esto solo sucede cuando se cierra una pestaña o ventana del navegador y no toda la aplicación del navegador). Aún así, esto no es lo ideal.

(Posible) Solución
He decidido que quiero dividir la parte de "correo electrónico" del script en un archivo separado al que puedo llamar después de que se haya publicado la notificación. Originalmente pensé en poner esto en la página de confirmación después de que la notificación se haya publicado correctamente. Sin embargo, el usuario no sabrá que este script se está ejecutando y las anomalías no serán evidentes para él; Este script no puede fallar.

Pero, ¿y si puedo ejecutar este script como proceso en segundo plano? Entonces, mi pregunta es la siguiente: ¿Cómo puedo ejecutar un script PHP para activarlo como un servicio en segundo plano y ejecutarlo de manera completamente independiente de lo que el usuario haya hecho en el nivel del formulario?

EDITAR: Esto no se puede cron'ed. Debe ejecutarse en el instante en que se envía el formulario. Estas son notificaciones de alta prioridad. Además, los administradores del sistema que ejecutan nuestros servidores no permiten que los crons se ejecuten con una frecuencia superior a 5 minutos.

Michael Irigoyen
fuente
Inicie un trabajo cron y avise al usuario en otra página que su solicitud está siendo procesada, o algo así.
Evan Mulawski
1
¿Quiere ejecutarlo con regularidad (todos los días o cada hora) o al enviarlo? Supongo que podrías usar php, exec()pero nunca probé esto.
Simon
2
Simplemente uso ajaxe inserto sleep()con intervalo de tiempo dentro del bucle (uso foreach en mi caso) para ejecutar el proceso en segundo plano. Funciona perfectamente en mi servidor. No estoy seguro de los demás. También estoy agregando ignore_user_abort()y set_time_limit()(con el tiempo de finalización calculado) antes del ciclo para asegurarme de que el servidor que uso no detenga el script, incluso según mi prueba, el script completando la tarea sin ignore_user_abort()y set_time_limit().
Ari
Vi que ya se te ocurrió una solución. Yo uso gearmancon php para lidiar con tareas en segundo plano. Es perfecto para escalar si se encuentra con un gran procesamiento y cargas pesadas. Deberías echarle un vistazo.
Andre García
Siga esta respuesta en [stackoverflow] ( stackoverflow.com/a/46617107/6417678 )
Joshy Francis

Respuestas:

89

¡Experimentando un poco execy shell_exeche descubierto una solución que funcionó perfectamente! Elijo usarlo shell_execpara poder registrar cada proceso de notificación que ocurre (o no). ( shell_execdevuelve como una cadena y esto fue más fácil que usarexec , asignar la salida a una variable y luego abrir un archivo para escribir).

Estoy usando la siguiente línea para invocar el script de correo electrónico:

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

Es importante notar la & al final del comando (como lo señala @netcoder). Este comando de UNIX ejecuta un proceso en segundo plano.

Las variables adicionales entre comillas simples después de la ruta al script se establecen como $_SERVER['argv'] variables que puedo llamar dentro de mi script.

El script de correo electrónico luego se envía a mi archivo de registro usando >>y generará algo como esto:

[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)
Michael Irigoyen
fuente
Gracias. Esto me salvó la vida.
Liam Bailey
¿Qué es eso $post_iddespués de la ruta del script php?
Perocat
@Perocat es una variable que está enviando al script. En este caso, está enviando un ID que será un parámetro para el script que se llama.
Chique
Dudo que esto funcione perfectamente, vea stackoverflow.com/questions/2212635/…
symcbean
1
Hay una sugerencia de edición pendiente para escapar $post_id; Yo prefiero echarlo directamente a un número: (int) $post_id. (Pidiendo su atención para decidir cuál es mejor.)
Al.G.
19

En servidores Linux / Unix, puede ejecutar un trabajo en segundo plano utilizando 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);

El &ser lo importante aquí. El guión continuará incluso si el guión original ha finalizado.

netcoder
fuente
Esto funciona cuando no llamo proc_closecuando la tarea se ha completado. proc_closehace que el guión se cuelgue entre 15 y 25 segundos. Necesitaría mantener un registro usando la información de la tubería, así que tengo que abrir un archivo para escribir, cerrarlo y luego cerrar la conexión proc. Entonces, desafortunadamente, esta solución no me funciona.
Michael Irigoyen
3
Por supuesto que no llamas proc_close, ¡ese es el punto! Y sí, puede canalizar a un archivo con proc_open. Ver respuesta actualizada.
netcoder
@netcoder: ¿qué pasa si no quiero almacenar el resultado en ningún archivo? ¿Qué cambios se requieren en stdout?
naggarwal11
9

De todas las respuestas, ninguna consideró la función ridículamente fácil fastcgi_finish_request , que cuando se llama, descarga toda la salida restante al navegador y cierra la sesión Fastcgi y la conexión HTTP, mientras deja que el script se ejecute en segundo plano.

Un ejemplo:

<?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,
Danogentili
fuente
esto es exactamente lo que estaba buscando, en shell_exec tengo que transferir de alguna manera los datos publicados al script ejecutado, lo cual en mi caso es una tarea que consume mucho tiempo y también complica mucho las cosas.
Aurangzeb
7

PHP exec("php script.php")puede hacerlo.

Del manual :

Si un programa se inicia con esta función, para que continúe ejecutándose en segundo plano, la salida del programa debe redirigirse a un archivo u otro flujo de salida. Si no lo hace, PHP se bloqueará hasta que finalice la ejecución del programa.

Entonces, si redirige la salida a un archivo de registro (lo que es una buena idea de todos modos), su script de llamada no se bloqueará y su script de correo electrónico se ejecutará en bg.

Simón
fuente
1
Gracias, pero esto no funcionó. La secuencia de comandos todavía "se bloquea" mientras procesa el segundo archivo.
Michael Irigoyen
Mire el elogio php.net/manual/en/function.exec.php#80582 Parece que esto se debe a que Safe_mode está habilitado. Hay una solución en Unix.
Simon
Desafortunadamente, el modo seguro no está habilitado en nuestra instalación. Es extraño que todavía esté colgando.
Michael Irigoyen
6

¿Y por qué no hacer una solicitud HTTP en el script e ignorar la respuesta?

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

Si realiza su solicitud en el script, debe llamar a su servidor web, lo ejecutará en segundo plano y puede (en su script principal) mostrar un mensaje que le indica al usuario que el script se está ejecutando.

Tornado
fuente
4

¿Qué tal esto?

  1. Su secuencia de comandos PHP que contiene el formulario guarda una bandera o algún valor en una base de datos o archivo.
  2. Un segundo script PHP sondea este valor periódicamente y, si se ha configurado, activa el script de correo electrónico de forma sincrónica.

Este segundo script PHP debe configurarse para ejecutarse como cron.

Adarshr
fuente
Gracias, pero he actualizado la pregunta original. Los cron no son una opción en este caso.
Michael Irigoyen
4

Como sé que no puede hacer esto de una manera fácil (vea fork exec, etc. (no funciona en Windows)), puede ser que pueda revertir el enfoque, use el fondo del navegador publicando el formulario en ajax, por lo que si la publicación aún trabajo no tienes tiempo de espera.
Esto puede ayudar incluso si tiene que hacer una larga elaboración.

Sobre el envío de correo, siempre se sugiere usar un spooler, puede ser un servidor smtp local y rápido que acepte sus solicitudes y las envíe al MTA real o las coloque todo en una base de datos, en lugar de usar un cron que ponga la cola en cola.
El cron puede estar en otra máquina llamando al spooler como URL externa:

* * * * * wget -O /dev/null http://www.example.com/spooler.php
Ivan Buttinoni
fuente
3

El trabajo cron en segundo plano parece una buena idea para esto.

Necesitará acceso ssh a la máquina para ejecutar el script como cron.

$ php scriptname.php para ejecutarlo.

Ian
fuente
1
sin necesidad de ssh. Muchos hosts no permiten ssh, pero tienen soporte cron.
Teson
Gracias, pero he actualizado la pregunta original. Los cron no son una opción en este caso.
Michael Irigoyen
3

Si puede acceder al servidor a través de ssh y puede ejecutar sus propios scripts, puede crear un servidor FIFO simple usando php (aunque tendrá que volver a compilar php con posixsoporte para fork).

El servidor se puede escribir en cualquier cosa en realidad, probablemente puedas hacerlo fácilmente en Python.

O la solución más simple sería enviar una HttpRequest y no leer los datos devueltos, pero el servidor podría destruir el script antes de que termine de procesarse.

Servidor de ejemplo:

<?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);
?>

Cliente de ejemplo:

<?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)));
}
?>
Uno de uno
fuente
3

La forma más sencilla de ejecutar un script PHP en segundo plano es

php script.php >/dev/null &

El script se ejecutará en segundo plano y la página también llegará más rápido a la página de acción.

Sandeep Dhanda
fuente
2

Suponiendo que está ejecutando en una plataforma * nix, use crony el phpejecutable.

EDITAR:

Hay bastantes preguntas que piden "ejecutar php sin cron" en SO. Aquí hay uno:

Programe scripts sin usar CRON

Dicho esto, la respuesta de exec () anterior suena muy prometedora :)

Normando espinoso
fuente
Gracias, pero he actualizado la pregunta original. Los cron no son una opción en este caso.
Michael Irigoyen
2

Si estás en Windows, investiga proc_openo popen...

Pero si estamos en el mismo servidor "Linux" que ejecuta cpanel, este es el enfoque correcto:

#!/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
}
?>

No los use fork()o curlsi duda que pueda manejarlos, es como abusar de su servidor

Por último, en el script.phparchivo que se llama arriba, tome nota de esto, asegúrese de escribir:

<?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

?>
soy ArbZ
fuente
Perdón por editarlo posteriormente porque estoy usando mi teléfono móvil y, de hecho, es mucho más difícil que escribir un script exec (). Lol;)
soy ArbZ
2

para el trabajador en segundo plano, creo que debería probar esta técnica, ya que ayudará a llamar a tantas páginas como desee, todas las páginas se ejecutarán a la vez de forma independiente sin esperar la respuesta de cada página como asincrónica.

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
    ?>

PD: si desea enviar parámetros de URL como bucle, siga esta respuesta: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
fuente
0

En mi caso tengo 3 params, uno de ellos es string (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 &");

En mi Process.php tengo este código:

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

$idCurso = $argv[1];
$idDestino = $argv[2];
$mensaje = $argv[3];
Hernaldo González
fuente
0

Esto funciona para mí. tyr esto

exec(“php asyn.php”.” > /dev/null 2>/dev/null &“);
Raj
fuente
¿Qué pasa si tengo valores de publicación? desde un formulario y lo envío a través de ajax y serialize()function. por ejemplo, tengo index.php con formulario y envío valor a través de ajax a index2.php Quiero realizar el envío de datos en index2.php en segundo plano, ¿qué sugieres? gracias
khalid JA
0

Utilice Amphp para ejecutar trabajos en paralelo y asincrónicamente.

Instalar la biblioteca

composer require amphp/parallel-functions

Muestra de código

<?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';

Para su caso de uso, puede hacer algo como a continuación

<?php

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

$responses = wait(parallelMap([
    '[email protected]',
    '[email protected]',
    '[email protected]',
], function ($to) {
    return send_mail($to);
}));
SkyRar
fuente