404 Not Found

404 Not Found


nginx

Filas e Tarefas Assíncronas no Laravel

Filas são a "equipe de bastidores" do Laravel — tarefas demoradas são transferidas para a fila para processamento assíncrono, para que os usuários não precisem esperar e as requisições retornem instantaneamente.

1. O Que Você Vai Aprender


2. A História de um Usuário

(1) Dor: Exportar relatórios causa o congelamento de todo o sistema

Bob clicou em "Exportar Relatório Mensal" no painel administrativo do ShopMetrics — levou 30 segundos para gerar um arquivo CSV contendo 100.000 registros de pedidos, durante os quais a página ficou carregando, e Alice também enfrentou lentidão ao acessar o dashboard ao mesmo tempo. Para piorar, Bob acionou cinco exportações de relatório simultaneamente, esgotando o pool de processos PHP e causando um erro 502 em todo o site.

(2) Solução Usando Filas

A fila envia tarefas demoradas para o segundo plano — quando um usuário clica em "Exportar", ele imediatamente vê a mensagem "Relatório está sendo gerado", enquanto o job é executado lentamente em segundo plano; assim que termina, um email é enviado com o link de download.

PHP
// Sync — bloqueia por 30 segundos
$csv = ReportService::generateMonthlyReport($tenant);

// Async — retorna instantaneamente, Job executa em segundo plano
GenerateReportJob::dispatch($tenant, 'monthly');
// Usuário vê: "Relatório está sendo gerado. Enviaremos um email quando estiver pronto."

(3) Resultado

Depois que Bob implementou a fila, as requisições de exportação de relatório agora retornam em 100 ms, os jobs em segundo plano executam suavemente, e o dashboard de Alice não congela mais.


3. Configuração de Driver de Fila

(1) Comparação de Drivers

Driver Persistência Performance Adequação Custo
sync ❌ Execução Instantânea Mais rápido Desenvolvimento/Testes Gratuito
database ✅ Tabela no BD Lento Projeto pequeno Gratuito
redis ✅ Memória Rápido Ambiente de Produção Redis
sqs ✅ AWS Alto Grande escala Pagamento por uso
beanstalkd ✅ Dedicado Rápido Médio Gratuito

(2) Configuração

BASH
# .env
QUEUE_CONNECTION=redis

# Conexão Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PHP
// config/queue.php
'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90,
    ],
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => '{default}',
        'retry_after' => 90,
        'block_for' => null,
    ],
],

(3) Criar Tabelas da Fila

BASH
# Para driver database
php artisan queue:table
php artisan queue:failed-table
php artisan migrate

# Cria: tabela jobs + tabela failed_jobs

(1) ▶ Exemplo: Configuração de Fila do ShopMetrics

BASH
# .env — Desenvolvimento
QUEUE_CONNECTION=database

# .env — Produção
QUEUE_CONNECTION=redis
REDIS_HOST=redis.shopmetrics.internal
REDIS_PORT=6379

# Criar tabelas da fila (driver database)
php artisan queue:table
php artisan queue:failed-table
php artisan queue:batches-table
php artisan migrate

Saída:

TEXT
# Comando executado com sucesso

4. Criação e Distribuição de Jobs

(1) Criar um Job

BASH
php artisan make:job GenerateReportJob
# Cria: app/Jobs/GenerateReportJob.php

(2) Definir um Job

PHP
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
    ) {}

    public function handle(
        ReportService $reportService,
        Mailer $mailer,
    ): void {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $url = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $this->tenant->users->each(function ($user) use ($url) {
            $mailer->to($user)->send(new ReportReadyNotification($url, $this->reportType));
        });
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('Falha na geração de relatório', [
            'tenant_id' => $this->tenant->id,
            'report_type' => $this->reportType,
            'error' => $exception->getMessage(),
        ]);
    }
}

(3) Distribuir o Job

PHP
// Dispatch básico
GenerateReportJob::dispatch($tenant, 'monthly');

// Dispatch com atraso
GenerateReportJob::dispatch($tenant, 'monthly')
    ->delay(now()->addMinutes(5));

// Dispatch para fila específica
GenerateReportJob::dispatch($tenant, 'monthly')
    ->onQueue('reports');

// Dispatch se a condição for atendida
GenerateReportJob::dispatchIf($tenant->subscription?->isActive(), $tenant, 'monthly');

// Dispatch após resposta enviada ao usuário
GenerateReportJob::dispatchAfterResponse($tenant, 'monthly');
Método de Distribuição Descrição Cenários Aplicáveis
dispatch() Enfileirar Tarefa Assíncrona Geral
dispatchSync() Executar sincronamente Deve ser completado imediatamente
dispatchAfterResponse() Executar após resposta Tarefa leve
delay() Execução Atrasada Tarefas Agendadas
onQueue() Fila Especificada Por Prioridade

(1) ▶ Exemplo: Job de Processamento de Pedidos do ShopMetrics

PHP
// app/Jobs/ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = [30, 60, 120];

    public function __construct(public Order $order) {}

    public function handle(
        OrderService $orderService,
        PaymentGateway $payment,
    ): void {
        // Cobrar pagamento
        $payment->charge($this->order);

        // Atualizar estoque
        $orderService->deductInventory($this->order);

        // Disparar evento
        event(new OrderPlaced($this->order));

        // Enviar email de confirmação
        $this->order->user->notify(new OrderConfirmationNotification($this->order));
    }

    public function failed(\Throwable $exception): void
    {
        $this->order->update(['status' => 'failed']);
        $this->order->user->notify(new OrderFailedNotification($this->order));
    }
}

Saída:

TEXT
// Execução bem-sucedida

5. Mecanismo de Retry em Caso de Falha

(1) Configuração de Retry

PHP
// Na classe Job
public int $tries = 3;           // Máximo de tentativas de retry
public int $backoff = 60;        // Segundos entre retentativas
public int $timeout = 120;       // Máximo de segundos por tentativa

// Ou backoff exponencial
public array $backoff = [30, 60, 120]; // 30s, 60s, 120s

// Ou backoff dinâmico
public function backoff(): int
{
    return 30 * $this->attempts();
}

(2) Tratamento de Erros

BASH
# Ver jobs falhados
php artisan queue:failed

# Retentar um job falhado específico
php artisan queue:retry 5

# Retentar todos os jobs falhados
php artisan queue:retry all

# Excluir um job falhado
php artisan queue:forget 5

# Limpar todos os jobs falhados
php artisan queue:flush

(3) Ciclo de Vida da Tarefa na Fila

100%
flowchart LR
    A[Job Enfileirado] --> B[Fila]
    B --> C[Worker Pega]
    C --> D{Sucesso?}
    D -->|Sim| E[Job Concluído]
    D -->|Não| F{attempts < tries?}
    F -->|Sim| G[Backoff + Retry]
    G --> B
    F -->|Não| H[Tabela failed_jobs]
    H --> I[Retry Manual / Flush]

(1) ▶ Exemplo: Configuração de Retry em Caso de Falha do ShopMetrics

PHP
// app/Jobs/SendWebhookJob.php
class SendWebhookJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];
    public int $timeout = 30;

    public function __construct(
        public Shop $shop,
        public array $payload,
    ) {}

    public function handle(): void
    {
        $response = Http::timeout($this->timeout)
            ->post($this->shop->webhook_url, $this->payload);

        if (!$response->successful()) {
            $this->release($this->backoff[$this->attempts() - 1] ?? 60);
        }
    }

    public function failed(\Throwable $exception): void
    {
        $this->shop->tenant->users->each(function ($user) {
            $user->notify(new WebhookFailedNotification($this->shop));
        });
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. Tarefas em Lote

(1) Criar um Batch

BASH
php artisan queue:batches-table
php artisan migrate
PHP
// app/Jobs/ProcessTenantAnalyticsJob.php
class ProcessTenantAnalyticsJob implements ShouldQueue
{
    use Batchable;

    public function __construct(public Tenant $tenant) {}

    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            return;
        }

        AnalyticsService::computeForTenant($this->tenant);
    }
}

(2) Distribuir Batch

PHP
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch(
    Tenant::active()->get()->map(
        fn ($tenant) => new ProcessTenantAnalyticsJob($tenant)
    )
)->then(function (Batch $batch) {
    // Todos os jobs completaram com sucesso
    Log::info("Batch {$batch->id} completado: {$batch->totalJobs} tenants processados.");
})->catch(function (Batch $batch, \Throwable $e) {
    // Primeira falha de job
    Log::error("Batch {$batch->id} falhou: {$e->getMessage()}");
})->finally(function (Batch $batch) {
    // Sempre executa (sucesso ou falha)
    Cache::forget('analytics:computing');
})->name('Processar Analytics Mensal')
  ->onQueue('analytics')
  ->dispatch();

(3) Gerenciamento de Batch

PHP
// Verificar progresso do batch
$batch = Bus::findBatch($batchId);
$batch->progress();     // 0-100
$batch->processedJobs();
$batch->totalJobs();
$batch->failedJobs();
$batch->finished();

// Cancelar batch
$batch->cancel();
Método Descrição
then() Todos os callbacks com sucesso
catch() Callback de falha na primeira tentativa
finally() Callback ao concluir (independentemente de sucesso ou falha)
progress() Porcentagem Completa
cancel() Cancelar Tarefas Restantes

(1) ▶ Exemplo: Tarefa em Lote de Análise Mensal do ShopMetrics

PHP
// app/Console/Commands/ProcessMonthlyAnalytics.php
class ProcessMonthlyAnalytics extends Command
{
    protected $signature = 'analytics:process-monthly';

    public function handle(): int
    {
        $tenants = Tenant::active()->get();
        $this->info("Processando analytics para {$tenants->count()} tenants...");

        Bus::batch(
            $tenants->map(fn ($t) => new ProcessTenantAnalyticsJob($t))
        )->then(function (Batch $batch) {
            $this->info("Todos os {$batch->totalJobs} tenants processados.");
        })->catch(function (Batch $batch, \Throwable $e) {
            $this->error("Batch falhou: {$e->getMessage()}");
        })->name('Analytics Mensal')
          ->onQueue('analytics')
          ->allowFailures()
          ->dispatch();

        return self::SUCCESS;
    }
}

Saída:

TEXT
// Execução bem-sucedida

7. Daemon Supervisor

(1) Instalar Supervisor

BASH
# Ubuntu/Debian
sudo apt-get install supervisor

# Criar configuração
sudo nano /etc/supervisor/conf.d/shopmetrics-worker.conf

(2) Configuração do Supervisor

INI
[program:shopmetrics-worker-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-default.log
stopwaitsecs=3600

[program:shopmetrics-worker-reports]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-reports.log

(3) O Comando supervisor

BASH
# Ler nova configuração
sudo supervisorctl reread
sudo supervisorctl update

# Iniciar/parar/reiniciar workers
sudo supervisorctl start shopmetrics-worker-default:*
sudo supervisorctl stop shopmetrics-worker-default:*
sudo supervisorctl restart shopmetrics-worker-default:*

# Verificar status
sudo supervisorctl status
Número de Processos Fila CPU Memória Descrição
2 default Baixo Médio Tarefas Gerais
1 reports Alto Alto Geração de Relatórios
1 analytics Alto Alto Análise de Dados
1 notifications Baixo Baixo Email/Push

(1) ▶ Exemplo: Iniciando um Worker Multi-Fila do ShopMetrics

BASH
# Desenvolvimento — worker único, todas as filas
php artisan queue:work --queue=default,reports,notifications

# Produção — workers separados por prioridade de fila
# Prioridade: high > default > low
php artisan queue:work redis --queue=high,default
php artisan queue:work redis --queue=reports,low

# Processar jobs com limite de tempo (auto-restart para vazamentos de memória)
php artisan queue:work --max-time=3600 --max-jobs=1000

# Monitorar fila
php artisan queue:monitor redis:default,redis:reports

Saída:

TEXT
# Comando executado com sucesso

8. Exemplo Compreensivo: Sistema de Relatórios Assíncronos do ShopMetrics

PHP
// ============================================
// Compreensivo: Sistema de Relatórios Assíncronos do ShopMetrics
// Abrange: jobs, batches, retries, supervisor, notifications
// ============================================

// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public array $backoff = [60, 180, 600];
    public int $timeout = 600;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
        public ?int $userId = null,
    ) {
        $this->onQueue('reports');
    }

    public function handle(ReportService $reportService): void
    {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $downloadUrl = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportReadyNotification(
            downloadUrl: $downloadUrl,
            reportType: $this->reportType,
            expiresAt: now()->addDays(7),
        ));
    }

    public function failed(\Throwable $exception): void
    {
        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportFailedNotification(
            reportType: $this->reportType,
            error: $exception->getMessage(),
        ));
    }
}

// Uso no controller
class ReportController extends Controller
{
    public function generate(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'report_type' => 'required|in:monthly,weekly,custom',
            'format' => 'sometimes|in:csv,xlsx,pdf',
            'date_from' => 'sometimes|date',
            'date_to' => 'sometimes|date|after:date_from',
        ]);

        GenerateReportJob::dispatch(
            tenant(),
            $validated['report_type'],
            $validated['format'] ?? 'csv',
            auth()->id(),
        );

        return response()->json([
            'message' => 'Geração de relatório iniciada. Você receberá um email quando estiver pronto.',
            'estimated_time' => '5-15 minutos',
        ], 202);
    }
}

❓ Perguntas Frequentes

P Qual é a diferença entre queue:work e queue:listen?
R queue:work executa em memória sem reiniciar o framework (rápido, mas requer restart manual após alterações de código); queue:listen reinicia o framework para cada tarefa (lento, mas carrega automaticamente o novo código). Use queue:work + Supervisor em produção, e queue:listen ou queue:work --once durante o desenvolvimento.
P Posso usar modelos Eloquent em um Job?
R Sim, a trait SerializesModels serializa automaticamente os IDs dos modelos e os recupera do banco de dados durante a deserialização. O benefício é que os Jobs têm menor pegada de dados; o risco é que se um modelo for excluído, o Job falhará (isso pode ser ignorado usando deleteWhenMissingModels).
P Como posso evitar que a fila acumule?
R Aumente o número de workers (numprocs), use um driver de maior performance (Redis), defina um timeout para o job para evitar deadlocks e configure alertas para monitorar o comprimento da fila.
P O que devo fazer se uma tarefa em um batch falhar?
R Por padrão, o Batch cancela as tarefas restantes ao encontrar a primeira falha. Defina allowFailures() para permitir que o batch continue executando apesar de falhas parciais. Use Bus::findBatch() para ver detalhes das falhas e retentar manualmente as tarefas falhadas.
P Como depurar um job?
R Durante o desenvolvimento, use QUEUE_CONNECTION=sync para executar o job sincronamente (erros são exibidos imediatamente); em produção, use php artisan queue:failed para ver mensagens de erro dos jobs falhados; você também pode registrar informações detalhadas no método failed() do job.
P Qual é a diferença entre Supervisor e systemd?
R Supervisor é um gerenciador de processos Python fácil de configurar e recomendado oficialmente pelo Laravel; systemd é o gerenciador de serviços nativo do Linux, que oferece melhor performance mas é mais complexo de configurar. Ambos podem executar o processo queue:work como daemon.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie um GenerateReportJob, faça dispatch de uma requisição assíncrona para gerar um relatório CSV no controller, teste usando o driver database e execute queue:work para verificar que o job é executado.

  2. Exercício Avançado (⭐⭐): Configure o driver de fila Redis para implementar 3 retentativas com backoff exponencial (30s/60s/120s), envie notificações aos usuários quando um job falhar e teste retentativas manuais (queue:retry).

  3. Desafio (⭐⭐⭐): Use Bus::batch() para implementar uma tarefa em lote de análise mensal multi-tenant — itere por todos os tenants ativos, crie um job por tenant, monitore a porcentagem de progresso, limpe o cache ao concluir e configure o Supervisor para monitorar o worker.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%