Outro dia eu precisei implementar um
script que, ao receber alguns dados enviados por POST pelos usuários do
site, faz uma requisição http a uma api externa usando parte desses
dados postados. Como as informações que essa api retorna não precisam
ser exibidas para o usuário, e como essa api geralmente leva cerca de 2
segundos pra responder, para não deixar o usuário “pendurado”
esperando, resolvi que faria uma requisição assíncrona não-bloqueante
para um outro script que, por sua vez, acessaria a api e iria
tratar/salvar os dados que eu necessitava.
Como o php não tem suporte a threads, a
minha solução foi implementada com a biblioteca curl, mais
especificamente com a função curl_multi_*(), que permite fazer
requisições paralelas e assíncronas. Porém, os exemplos que encontrei
tanto na documentação no php.net quanto em classes disponibilizadas por
terceiros não funcionavam exatamente do jeito que eu queria, e acabei
quebrando a cabeça por algumas horas para encontrar a solução, que
gostaria de compartilhar aqui.
A abordagem quase unânime para uso do
multi_curl com as quais me deparei propunham o uso dela para fazer as
requisições paralelas não bloqueantes, executar em seguida algum
processamento não relacionado às requisições (por exemplo, fazer um
select no banco de dados), e depois ficar em busy-waiting até que todas
as requisições recebam uma resposta, para então finalizar o script. Um
exemplo de como fazer isso seria:
// Inicializa um multi-curl handle
$mch = curl_multi_init();
// Inicializa e seta as opções para cada requisição
$ch1 = curl_init('http://www.yahoo.com');
curl_setopt($ch1, CURLOPT_RETURNTRANSFER);
$ch2 = curl_init('http://www.google.com');
curl_setopt($ch2, CURLOPT_RETURNTRANSFER);
// Adiciona a requisição $ch1 ao multi-curl handle $mch.
curl_multi_add_handle($mch, $ch1);
// Executa requisição multi-curl e retorna imediatamente.
curl_multi_exec($mch, $active);
// Repete o procedimento para a requisição $ch2
curl_multi_add_handle($mch, $ch2);
curl_multi_exec($mch, $active);
// Executa outros processamentos
// Fica em busy-waiting até que todas as requisições retornem
do{
curl_multi_exec($mch, $active);
}while($active > 0);
// Acessa as respostas das requisições
$resp1 = curl_multi_getcontent($ch1);
$resp2 = curl_multi_getcontent($ch2);
No meu caso, como eu não precisava das
respostas, inicialmente tentei usar este mesmo código, removendo o “do
{ } while ( )” e as chamadas à curl_multi_getcontent(), isso porque
tanto o manual do php.net quanto os textos que li dão a entender que uma
única chamada a curl_multi_exec() seria suficiente pra iniciar as
requisições assíncronas e retornar imediatamente. Não foi bem o que
aconteceu com o meu script, que só fazia a requisição de fato quando eu
deixava o curl_multi_exec() em loop, o que não adiantava pra mim, pois
o loop só finalizava depois que a mesma retornava uma resposta.
Eis que, depois de algumas horas
pesquisando, encontrei o seguinte comentário: “curl_multi_perform is
asynchronous. It will only execute as little as possible and then
return back control to your program. It is designed to never block. If
it returns CURLM_CALL_MULTI_PERFORM you better call it again soon, as
that is a signal that it still has local data to send or remote data to
receive.”
Assim, pra resolver o meu problema
bastou substituir a chamada única à curl_multi_exec e o loop que ficava
em busy-waiting enquanto a conexão estivesse ativa por um loop que faz
chamadas a curl_multi_exec somente enquanto a constante
CURLM_CALL_MULTI_PERFORM estivesse retornando TRUE. No caso, o código
pra fazer isso seria:
do {
$res = curl_multi_exec($mch, $active);
} while ($res == CURLM_CALL_MULTI_PERFORM);
Deste modo, a requisição é feita (nos
meus testes, três iterações deste loop foram suficientes para concluir
o envio) e rapidamente retorna o controle para o script, que pode ser
finalizado sem esperar por uma resposta, deixando todo mundo (o
usuário, o programador e o servidor) feliz 🙂
P.S.: enquanto pesquisava uma solução,
me deparei com algumas classes e funções interessantes para usar o
curl_multi. Uma delas está neste post,
que contém a implementação eficiente de uma função para quem precisar
fazer grande número de requisições em paralelo e processar os
resultados à medida que forem retornando. Vai me ser útil num futuro
próximo.