Dev (Back & Front)ARTIGO

Non-blocking asynchronous requests usando curl_multi e php

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<br />$mch = curl_multi_init();<br />// Inicializa e seta as opções para cada requisição<br />$ch1 = curl_init('http://www.yahoo.com');<br />curl_setopt($ch1, CURLOPT_RETURNTRANSFER);<br />$ch2 = curl_init('http://www.google.com');<br />curl_setopt($ch2, CURLOPT_RETURNTRANSFER);<br />// Adiciona a requisição $ch1 ao multi-curl handle $mch.<br />curl_multi_add_handle($mch, $ch1);<br /><br />// Executa requisição multi-curl e retorna imediatamente.<br />curl_multi_exec($mch, $active);<br /><br />// Repete o procedimento para a requisição $ch2<br />curl_multi_add_handle($mch, $ch2);<br />curl_multi_exec($mch, $active);<br /><br />// Executa outros processamentos<br /><br />// Fica em busy-waiting até que todas as requisições retornem<br />do{<br />curl_multi_exec($mch, $active);<br />}while($active > 0);<br /><br />// Acessa as respostas das requisições<br />$resp1 = curl_multi_getcontent($ch1);<br />$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 {<br />$res = curl_multi_exec($mch, $active);<br />} 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.

mora em Vitória-ES, é empreendedor e desenvolvedor na internet há 10 anos, co-fundador do Flogao.com.br e (quase) bacharel em Ciência da Computação pela UFES. www.sanainside.com

Ver perfil