continue processando php após enviar resposta http

101

Meu script é chamado pelo servidor. Do servidor receberei ID_OF_MESSAGEe TEXT_OF_MESSAGE.

Em meu script, tratarei do texto recebido e gerarei uma resposta com os parâmetros: ANSWER_TO_IDe RESPONSE_MESSAGE.

O problema é que estou enviando resposta a incomming "ID_OF_MESSAGE", mas o servidor que me envia mensagem para tratar irá definir sua mensagem como entregue a mim (significa que posso enviar a resposta para aquele ID), após receber a resposta http 200.

Uma das soluções é salvar a mensagem no banco de dados e fazer algum cron que será executado a cada minuto, mas preciso gerar uma mensagem de resposta imediatamente.

Existe alguma solução como enviar ao servidor a resposta 200 http e continuar executando o script php?

Muito obrigado

user1700214
fonte

Respostas:

191

Sim. Você consegue fazer isso:

ignore_user_abort(true);
set_time_limit(0);

ob_start();
// do initial processing here
echo $response; // send the response
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();

// now the request is sent to the browser, but the script is still running
// so, you can continue...
vcampitelli
fonte
5
É possível fazer isso com uma conexão keep-alive?
Congelli501
3
Excelente!! Esta é a única resposta a esta pergunta que realmente funciona !!! 10p +
Martin_Lakes
3
há uma razão para você usar ob_flush após ob_end_flush? Eu entendo a necessidade da função flush lá no final, mas não tenho certeza por que você precisaria de ob_flush com ob_end_flush sendo chamado.
ars265 de
9
Observe que se um cabeçalho de codificação de conteúdo for definido como qualquer outra coisa que 'nenhum', ele pode tornar este exemplo inútil, pois ainda deixaria o usuário aguardar o tempo de execução completo (até o tempo limite?). Portanto, para ter certeza absoluta de que funcionará localmente e no ambiente de produção, defina o cabeçalho 'content-encoding' como 'none':header("Content-Encoding: none")
Brian
17
Dica: comecei a usar PHP-FPM, então tive que adicionar fastcgi_finish_request()no final
vcampitelli
44

Tenho visto muitas respostas aqui que sugerem o uso, ignore_user_abort(true);mas este código não é necessário. Tudo isso garante que o script continue em execução antes do envio de uma resposta, caso o usuário aborte (fechando o navegador ou pressionando escape para interromper a solicitação). Mas não é isso que você está perguntando. Você está pedindo para continuar a execução APÓS o envio de uma resposta. Tudo que você precisa é o seguinte:

    // Buffer all upcoming output...
    ob_start();

    // Send your response.
    echo "Here be response";

    // Get the size of the output.
    $size = ob_get_length();

    // Disable compression (in case content length is compressed).
    header("Content-Encoding: none");

    // Set the content length of the response.
    header("Content-Length: {$size}");

    // Close the connection.
    header("Connection: close");

    // Flush all output.
    ob_end_flush();
    ob_flush();
    flush();

    // Close current session (if it exists).
    if(session_id()) session_write_close();

    // Start your background work here.
    ...

Se você está preocupado com o fato de seu trabalho em segundo plano demorar mais do que o limite de tempo de execução de script padrão do PHP, continue set_time_limit(0);no topo.

Kosta Kontos
fonte
3
Tentei MUITAS combinações diferentes, ESTE é o que funciona !!! Obrigado Kosta Kontos !!!
Martin_Lakes
1
Funciona perfeitamente no apache 2, php 7.0.32 e ubuntu 16.04! Obrigado!
KyleBunga
1
Tentei outras soluções, e apenas esta funcionou para mim. A ordem das linhas também é importante.
Sinisa
32

Se estiver usando o processamento FastCGI ou PHP-FPM, você pode:

session_write_close(); //close the session
ignore_user_abort(true); //Prevent echo, print, and flush from killing the script
fastcgi_finish_request(); //this returns 200 to the user, and processing continues

// do desired processing ...
$expensiveCalulation = 1+1;
error_log($expensiveCalculation);

Fonte: https://www.php.net/manual/en/function.fastcgi-finish-request.php

Problema de PHP nº 68722: https://bugs.php.net/bug.php?id=68772

DarkNeuron
fonte
2
Obrigado por isso, depois de passar algumas horas, isso funcionou para mim no nginx
Ehsan
7
esse cálculo de soma caro tho: o muito impressionado, tão caro!
Friedrich Roell
Obrigado DarkNeuron! Ótima resposta para nós usando php-fpm, acabei de resolver meu problema!
Sinisa
21

Gastei algumas horas neste problema e vim com esta função que funciona no Apache e Nginx:

/**
 * respondOK.
 */
protected function respondOK()
{
    // check if fastcgi_finish_request is callable
    if (is_callable('fastcgi_finish_request')) {
        /*
         * This works in Nginx but the next approach not
         */
        session_write_close();
        fastcgi_finish_request();

        return;
    }

    ignore_user_abort(true);

    ob_start();
    $serverProtocole = filter_input(INPUT_SERVER, 'SERVER_PROTOCOL', FILTER_SANITIZE_STRING);
    header($serverProtocole.' 200 OK');
    header('Content-Encoding: none');
    header('Content-Length: '.ob_get_length());
    header('Connection: close');

    ob_end_flush();
    ob_flush();
    flush();
}

Você pode chamar esta função antes de seu longo processamento.

Ehsan
fonte
2
Isso é adorável! Depois de tentar tudo o mais acima, isso é a única coisa que funcionou com o nginx.
Spice
Seu código é quase exatamente igual ao código aqui, mas sua postagem é mais antiga :) +1
Contador م
tenha cuidado com a filter_inputfunção, ela retorna NULL às vezes. veja esta contribuição do usuário para detalhes
Contador م
4

Modificou um pouco a resposta de @vcampitelli. Não pense que você precisa do closecabeçalho. Eu estava vendo cabeçalhos fechados duplicados no Chrome.

<?php

ignore_user_abort(true);

ob_start();
echo '{}';
header($_SERVER["SERVER_PROTOCOL"] . " 202 Accepted");
header("Status: 202 Accepted");
header("Content-Type: application/json");
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();

sleep(10);
Justin
fonte
3
Mencionei isso na resposta original, mas direi aqui também. Você não precisa necessariamente fechar a conexão, mas então o que acontecerá é que o próximo ativo solicitado na mesma conexão será forçado a esperar. Portanto, você pode entregar o HTML rapidamente, mas um de seus arquivos JS ou CSS pode carregar lentamente, pois a conexão deve terminar de obter a resposta do PHP antes de obter o próximo ativo. Por isso, fechar a conexão é uma boa ideia para que o navegador não tenha que esperar que ela seja liberada.
Nate Lampton,
3

Eu uso a função php register_shutdown_function para isso.

void register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] )

http://php.net/manual/en/function.register-shutdown-function.php

Edit : O acima não está funcionando. Parece que fui enganado por alguma documentação antiga. O comportamento de register_shutdown_function mudou desde o link do link do PHP 4.1

martti
fonte
Não é isso que está sendo solicitado - essa função basicamente apenas estende o evento de encerramento do script e ainda faz parte do buffer de saída.
fisk
1
Achei que vale a pena um upvote, pois mostra o que não funciona.
Trendfischer
1
Idem - voto positivo porque esperava ver isso como uma resposta, e útil ver que não funciona.
HappyDog
2

Não consigo instalar o pthread e nem as soluções anteriores funcionam para mim. Descobri que apenas a seguinte solução funcionava (ref: https://stackoverflow.com/a/14469376/1315873 ):

<?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');
?>
Fil
fonte
1

no caso de uso de php file_get_contents, o fechamento da conexão não é suficiente. php ainda espera por eof witch enviado pelo servidor.

minha solução é ler 'Comprimento do conteúdo:'

aqui está um exemplo:

response.php:

 <?php

ignore_user_abort(true);
set_time_limit(500);

ob_start();
echo 'ok'."\n";
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();
sleep(30);

Observe o "\ n" em resposta à linha fechada, se não o fget lido enquanto espera eof.

read.php:

<?php
$vars = array(
    'hello' => 'world'
);
$content = http_build_query($vars);

fwrite($fp, "POST /response.php HTTP/1.1\r\n");
fwrite($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
fwrite($fp, "Content-Length: " . strlen($content) . "\r\n");
fwrite($fp, "Connection: close\r\n");
fwrite($fp, "\r\n");

fwrite($fp, $content);

$iSize = null;
$bHeaderEnd = false;
$sResponse = '';
do {
    $sTmp = fgets($fp, 1024);
    $iPos = strpos($sTmp, 'Content-Length: ');
    if ($iPos !== false) {
        $iSize = (int) substr($sTmp, strlen('Content-Length: '));
    }
    if ($bHeaderEnd) {
        $sResponse.= $sTmp;
    }
    if (strlen(trim($sTmp)) == 0) {
        $bHeaderEnd = true;
    }
} while (!feof($fp) && (is_null($iSize) || !is_null($iSize) && strlen($sResponse) < $iSize));
$result = trim($sResponse);

Como você pode ver, este script deve esperar um pouco se o comprimento do conteúdo for atingido.

espero que ajude

Jérome S.
fonte
1

Eu fiz esta pergunta a Rasmus Lerdorf em abril de 2012, citando estes artigos:

Eu sugeri o desenvolvimento de uma nova função embutida do PHP para notificar a plataforma que nenhuma saída adicional (no stdout?) Será gerada (tal função pode cuidar do fechamento da conexão). Rasmus Lerdorf respondeu:

Veja Gearman . Você realmente não quer que seus servidores da Web de front-end façam processamento de back-end assim.

Eu posso ver seu ponto de vista e apoiar sua opinião para algumas aplicações / cenários de carregamento! No entanto, em alguns outros cenários, as soluções de vcampitelli et al, são boas.

Matthew Slyman
fonte
1

Eu tenho algo que pode compactar e enviar a resposta e deixar outro código php ser executado.

function sendResponse($response){
    $contentencoding = 'none';
    if(ob_get_contents()){
        ob_end_clean();
        if(ob_get_contents()){
            ob_clean();
        }
    }
    header('Connection: close');
    header("cache-control: must-revalidate");
    header('Vary: Accept-Encoding');
    header('content-type: application/json; charset=utf-8');
    ob_start();
    if(phpversion()>='4.0.4pl1' && extension_loaded('zlib') && GZIP_ENABLED==1 && !empty($_SERVER["HTTP_ACCEPT_ENCODING"]) && (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], 'gzip') !== false) && (strstr($GLOBALS['useragent'],'compatible') || strstr($GLOBALS['useragent'],'Gecko'))){
        $contentencoding = 'gzip';
        ob_start('ob_gzhandler');
    }
    header('Content-Encoding: '.$contentencoding);
    if (!empty($_GET['callback'])){
        echo $_GET['callback'].'('.$response.')';
    } else {
        echo $response;
    }
    if($contentencoding == 'gzip') {
        if(ob_get_contents()){
            ob_end_flush(); // Flush the output from ob_gzhandler
        }
    }
    header('Content-Length: '.ob_get_length());
    // flush all output
    if (ob_get_contents()){
        ob_end_flush(); // Flush the outer ob_start()
        if(ob_get_contents()){
            ob_flush();
        }
        flush();
    }
    if (session_id()) session_write_close();
}
Mayur S
fonte
0

Existe outra abordagem e vale a pena considerar se você não deseja alterar os cabeçalhos de resposta. Se você iniciar um thread em outro processo, a função chamada não aguardará sua resposta e retornará ao navegador com um código http finalizado. Você precisará configurar o pthread .

class continue_processing_thread extends Thread 
{
     public function __construct($param1) 
     {
         $this->param1 = $param1
     }

     public function run() 
     {
        //Do your long running process here
     }
}

//This is your function called via an HTTP GET/POST etc
function rest_endpoint()
{
  //do whatever stuff needed by the response.

  //Create and start your thread. 
  //rest_endpoint wont wait for this to complete.
  $continue_processing = new continue_processing_thread($some_value);
  $continue_processing->start();

  echo json_encode($response)
}

Assim que executarmos $ continue_processing-> start (), o PHP não irá esperar pelo resultado de retorno desta thread e, portanto, até onde rest_endpoint é considerado. Está feito.

Alguns links para ajudar com pthreads

Boa sorte.

Jonathan
fonte
0

Eu sei que é um antigo, mas possivelmente útil neste momento.

Com essa resposta, não apoio a questão real, mas como resolver esse problema corretamente. Espero que ajude outras pessoas a resolver problemas como este.

Eu sugeriria usar RabbitMQ ou serviços semelhantes e executar a carga de trabalho em segundo plano usando instâncias de trabalho . Existe um pacote chamado amqplib para php que faz todo o trabalho para você usar o RabbitMQ.

Profissionais:

  1. É de alto desempenho
  2. Bem estruturado e sustentável
  3. É totalmente escalonável com instâncias de trabalho

Neg's:

  1. O RabbitMQ deve ser instalado no servidor, isso pode ser um problema com algum web hoster.
Samuel Breu
fonte