Task scheduling com Laravel Scheduler

1. Introdução ao Agendamento de Tarefas no Laravel

1.1. O problema do cron tradicional vs. Laravel Scheduler

Gerenciar tarefas agendadas em servidores web sempre foi um desafio. No modelo tradicional, cada tarefa exige uma entrada separada no crontab do servidor, o que rapidamente se torna um pesadelo de manutenção. Você precisa acessar o servidor, editar arquivos de configuração e lidar com sintaxes diferentes entre distribuições Linux.

O Laravel Scheduler resolve esse problema de forma elegante. Com ele, você define todas as suas tarefas agendadas em um único arquivo PHP, usando a sintaxe familiar do framework. Uma única entrada no cron do servidor é suficiente para gerenciar dezenas ou centenas de tarefas.

1.2. Como o Scheduler funciona por baixo dos panos

O coração do Scheduler é o comando php artisan schedule:run. Quando o cron do servidor executa esse comando a cada minuto, o Laravel verifica todas as tarefas registradas e executa aquelas cuja frequência coincide com o horário atual. Isso significa que a precisão máxima é de um minuto — suficiente para a grande maioria dos casos de uso.

1.3. Pré-requisitos: configurar o cron no servidor

Para começar, adicione esta única linha ao crontab do seu servidor:

* * * * * cd /caminho/para/seu/projeto && php artisan schedule:run >> /dev/null 2>&1

Esse comando executa o agendador a cada minuto. O redirecionamento para /dev/null evita que logs desnecessários poluam o terminal.

2. Definindo Tarefas Agendadas: Sintaxe e Frequências

2.1. Estrutura do método schedule() no App\Console\Kernel

Toda a configuração das tarefas acontece no arquivo app/Console/Kernel.php, dentro do método schedule():

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        // Suas tarefas aqui
    }
}

2.2. Frequências comuns

O Laravel oferece uma API fluente para definir frequências:

$schedule->command('emails:send')->daily();           // Executa uma vez por dia à meia-noite
$schedule->command('reports:generate')->hourly();     // Executa no início de cada hora
$schedule->command('cache:clear')->everyMinute();     // Executa a cada minuto
$schedule->command('backup:run')->cron('0 2 * * 0'); // Domingo às 2h da manhã

2.3. Executando comandos Artisan, closures e scripts externos

O Scheduler aceita três tipos de tarefas:

// Comando Artisan
$schedule->command('inspire')->daily();

// Closure (cuidado: não tem acesso ao container completo)
$schedule->call(function () {
    \Log::info('Tarefa executada manualmente');
})->everyMinute();

// Script externo
$schedule->exec('/usr/bin/python3 /scripts/cleanup.py')->daily();

3. Condições e Restrições de Execução

3.1. Métodos condicionais

Use when() para executar apenas se uma condição for verdadeira, e skip() para pular quando necessário:

$schedule->command('reports:generate')
    ->daily()
    ->when(function () {
        return \App\Models\Order::whereDate('created_at', today())->count() > 0;
    });

$schedule->command('emails:reminder')
    ->hourly()
    ->skip(function () {
        return today()->isWeekend();
    });

3.2. Restrições de ambiente

Controle em quais ambientes a tarefa deve rodar:

$schedule->command('backup:database')
    ->daily()
    ->environments('production');

$schedule->command('maintenance:check')
    ->hourly()
    ->between('8:00', '18:00')
    ->unlessBetween('12:00', '13:00');

3.3. Evitando sobreposição

Para tarefas que podem demorar mais que o intervalo entre execuções:

$schedule->command('backup:full')
    ->daily()
    ->withoutOverlapping(60); // Timeout de 60 minutos

4. Saída, Logs e Notificações de Tarefas

4.1. Redirecionando saída

$schedule->command('reports:export')
    ->daily()
    ->sendOutputTo(storage_path('logs/reports.log'))
    ->appendOutputTo(storage_path('logs/reports_history.log'));

4.2. Enviando resultados por e-mail

$schedule->command('orders:summary')
    ->daily()
    ->emailOutputTo('admin@exemplo.com');

4.3. Notificações de falha

$schedule->command('health:check')
    ->everyMinute()
    ->pingBefore('https://health.example.com/start')
    ->thenPing('https://health.example.com/success');

5. Tarefas em Background e Execução Assíncrona

5.1. Executando tarefas em fila

$schedule->job(new \App\Jobs\ProcessOrders)
    ->hourly()
    ->onQueue('high')
    ->onConnection('redis');

5.2. Comandos longos e execução em background

$schedule->command('reports:heavy')
    ->daily()
    ->runInBackground(); // Libera o scheduler imediatamente

5.3. Diferenças entre execução síncrona e assíncrona

Na execução síncrona (padrão), o scheduler espera a tarefa terminar antes de processar a próxima. Com runInBackground(), a tarefa é enviada para execução em segundo plano, permitindo que outras tarefas sejam processadas simultaneamente.

6. Manutenção, Monitoramento e Boas Práticas

6.1. Verificando tarefas agendadas

php artisan schedule:list

Esse comando mostra todas as tarefas registradas, suas frequências e próximas execuções.

6.2. Testando tarefas localmente

php artisan schedule:work

Esse comando executa o scheduler continuamente no terminal, ideal para testes em desenvolvimento.

6.3. Boas práticas

  • Idempotência: Garanta que executar a mesma tarefa múltiplas vezes não cause efeitos colaterais
  • Tratamento de erros: Use try-catch nas closures e comandos
  • Logs estruturados: Inclua identificadores únicos nas tarefas para facilitar debugging
$schedule->command('cleanup:temp')
    ->daily()
    ->onSuccess(function () {
        \Log::info('Limpeza concluída com sucesso');
    })
    ->onFailure(function () {
        \Log::error('Falha na limpeza');
    });

7. Casos de Uso Avançados e Integrações

7.1. Agendamento dinâmico com banco de dados

protected function schedule(Schedule $schedule)
{
    $tasks = \App\Models\ScheduledTask::where('active', true)->get();

    foreach ($tasks as $task) {
        $schedule->command($task->command)
            ->cron($task->cron_expression)
            ->withoutOverlapping();
    }
}

7.2. Integração com filas Redis e Horizon

$schedule->command('process:notifications')
    ->everyMinute()
    ->withoutOverlapping()
    ->onConnection('redis')
    ->onQueue('notifications');

7.3. Exemplo completo

protected function schedule(Schedule $schedule)
{
    // Backup automático do banco
    $schedule->command('backup:database')
        ->dailyAt('02:00')
        ->environments('production')
        ->withoutOverlapping()
        ->sendOutputTo(storage_path('logs/backup.log'));

    // Limpeza de cache
    $schedule->command('cache:clear')
        ->daily()
        ->appendOutputTo(storage_path('logs/cache_clear.log'));

    // Envio de relatórios semanais
    $schedule->command('reports:weekly')
        ->weekly()
        ->mondays()
        ->at('09:00')
        ->emailOutputTo('equipe@exemplo.com');

    // Verificação de saúde do sistema
    $schedule->command('health:check')
        ->everyFiveMinutes()
        ->thenPing('https://monitor.exemplo.com/healthy');
}

Referências