Sana Inside » Post 'Non-blocking asynchronous requests usando curl_multi e php'

Non-blocking asynchronous requests usando curl_multi e php

Nesta semana 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 posts 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.

Tags: ,
  • Wemerson C. Guimarães

    Bom dia… abordagem interessante…

    Uma dúvida… quero mostrar na tela só dados relevantes após e não todo o resultado da requisição, mas mostra tudo mesmo sem dar nenhum echo.

© 2008 Powered by WordPress