Jobs no Laravel e tratamento de exceção

O que é um Job no Laravel?

No Laravel, os Jobs executam tarefas de forma assíncrona ou em segundo plano. Eles distribuem tarefas demoradas, como envio de e-mails e processamento de arquivos, o que, consequentemente, melhora o desempenho da aplicação. Como resultado, os Jobs mantêm a aplicação responsiva ao gerenciar de forma eficaz tarefas pesadas, o que, por sua vez, aumenta a escalabilidade e evita bloqueios no sistema. Além disso, o sistema permite que as tarefas sejam reenfileiradas automaticamente em caso de erros, garantindo maior robustez no processamento.

  • Enviar e-mails em massa
  • Processar imagens ou vídeos
  • Executar relatórios e análises de dados
  • Sincronizar grandes volumes de dados entre sistemas

Por que utilizar Jobs?

  1. Desempenho Melhorado: Jobs garantem que tarefas pesadas não interrompam a interação do usuário, tornando a aplicação mais fluida e escalável.
  2. Tolerância a Falhas: Quando ocorre uma falha, os Jobs podem ser reenfileirados automaticamente. Além disso, você pode configurar o número máximo de tentativas antes de marcar o Job como falhado.
  3. Manutenção Facilitada: Ao separar tarefas em Jobs, você torna a organização e a manutenção do código muito mais simples, deixando-o mais legível e modular.

Como Criar um Job

Você pode criar um Job no Laravel de maneira simples. Primeiro, execute o seguinte comando Artisan:

php artisan make:job Teste

Após você executar esse comando, o Laravel cria uma classe em app/Jobs, e o método handle, em seguida, define a lógica do Job. Por exemplo, no código abaixo, o Job força propositalmente uma falha.

namespace App\Jobs;

use Exception;

class Teste extends Job
{
    public function handle()
    {
        // Aqui você adiciona sua lógica de job mas vou forçar uma exceção
        throw new Exception("Erro de teste");
    }
}

Após a criação, podemos enfileirar ester Job com o comando:

Teste::dispatch();

Note que o comando dispatch() pode ser disparado de várias partes da aplicação, como em rotas, controladores ou eventos. Por isso, podemos incluir uma rota de teste dessa maneira:

Route::get('/test-job', function () {
    Teste::dispatch();
    return 'Job enfileirado com sucesso!';
});

Uma vez que invocamos aquela rota e enfileiramos o Job Teste, podemos iniciar o worker para processá-lo e fazemos com o seguinte comando:

php artisan queue:work

Então, veja que, enquanto o worker processa os Jobs na fila, ele executa a lógica definida no método handle de cada Job. Portanto, se ocorrer uma falha, o Laravel capturará automaticamente a exceção e tentará executar o Job novamente, conforme a quantidade de tentativas configurada para esse Job.

Onde Chamar o Dispatch

Você pode chamar o dispatch em diferentes partes da aplicação, como:

Dentro de um Controller:

public function enviarEmail(Request $request)
{
    NomeDoJob::dispatch($request->email);
    return response()->json('Job enfileirado!');
}

Dentro de um Event Listener:

public function handle(UserRegistered $event)
{
    NomeDoJob::dispatch($event->user);
}

Diretamente em Middleware ou Services: Você também pode chamar dispatch() em middleware, serviços ou até mesmo em comandos artisan, dependendo da necessidade.

Finalmente, o local onde você decide chamar dispatch() depende do contexto em que o Job precisa ser processado.

O Que Acontece Quando Um Job É Disparado?

Ao disparar um Job no Laravel com dispatch(), o Laravel o coloca em uma fila. Essa fila mantém os Jobs até que um worker os processe. O Laravel suporta diferentes backends para filas, como:

  1. Banco de Dados: Armazena os Jobs em uma tabela (jobs).
  2. Redis: Mantém os Jobs na memória, proporcionando maior desempenho.
  3. SQS (Amazon SQS): Serviço de fila da AWS.

Você escolhe o backend configurando a variável QUEUE_CONNECTION no arquivo .env. Por exemplo:

QUEUE_CONNECTION=database

Enquanto o worker está em execução, ele processa cada Job na fila. Se o Laravel processar o Job com sucesso, ele o removerá da fila. No entanto, caso ocorra uma exceção durante o processamento, o Laravel oferece mecanismos automáticos para lidar com falhas.

Tratamento de Exceções

Jobs podem falhar em situações comuns, como:

  • Problemas de permissões de arquivos: Se o Job lida com upload ou manipulação de arquivos, ele pode falhar devido à falta de permissões.
  • Falha em integração com APIs externas: Jobs podem falhar por problemas de rede ou autenticação ao se comunicar com APIs.

O Laravel captura essas exceções automaticamente. Além disso, você pode configurar quantas tentativas o Job terá antes de o Laravel marcá-lo como falhado. Caso o número de tentativas seja excedido, o Laravel registrará o Job na tabela failed_jobs para que você consulte o erro manualmente.

Exemplo de configuração de tentativas em um Job:

public $tries = 5; // Tentativas antes de marcar como falhado

Como Consultar Falhas de Jobs no Banco de Dados

Se você configurou o banco de dados como backend para armazenar os Jobs, siga estes passos para consultar manualmente o motivo de uma falha:

1. Verifique a tabela failed_jobs: Os Jobs que falharam ficam registrados nessa tabela.

2. Execute uma consulta SQL para visualizar o motivo da falha:

SELECT * FROM failed_jobs WHERE id = <id_do_job>;

        3. Verifique a coluna exception: Ela contém a mensagem de erro e o stack trace da exceção.

        idconnectionqueuepayloadexceptionfailed_at
        1databasedefault{“job”:”SendEmailJob”,”data”:{“user_id”:1}}SQLSTATE[HY000]: General error: 1364 Field ‘email’ doesn’t have a default value2024-10-02 10:12:45
        2redisemails{“job”:”ProcessReportJob”,”data”:{“report_id”:5}}Connection refused [tcp://localhost:6379] during Redis operation2024-10-02 11:30:12
        3databasedefault{“job”:”UpdateStatsJob”,”data”:{“stats_id”:10}}Error: Undefined variable: stats_id in UpdateStatsJob.php2024-10-02 12:45:00
        Simulação de resultado da consulta ao danco de dados

        Olhando Dentro do Worker

        Como vimos, ao enfileirar um Job com dispatch(), ele entra na fila, onde o worker o processa. O worker é um processo em segundo plano que monitora as filas e processa os Jobs assim que são enfileirados.

        Para iniciar o worker, execute o seguinte comando:

        php artisan queue:work
        

        Quando o worker detecta um Job na fila, ele segue este fluxo:

        O comando queue:work chama o método handle da classe WorkCommand, que inicia o processamento dos Jobs. Em seguida, o método handle invoca runNextJob no worker para processar o próximo Job.

          // trecho da classe: vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php
          
          public function handle()
          {
              $this->worker->runNextJob($this->gatherWorkerOptions());
          }
          

          Aqui, o método runNextJob é chamado para processar o próximo Job da fila.

          Executando o Próximo Job

          Assim que o worker obtém o próximo Job da fila, ele começa a executá-lo. Posteriormente, o método runNextJob da classe Worker captura o Job na fila e o encaminha para processamento. Logo em seguida, ele chama o método process(), que então gerencia a execução do Job.

          // trecho da classe: vendor/laravel/framework/src/Illuminate/Queue/Worker.php
          
          public function runNextJob(WorkerOptions $options)
          {
              // Código para pegar o próximo job da fila
              $this->process($connectionName, $job, $options);
          }
          

          Dentro do método process(), o Laravel invoca o método fire(). Este método, por sua vez, chama o método handle() do Job, onde a lógica de processamento definida por você é executada.

            // vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php
            
            public function fire()
            {
                $payload = $this->payload();
                $this->instance = $this->resolve($payload);
                $this->instance->handle();
            }
            

            Neste ponto, o Job está sendo executado com base no código dentro do método handle().

            Tratamento de Exceções Durante o Processamento

            Quando uma exceção ocorre durante a execução do Job (no método handle()), o Laravel captura automaticamente essa exceção. o método process() captura essa exceção durante a execução, gerenciando-a dentro de um bloco try-catch.

            Entretanto, se o código lançar uma exceção, o método handleJobException a tratará. Ele pode reenfileirar o Job, dependendo do número de tentativas configuradas, ou marcá-lo como falhado.

            // vendor/laravel/framework/src/Illuminate/Queue/Worker.php
            
            public function process($connectionName, $job, WorkerOptions $options)
            {
                try {
                    $job->fire(); // Executa o Job
                } catch (Exception $e) {
                    // Captura a exceção e trata
                    $this->handleJobException($connectionName, $job, $options, $e);
                }
            }
            
            Tratamento de Exceção no Job

            Quando o método handle do Job lança uma exceção, o bloco catch no método process captura essa exceção. Em seguida, o método handleJobException lida com ela.

            try {
                // 
            } catch (Exception $e) {
                $this->handleJobException($connectionName, $job, $options, $e);
            }
            

              O Laravel oferece suporte para reexecutar o Job automaticamente. No entanto, se o limite de tentativas for atingido, o Job será marcado como falhado. Além disso, o método handleJobException marca o Job como falhado, se necessário.

              // vendor/laravel/framework/src/Illuminate/Queue/Worker.php
              
              protected function handleJobException($connectionName, $job, WorkerOptions $options, $e)
              {
                  $this->markJobAsFailedIfWillExceedMaxAttempts(
                      $connectionName, $job, $options->maxTries, $e
                  );
                  // Código para registrar a falha e disparar eventos
              }
              

              Então, esse método decide se o Job deve ser reenfileirado para novas tentativas ou marcado como falhado.

              Registro de Job que falhou

              Quando um Job falha após todas as tentativas configuradas, o Laravel registra essa falha na tabela failed_jobs. A tabela mantém um histórico dos Jobs que não conseguiram ser concluídos. Dessa forma, você pode inspecionar as falhas, e se necessário, reprocessar manualmente.

              O método failJob realiza o registro do Job na tabela failed_jobs:

              // vendor/laravel/framework/src/Illuminate/Queue/Worker.php
              
              protected function markJobAsFailedIfWillExceedMaxAttempts($connectionName, $job, $maxTries, $e)
              {
                  if ($job->attempts() >= $maxTries) {
                      $this->failJob($connectionName, $job, $e);
                  }
              }
              
              protected function failJob($connectionName, $job, $e)
              {
                  // Registra a falha na tabela `failed_jobs`
                  $this->fail($job, $e);
              }
              

              Além disso, o aquivo queue.php contem a configuração da tabela failed_jobs. Contudo, geralmente você define o driver no arquivo .env, como:

              QUEUE_CONNECTION=redis
              
              // config/queue.php
              'failed' => [
                  'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
                  'database' => 'mysql',
                  'table' => 'failed_jobs',
              ],
              
              Diagrama de classes: Jobs no Laravel
              Diagrama de classes: Jobs no Laravel

              Resumo

              • Criação do Job: Crie o arquivo Teste.php com o método handle.
              • Disparo do Job: Execute com Teste::dispatch().
              • Processamento pelo Worker: O comando queue:work chama handle em WorkCommand, que então invoca runNextJob no Worker. O Worker, por sua vez, chama process(), que executa o Job com fire() e aciona o método handle().
              • Tratamento de Exceções: Se ocorrer uma exceção, process() captura e trata no método handleJobException.
              • Registro de Falha: Caso o Job falhe após todas as tentativas, o método markJobAsFailedIfWillExceedMaxAttempts o registra na tabela de falhas.
              Diagrama de sequência: cliclo de vida Job no Laravel
              Diagrama de sequência: cliclo de vida Job no Laravel

              Conclusão

              Utilizar Jobs no Laravel melhora tanto a escalabilidade quanto a performance da aplicação, pois processa tarefas demoradas de forma assíncrona sem interferir no fluxo principal. Como resultado, a aplicação oferece uma experiência de usuário mais rápida e responsiva, mesmo em operações complexas.

              Além disso, o Laravel proporciona um sistema robusto para tratar exceções, monitorar e reenfileirar Jobs que falham, minimizando riscos e aumentando a confiabilidade. Dessa forma, o fluxo completo de Jobs assegura que a aplicação gerencie grandes volumes de tarefas sem comprometer o desempenho.

              Saiba Mais

              Se você deseja se aprofundar na configuração e no funcionamento de Jobs no Laravel, confira os seguintes recursos:

              Deixe um comentário

              O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

              Rolar para cima