🚀 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

  1. Por que Fibers para HTTP?
  2. Trick #1: Agregação Paralela de APIs
  3. Trick #2: Rate Limiting Inteligente
  4. Trick #3: Circuit Breaker Pattern
  5. Trick #4: Retry com Exponential Backoff
  6. 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:

  1. Fiber::suspend() = "Pausa aqui, faça outra coisa"
  2. $fiber->resume() = "Continua de onde parou"
  3. 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:

  1. API de Produtos (externa, ~500ms, rate limit 5 req/s)

    • Retorna detalhes do produto
    • Tem rate limiting estrito
  2. API de Estoque (interna, ~100ms, confiável)

    • Retorna quantidade disponível
    • Rápida e confiável
  3. API de Preços (externa, ~300ms, instável!)

    • Retorna preço e descontos
    • Às vezes fica fora (precisa de circuit breaker)
  4. 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 transformam a forma como lidamos com I/O em PHP! 🚀