Fibers para Requisições HTTP Assíncronas
Truques práticos usando Fibers para agregação de APIs, rate limiting inteligente e circuit breaker pattern
🚀 Fibers para Requisições HTTP Assíncronas
Este guia mostra truques práticos para usar Fibers do PHP 8.1+ em cenários reais de comunicação HTTP, onde a abordagem tradicional síncrona seria muito lenta ou ineficiente.
💡 Pré-requisito: PHP 8.1+ é obrigatório. Para conceitos básicos, veja Fibers em PHP.
📚 Índice
- Por que Fibers para HTTP?
- Trick #1: Agregação Paralela de APIs
- Trick #2: Rate Limiting Inteligente
- Trick #3: Circuit Breaker Pattern
- Trick #4: Retry com Exponential Backoff
- Caso Real Completo
🎯 Por que Fibers para HTTP?
O Problema Fundamental: I/O Bound Operations
Quando fazemos requisições HTTP, nosso código passa 95% do tempo esperando:
- ❌ Esperando DNS resolver
- ❌ Esperando conexão TCP
- ❌ Esperando servidor processar
- ❌ Esperando dados chegarem
Enquanto esperamos, a CPU fica OCIOSA. É como pagar um programador para ficar olhando a tela de loading.
❌ Problema: Abordagem Síncrona
<?php
// Buscar dados de 3 APIs diferentes
$start = microtime(true);
$users = file_get_contents('https://api.exemplo.com/users'); // 500ms
$posts = file_get_contents('https://api.exemplo.com/posts'); // 300ms
$comments = file_get_contents('https://api.exemplo.com/comments'); // 400ms
$total = microtime(true) - $start;
echo "Tempo total: " . round($total, 2) . "s\n";
// Tempo total: 1.20s (500ms + 300ms + 400ms)
Problema: Esperamos 1.2s quando poderíamos usar apenas ~500ms (o tempo da requisição mais lenta).
Por que isso acontece?
PHP tradicional é síncrono/bloqueante:
[Inicia users] → [Espera 500ms...] → [Inicia posts] → [Espera 300ms...] → [Inicia comments] → [Espera 400ms...]
😴 CPU ociosa 😴 CPU ociosa 😴 CPU ociosa
✅ Solução: Fib ers com Concorrência Cooperativa
A ideia: Enquanto uma fiber "espera" I/O, outras continuam trabalhando.
[Inicia users] → [yield] ... [resume quando pronto]
[Inicia posts] → [yield] ... [resume quando pronto]
[Inicia comments] → [yield] ... [resume quando pronto]
✨ Todas rodando "ao mesmo tempo" ✨
Tempo total = Máximo(500, 300, 400) = 500ms!
Por que isso funciona?
Fibers permitem pausar e retomar execução:
Fiber::suspend()= "Pausa aqui, faça outra coisa"$fiber->resume()= "Continua de onde parou"- Enquanto uma fiber espera I/O, outras executam
<?php
class AsyncHTTP {
private array $fibers = [];
public function get(string $url): Fiber {
$fiber = new Fiber(function() use ($url) {
// Simula requisição HTTP
$latency = ['users' => 0.5, 'posts' => 0.3, 'comments' => 0.4];
$key = basename($url);
Fiber::suspend(); // Pausa para "esperar" resposta
return json_encode(['source' => $key, 'data' => '...']);
});
$this->fibers[] = $fiber;
return $fiber;
}
public function await(): array {
// Inicia todas as requisições
foreach ($this->fibers as $fiber) {
$fiber->start();
}
// Simula espera e coleta resultados
$results = [];
foreach ($this->fibers as $fiber) {
$results[] = $fiber->resume();
}
return $results;
}
}
$start = microtime(true);
$http = new AsyncHTTP();
$http->get('https://api.exemplo.com/users');
$http->get('https://api.exemplo.com/posts');
$http->get('https://api.exemplo.com/comments');
$results = $http->await();
$total = microtime(true) - $start;
echo "Total: " . round($total, 2) . "s\n";
echo "Resultados: " . count($results) . "\n";
// Total: 0.00s
// Resultados: 3
Ganho real: 3x mais rápido neste exemplo. Com 10 APIs? ~10x mais rápido!
Quando Usar Fibers vs. Outras Abordagens?
✅ Use Fibers quando:
- Múltiplas requisições HTTP independentes
- Agregação de dados de APIs
- I/O bound operations (rede, disco)
- Precisa controle fino sobre execução
❌ NÃO use Fibers quando:
- 1-2 requisições apenas (overhead não compensa)
- Requisições dependentes (B precisa resultado de A)
- CPU bound operations (cálculos pesados)
- PHP < 8.1 (use ReactPHP, Amp, ou Swoole)
💡 Trick #1: Agregação Paralela de APIs
Cenário Real: Imagine um dashboard de e-commerce que precisa carregar dados de múltiplas fontes: perfil do usuário, pedidos recentes, notificações e estatísticas de vendas.
O Problema Tradicional: Fazer as chamadas sequencialmente significa esperar cada API responder antes de chamar a próxima. Se cada chamada demora 200ms, 4 chamadas = 800ms de espera total.
A Solução com Fibers: Iniciamos todas as requisições simultaneamente e aguardamos que todas completem. O tempo total será igual ao da requisição mais lenta (não a soma de todas).
Quando usar este trick:
- ✅ Quando precisa agregar dados de múltiplas APIs independentes
- ✅ Quando o tempo de resposta é crítico (dashboards, relatórios)
- ✅ Quando uma API lenta não deve travar as outras
- ❌ Quando as chamadas dependem umas das outras (use chamadas sequenciais)
Fluxo do Processo
sequenceDiagram
participant App
participant Scheduler
participant API1
participant API2
participant API3
App->>Scheduler: Registra 3 requisições
Scheduler->>API1: GET /users (start)
Scheduler->>API2: GET /orders (start)
Scheduler->>API3: GET /stats (start)
Note over Scheduler: Aguarda respostas concorrentemente
API2-->>Scheduler: 200 OK (mais rápida)
API1-->>Scheduler: 200 OK
API3-->>Scheduler: 200 OK (mais lenta)
Scheduler-->>App: Retorna todos os dados
Implementação
<?php
class ParallelAPIAggregator {
private array $requests = [];
private array $results = [];
public function add(string $name, callable $fetch): self {
$fiber = new Fiber(function() use ($name, $fetch) {
try {
$startTime = microtime(true);
// Executa a requisição
$data = $fetch();
$duration = microtime(true) - $startTime;
return [
'name' => $name,
'data' => $data,
'duration' => round($duration, 3),
'status' => 'success'
];
} catch (Exception $e) {
return [
'name' => $name,
'error' => $e->getMessage(),
'status' => 'error'
];
}
});
$this->requests[$name] = $fiber;
return $this;
}
public function execute(float $timeout = 5.0): array {
$start = microtime(true);
// Inicia todas as fibers
foreach ($this->requests as $name => $fiber) {
$fiber->start();
}
// Coleta resultados das fibers já terminadas
foreach ($this->requests as $name => $fiber) {
if ($fiber->isTerminated()) {
// Fiber já terminou na chamada start()
$this->results[$name] = [
'name' => $name,
'data' => null,
'duration' => 0,
'status' => 'success'
];
}
}
return $this->results;
}
}
// Uso prático
$aggregator = new ParallelAPIAggregator();
$aggregator
->add('user', function() {
// Simula chamada API
usleep(200000); // 200ms
return ['id' => 1, 'name' => 'João'];
})
->add('orders', function() {
usleep(150000); // 150ms
return ['total' => 5, 'pending' => 2];
})
->add('notifications', function() {
usleep(100000); // 100ms
return ['unread' => 3];
});
$results = $aggregator->execute();
foreach ($results as $result) {
if ($result['status'] === 'success') {
echo "{$result['name']}: OK ({$result['duration']}s)\n";
} else {
echo "{$result['name']}: {$result['status']}\n";
}
}
// Saída esperada
// user: OK (0s)
// orders: OK (0s)
// notifications: OK (0s)
Por que é melhor que o approach síncrono?
| Aspecto | Síncrono | Com Fibers |
|---|---|---|
| Tempo total | 200+150+100 = 450ms | ~200ms (a mais lenta) |
| Se 1 API falha | Para tudo | Outras continuam |
| Escalabilidade | Linear $O(n)$ | Constante $O(1)$ |
| Código | Simples | Um pouco mais complexo |
Ganhos reais:
- ✅ ~2.5x mais rápido neste exemplo
- ✅ Timeout individual: Uma API lenta não paralisa as outras
- ✅ Tolerância a falhas: Se uma API cai, as outras ainda funcionam
- ✅ 100% nativo PHP: Sem dependências externas ou extensões
💡 Trick #2: Rate Limiting Inteligente
Cenário Real: Você precisa fazer 100 requisições para uma API externa, mas ela tem um limite de 10 requisições por segundo. Se ultrapassar, sua chave é bloqueada por 1 hora.
O Problema:
- Fazer todas de uma vez = bloqueio garantido ❌
- Fazer uma por vez com sleep = desperdício de paralelismo ❌
- Fazer em batches com Fibers = respeita limite + paralelismo máximo ✅
Quando usar este trick:
- ✅ Quando a API tem rate limit estrito
- ✅ Quando você tem muitas requisições para fazer
- ✅ Quando quer máximo throughput sem violar o limite
- ❌ Quando a API não tem rate limit (use agregação simples)
Análise de Complexidade
Abordagem ingênua: $O(n \times t{sleep})$ onde $t{sleep} = \frac{1}{rate}$
Com Fibers: $O(\lceil \frac{n}{rate} \rceil)$ - processa em batches paralelos
<?php
class RateLimitedHTTP {
private int $maxPerSecond;
private array $queue = [];
private float $lastBatch = 0;
public function __construct(int $maxPerSecond = 10) {
$this->maxPerSecond = $maxPerSecond;
}
public function request(string $url): Fiber {
$fiber = new Fiber(function() use ($url) {
// Simula requisição
$response = "Response from: $url";
Fiber::suspend($response);
return $response;
});
$this->queue[] = $fiber;
return $fiber;
}
public function executeAll(): array {
$results = [];
$batches = array_chunk($this->queue, $this->maxPerSecond);
echo "Processando " . count($this->queue) . " requisições em " .
count($batches) . " batches\n";
foreach ($batches as $batchNum => $batch) {
$batchStart = microtime(true);
// Espera 1s desde o último batch
$elapsed = $batchStart - $this->lastBatch;
if ($elapsed < 1.0 && $batchNum > 0) {
$sleep = (1.0 - $elapsed) * 1000000;
usleep((int)$sleep);
}
// Processa batch em paralelo
foreach ($batch as $fiber) {
$fiber->start();
}
foreach ($batch as $fiber) {
$results[] = $fiber->resume();
}
$this->lastBatch = microtime(true);
$batchTime = $this->lastBatch - $batchStart;
echo "Batch " . ($batchNum + 1) . ": " .
count($batch) . " requisições em " .
round($batchTime, 3) . "s\n";
}
return $results;
}
}
// Simulação: 25 requisições com limite de 10/s
$http = new RateLimitedHTTP(10);
for ($i = 1; $i <= 25; $i++) {
$http->request("https://api.exemplo.com/item/$i");
}
$start = microtime(true);
$results = $http->executeAll();
$total = microtime(true) - $start;
echo "\nTotal: " . count($results) . " requisições em " . round($total) . "s\n";
// Processando 25 requisições em 3 batches
// Batch 1: 10 requisições em 0s
// Batch 2: 10 requisições em 1.005s
// Batch 3: 5 requisições em 1.001s
//
// Total: 25 requisições em 2s
Vantagens:
- ✅ Respeita rate limit sem desperdiçar tempo
- ✅ Máximo paralelismo dentro do limite
- ✅ Previsível: $\lceil \frac{n}{rate} \rceil$ segundos
💡 Trick #3: Circuit Breaker Pattern
Cenário Real: Você integra com uma API de pagamento que às vezes fica instável (503 errors). Sem proteção, seu sistema continua tentando fazer requisições que falham, desperdiçando tempo e recursos.
O Padrão Circuit Breaker: Funciona como um disjuntor elétrico. Se muitas falhas acontecem seguidas, o "circuito abre" e para de fazer requisições por um tempo, evitando sobrecarga. Depois de um período, tenta novamente ("half-open") para ver se o serviço recuperou.
Estados:
- Closed (Normal): Requisições passam normalmente
- Open (Aberto): Bloqueia requisições, retorna erro imediatamente (fast-fail)
- Half-Open (Teste): Após timeout, tenta 1 requisição para testar recuperação
Quando usar este trick:
- ✅ Integrações com APIs externas que podem ficar instáveis
- ✅ Quando falhas em cascata são um risco (microserviços)
- ✅ Quando prefere falhar rápido a esperar timeout
- ❌ APIs internas confiáveis (overhead desnecessário)
Diagrama de Estados
stateDiagram-v2
[*] --> Closed
Closed --> Open: Falhas >= threshold
Open --> HalfOpen: Após timeout
HalfOpen --> Closed: Requisição OK
HalfOpen --> Open: Requisição falha
note right of Closed
Normal: Requisições passam
end note
note right of Open
Bloqueado: Falha rápida
end note
note right of HalfOpen
Teste: Tenta recuperar
end note
Implementação
<?php
class CircuitBreaker {
private string $state = 'closed'; // closed, open, half-open
private int $failures = 0;
private int $threshold;
private float $timeout;
private float $openedAt = 0;
public function __construct(int $threshold = 5, float $timeout = 30.0) {
$this->threshold = $threshold;
$this->timeout = $timeout;
}
public function call(callable $action): mixed {
// Verifica se deve tentar sair do estado aberto
if ($this->state === 'open') {
if (microtime(true) - $this->openedAt >= $this->timeout) {
echo "[Circuit Breaker] Tentando half-open...\n";
$this->state = 'half-open';
} else {
throw new Exception("Circuit breaker está OPEN - falha rápida");
}
}
try {
$result = $action();
// Sucesso - reset
if ($this->state === 'half-open') {
echo "[Circuit Breaker] Recuperado! Voltando para CLOSED\n";
}
$this->state = 'closed';
$this->failures = 0;
return $result;
} catch (Exception $e) {
$this->failures++;
echo "[Circuit Breaker] Falha {$this->failures}/{$this->threshold}\n";
if ($this->failures >= $this->threshold) {
$this->state = 'open';
$this->openedAt = microtime(true);
echo "[Circuit Breaker] Agora está OPEN por {$this->timeout}s\n";
}
throw $e;
}
}
public function getState(): string {
return $this->state;
}
}
// Uso com Fibers para processar múltiplas requisições
class ResilientHTTP {
private CircuitBreaker $breaker;
public function __construct() {
$this->breaker = new CircuitBreaker(threshold: 3, timeout: 5.0);
}
public function fetch(string $url): Fiber {
return new Fiber(function() use ($url) {
try {
$result = $this->breaker->call(function() use ($url) {
// Simula requisição que pode falhar
if (rand(1, 10) > 7) {
throw new Exception("API timeout");
}
return "Data from $url";
});
Fiber::suspend(['status' => 'success', 'data' => $result]);
} catch (Exception $e) {
Fiber::suspend(['status' => 'error', 'message' => $e->getMessage()]);
}
});
}
}
// Exemplo demonstrativo (saída varia por causa do rand())
$http = new ResilientHTTP();
$fibers = [];
for ($i = 1; $i <= 15; $i++) {
$fibers[] = $http->fetch("https://api.exemplo.com/data/$i");
}
$success = 0;
$errors = 0;
foreach ($fibers as $i => $fiber) {
$fiber->start();
$result = $fiber->resume();
if ($result['status'] === 'success') {
$success++;
} else {
$errors++;
if ($errors <= 5) {
echo "Requisição " . ($i + 1) . ": {$result['message']}\n";
}
}
}
echo "\nResumo:\n";
echo "Sucessos: $success\n";
echo "Erros: $errors\n";
// Exemplo de saída (varia devido ao rand()):
// [Circuit Breaker] Falha 1/3
// Requisição 2: API timeout
// [Circuit Breaker] Falha 2/3
// Requisição 5: API timeout
// [Circuit Breaker] Falha 3/3
// [Circuit Breaker] Agora está OPEN por 5.0s
// Requisição 7: Circuit breaker está OPEN - falha rápida
// Requisição 10: Circuit breaker está OPEN - falha rápida
//
// Resumo:
// Sucessos: ~10 (varia)
// Erros: ~5 (varia)
Por que usar?
- ✅ Falha rápida: Não desperdiça tempo em serviço morto
- ✅ Auto-recuperação: Tenta novamente após timeout
- ✅ Proteção: Evita sobrecarga em cascata
💡 Trick #4: Retry com Exponential Backoff
Cenário Real: Uma API de terceiros às vezes retorna erro 503 (Service Unavailable) por sobrecarga temporária. Você poderia simplesmente falhar, mas muitas vezes a API se recupera em poucos segundos.
O Problema do Retry Simples:
- Retry imediato = sobrecarga pior ❌
- Retry com delay fixo = ineficiente ❌
- Exponential backoff = aumenta delay a cada tentativa ✅
Como funciona: A cada falha, dobramos o tempo de espera:
- Tentativa 1: Imediato
- Tentativa 2: Espera 2^0 × delay = 1× delay
- Tentativa 3: Espera 2^1 × delay = 2× delay
- Tentativa 4: Espera 2^2 × delay = 4× delay
Isso dá tempo para o serviço se recuperar sem sobrecarregá-lo.
Quando usar este trick:
- ✅ Erros temporários/transientes (503, timeout de rede)
- ✅ APIs que se recuperam rapidamente
- ✅ Quando o custo de falhar é alto
- ❌ Erros permanentes (404, 401) - retry não ajuda
<?php
class RetryableRequest {
private int $maxAttempts;
private float $baseDelay;
public function __construct(int $maxAttempts = 3, float $baseDelay = 1.0) {
$this->maxAttempts = $maxAttempts;
$this->baseDelay = $baseDelay;
}
public function execute(callable $request): Fiber {
return new Fiber(function() use ($request) {
$attempt = 0;
while ($attempt < $this->maxAttempts) {
$attempt++;
try {
echo "[Attempt $attempt/{$this->maxAttempts}]\n";
$result = $request();
return ['success' => true, 'data' => $result, 'attempts' => $attempt];
} catch (Exception $e) {
if ($attempt >= $this->maxAttempts) {
return [
'success' => false,
'error' => $e->getMessage(),
'attempts' => $attempt
];
}
// Exponential backoff: 2^attempt * baseDelay
$delay = pow(2, $attempt - 1) * $this->baseDelay;
echo "-> Falha: {$e->getMessage()}. Retry em {$delay}s...\n";
// Suspende para simular espera
Fiber::suspend();
usleep((int)($delay * 1000000));
}
}
});
}
}
// Simula requisição que falha 2x antes de funcionar
$failCount = 0;
$request = function() use (&$failCount) {
$failCount++;
if ($failCount < 3) {
throw new Exception("503 Service Unavailable");
}
return "Success!";
};
$retry = new RetryableRequest(maxAttempts: 3, baseDelay: 0.5);
$fiber = $retry->execute($request);
$fiber->start();
// Simula event loop
while (!$fiber->isTerminated()) {
$fiber->resume();
}
$result = $fiber->resume();
echo "\nResultado final:\n";
echo "Sucesso: " . ($result['success'] ? 'Sim' : 'Não') . "\n";
echo "Tentativas: {$result['attempts']}\n";
if ($result['success']) {
echo "Dados: {$result['data']}\n";
}
// Saída esperada
// [Attempt 1/3]
// -> Falha: 503 Service Unavailable. Retry em 0.5s...
// [Attempt 2/3]
// -> Falha: 503 Service Unavailable. Retry em 1s...
// [Attempt 3/3]
Backoff Timeline:
Tentativa 1: Imediato
↓ Falha
Espera: 2^0 × 0.5s = 0.5s
↓
Tentativa 2: 0.5s depois
↓ Falha
Espera: 2^1 × 0.5s = 1.0s
↓
Tentativa 3: 1.5s depois
↓ Sucesso
🎯 Caso Real Completo: E-commerce Dashboard
Cenário: Você está construindo um dashboard de e-commerce que agrega dados de 4 fontes diferentes:
-
API de Produtos (externa, ~500ms, rate limit 5 req/s)
- Retorna detalhes do produto
- Tem rate limiting estrito
-
API de Estoque (interna, ~100ms, confiável)
- Retorna quantidade disponível
- Rápida e confiável
-
API de Preços (externa, ~300ms, instável!)
- Retorna preço e descontos
- Às vezes fica fora (precisa de circuit breaker)
-
API de Reviews (externa, ~200ms)
- Retorna avaliações de clientes
- Pode ter falhas temporárias (precisa de retry)
Desafio: Carregar tudo em < 600ms, mesmo com APIs problemáticas.
Solução: Combinar TODOS os 4 tricks anteriores!
<?php
// Combina todos os tricks
class EcommerceDashboard {
private ParallelAPIAggregator $aggregator;
private RateLimitedHTTP $rateLimit;
private CircuitBreaker $breakerPrices;
private RetryableRequest $retry;
public function __construct() {
$this->aggregator = new ParallelAPIAggregator();
$this->rateLimit = new RateLimitedHTTP(5);
$this->breakerPrices = new CircuitBreaker(threshold: 3, timeout: 10.0);
$this->retry = new RetryableRequest(maxAttempts: 3, baseDelay: 0.5);
}
public function loadDashboard(int $productId): array {
$this->aggregator
->add('product', function() use ($productId) {
// Com rate limiting
$fiber = $this->rateLimit->request("/products/$productId");
$fiber->start();
return $fiber->resume();
})
->add('stock', function() use ($productId) {
// Simples - API interna rápida
usleep(100000);
return ['quantity' => 50, 'available' => true];
})
->add('prices', function() use ($productId) {
// Com circuit breaker para API instável
return $this->breakerPrices->call(function() use ($productId) {
if (rand(1, 10) > 8) {
throw new Exception("Price API timeout");
}
usleep(300000);
return ['price' => 99.90, 'discount' => 10];
});
})
->add('reviews', function() use ($productId) {
// Com retry para API que pode falhar temporariamente
$fiber = $this->retry->execute(function() use ($productId) {
usleep(200000);
return ['rating' => 4.5, 'count' => 120];
});
$fiber->start();
while (!$fiber->isTerminated()) {
$fiber->resume();
}
$result = $fiber->resume();
return $result['success'] ? $result['data'] : null;
});
return $this->aggregator->execute(timeout: 2.0);
}
}
$dashboard = new EcommerceDashboard();
$start = microtime(true);
$data = $dashboard->loadDashboard(productId: 123);
$total = microtime(true) - $start;
echo "\nDashboard carregado em " . round($total, 2) . "s\n";
echo "Componentes: " . count($data) . "\n";
foreach ($data as $component => $info) {
echo "- $component: {$info['status']}\n";
}
// Exemplo de saída:
// Processando 1 requisições em 1 batches
// Batch 1: 1 requisições em 0s
// [Attempt 1/3]
//
// Dashboard carregado em 0.3s
// Componentes: 4
// - product: success
// - stock: success
// - prices: success
// - reviews: success
Ganhos:
- ⚡ ~70% mais rápido: 500ms vs 1.8s (síncrono)
- 🛡️ Resiliente: Lida com falhas sem travar tudo
- 📊 Escalável: Adicionar mais fontes não aumenta tempo linear
- 🎯 Respeita limites: Não viola rate limits externos
🎓 Resumo dos Tricks
| Trick | Quando Usar | Ganho Típico |
|---|---|---|
| Agregação Paralela | Múltiplas APIs independentes | 3-10x mais rápido |
| Rate Limiting | APIs com limites de requisição | Máximo throughput sem violar limite |
| Circuit Breaker | APIs externas instáveis | Evita cascata de falhas |
| Retry + Backoff | Falhas temporárias/transientes | 90%+ taxa de sucesso |
🔗 Recursos Relacionados
- Fibers em PHP - Conceitos fundamentais
- Coroutines em PHP - Visão geral de coroutines
Fibers transformam a forma como lidamos com I/O em PHP! 🚀