70 votes

Poursuivre l'exécution de PHP après l'envoi de la réponse HTTP

Comment puis-je faire en sorte que PHP 5.2 (exécuté en tant que apache mod_php) envoie une réponse HTTP complète au client, puis continue à exécuter des opérations pendant une minute supplémentaire ?

La longue histoire :

J'ai un script PHP qui doit exécuter quelques longues requêtes de base de données et envoyer des e-mails, ce qui prend 45 à 60 secondes pour s'exécuter. Ce script est appelé par une application sur laquelle je n'ai aucun contrôle. J'ai besoin que l'application signale tout message d'erreur reçu du script PHP (principalement des erreurs de paramètres invalides).

L'application a un délai d'attente inférieur à 45 secondes (je ne connais pas la valeur exacte) et enregistre donc chaque exécution du script de PHP comme une erreur. Par conséquent, j'ai besoin que PHP envoie la réponse HTTP complète au client aussi rapidement que possible (idéalement, dès que les paramètres d'entrée ont été validés), puis qu'il exécute le traitement de la base de données et des e-mails.

J'utilise mod_php, donc pcntl_fork n'est pas disponible. Je pourrais contourner ce problème en sauvegardant les données à traiter dans la base de données et en exécutant le processus réel à partir de l'application cron mais je cherche une solution plus courte.

5 votes

Désolé, mais cela ressemble à une mauvaise utilisation du langage PHP.

3 votes

Ce n'est pas tant une mauvaise utilisation du langage PHP qu'une mauvaise utilisation d'un processus de serveur web. Si aucun HTTP / web n'est impliqué, aucun serveur web ne devrait s'en occuper.

83 votes

Abus du système ou non, nous devons parfois faire des choses que nous n'aimons pas en raison d'exigences qui échappent à notre contrôle. Cela ne rend pas la question invalide, mais la situation est malheureuse.

52voto

povilasp Points 1600

J'avais ce snippet dans ma boîte à outils "special scripts", mais il s'est perdu (les nuages n'étaient pas courants à l'époque), alors je l'ai cherché et j'ai trouvé cette question, surpris de voir qu'il est manquant, j'ai cherché davantage et je suis revenu ici pour le poster :

<?php
 ob_end_clean();
 header("Connection: close");
 ignore_user_abort(); // optional
 ob_start();
 echo ('Text the user will see');
 $size = ob_get_length();
 header("Content-Length: $size");
 ob_end_flush(); // Strange behaviour, will not work
 flush();            // Unless both are called !
 session_write_close(); // Added a line suggested in the comment
 // Do processing here 
 sleep(30);
 echo('Text user will never see');
?>

Je l'utilise en fait dans quelques endroits. Et c'est tout à fait logique : un lien bancaire renvoie la demande d'un paiement réussi et je dois appeler de nombreux services et traiter de nombreuses données lorsque cela se produit. Cela prend parfois plus de 10 secondes, mais le lien bancaire a un délai d'attente fixe. Je reconnais donc le lien bancaire et lui montre la sortie, et je fais mon travail quand il est déjà parti.

2 votes

Je conseille d'ajouter session_write_close(); après flush(); si vous utilisez des sessions, sinon vous ne pourrez pas utiliser votre site (dans le même onglet du navigateur) jusqu'à la fin du traitement (en arrière-plan).

1 votes

Approche intéressante, mais malheureusement elle ne fonctionne pas si vous êtes derrière varnish ou, vraisemblablement, d'autres proxies.

3 votes

Cela ne fonctionne pas sur php5 et le navigateur chrome sur linux, chrome attend 30 secondes avant de terminer la connexion

30voto

Alex Howansky Points 16820

Demandez au script qui traite la demande initiale de créer une entrée dans une file d'attente de traitement, puis de revenir immédiatement. Ensuite, créez un processus séparé (via cron peut-être) qui exécute régulièrement tous les travaux en attente dans la file d'attente.

2 votes

C'est la solution que j'avais en tête à l'origine. D'un autre côté, la mise en place d'une file de traitement dans le seul but de contourner un délai d'attente dans une application tierce me met un peu mal à l'aise.

2 votes

Cette solution souffre d'un manque de parallélisme... ou alors il faudra démarrer un pool de processeurs ouvriers pour servir la file d'attente. J'ai fini par poster puis déconnecter les requêtes http vers self-localhost (d'une manière décrite par SomeGuy ici) pour utiliser un pool de workers httpd existants comme processeurs de fond.

8voto

Yanick Rochon Points 18537

Ce dont vous avez besoin, c'est de ce genre de configuration

alt text

0 votes

Umm, mais, d'après ce diagramme, le message d'état est renvoyé au client uniquement lorsque le cron s'exécute - 5-10 minutes maximum. Quoi qu'il en soit, beau diagramme !

0 votes

Les messages d'état peuvent être demandés à tout moment :) le but est de montrer qu'il y a deux processus distincts et indépendants en cours ici. Mais sinon, merci !

2 votes

+1 Wow, super diagramme ! Mais au lieu que l'utilisateur demande le statut continuellement, je pense que les websockets sont mieux.

8voto

SomeGuy Points 21

On peut utiliser "http fork" pour soi-même ou tout autre script. Je veux dire quelque chose comme ça :

// parent sript, called by user request from browser

// create socket for calling child script
$socketToChild = fsockopen("localhost", 80);

// HTTP-packet building; header first
$msgToChild = "POST /sript.php?&param=value&<more params> HTTP/1.0\n";
$msgToChild .= "Host: localhost\n";
$postData = "Any data for child as POST-query";
$msgToChild .= "Content-Length: ".strlen($postData)."\n\n";

// header done, glue with data
$msgToChild .= $postData;

// send packet no oneself www-server - new process will be created to handle our query
fwrite($socketToChild, $msgToChild);

// wait and read answer from child
$data = fread($socketToChild, $dataSize);

// close connection to child
fclose($socketToChild);
...

Maintenant, l'enfant script :

// parse HTTP-query somewhere and somehow before this point

// "disable partial output" or 
// "enable buffering" to give out all at once later
ob_start();

// "say hello" to client (parent script in this case) disconnection
// before child ends - we need not care about it
ignore_user_abort(1);

// we will work forever
set_time_limit(0);

// we need to say something to parent to stop its waiting
// it could be something useful like client ID or just "OK"
...
echo $reply;

// push buffer to parent
ob_flush();

// parent gets our answer and disconnects
// but we can work "in background" :)
...

L'idée principale est :

  • parent script appelé par la demande de l'utilisateur ;
  • Le parent appelle le script de l'enfant (le même que le parent ou un autre) sur le même serveur (ou tout autre serveur) et lui donne les données de la requête ;
  • le parent dit ok à l'utilisateur et termine ;
  • l'enfant travaille.

Si vous avez besoin d'interagir avec l'enfant - vous pouvez utiliser DB comme "moyen de communication" : le parent peut lire le statut de l'enfant et écrire des commandes, l'enfant peut lire des commandes et écrire le statut. Si vous avez besoin de cela pour plusieurs scripts d'enfant - vous devriez garder l'id de l'enfant du côté utilisateur pour les discriminer et envoyer cet id au parent chaque fois que vous voulez vérifier le statut de l'enfant respectif.

Je l'ai trouvé ici - http://linuxportal.ru/forums/index.php/t/22951/

1 votes

Cette approche (légèrement modifiée) est la seule solution efficace que j'ai trouvée pour créer une tâche d'arrière-plan à partir du mod_php d'Apache. sans au lieu de démarrer un processus d'OS séparé, ceci occupera et utilisera l'un des travailleurs httpd déjà existants à la place.

0 votes

Dans le fichier script parent fread($socketToChild, $dataSize) où est-ce que $dataSize proviennent-ils ? Avez-vous besoin de connaître exactement la quantité de données à attendre du socket (y compris la taille des en-têtes) ? Quelque chose doit m'échapper.

5voto

SorcyCat Points 917

Que diriez-vous d'appeler un script sur le serveur de fichiers pour l'exécuter comme s'il avait été déclenché à la ligne de commande ? Vous pouvez le faire avec la fonction exec .

0 votes

+1, quelque chose comme Gearman est déjà configuré pour cela (mais d'autres / ses propres solutions sont bien sûr tout aussi valables).

1 votes

Exec() est souvent un problème dans les espaces partagés/hébergés. C'est aussi un énorme risque pour la sécurité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X