5.5 Jobs & Queues
Resumo
Jobs & Queues — sistema de execução assíncrona em background. Job é a classe com a lógica (SendEmail, ProcessFile). Queue (fila) é a fila de jobs.
Dispatch:
JobName::dispatch()manda para a queue. O worker (queue:work) processa os jobs.Importante:
ShouldQueuepara assíncrono,delay()para adiar,triespara retry. Job chains para sequência, batches para jobs em paralelo.
Conteúdo
O que é
O que é: Jobs — tarefas em background (enviar email, processar arquivo). Queues — filas de jobs para execução assíncrona.
O essencial:
- Job — tarefa em background
- Queue — fila de jobs
- Worker — processo que executa os jobs
Como funciona
Criar o Job:
php artisan make:job SendWelcomeEmailnamespace App\Jobs;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
// Tentativas
public $tries = 3;
// Timeout (segundos)
public $timeout = 60;
// Queue
public $queue = 'emails';
public function __construct(public User $user)
{
}
public function handle(): void
{
// Enviar email
$this->user->notify(new WelcomeNotification());
}
// Tratamento de erro
public function failed(\Throwable $exception): void
{
Log::error('Failed to send welcome email', [
'user_id' => $this->user->id,
'error' => $exception->getMessage(),
]);
}
}Dispatch (chamar) o Job:
use App\Jobs\SendWelcomeEmail;
// Mandar para a queue
SendWelcomeEmail::dispatch($user);
// Dispatch com delay
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(10));
// Dispatch em uma queue específica
SendWelcomeEmail::dispatch($user)->onQueue('emails');
// Dispatch síncrono (sem queue)
SendWelcomeEmail::dispatchSync($user);
// Dispatch depois do DB commit
SendWelcomeEmail::dispatch($user)->afterCommit();
// Dispatch se a condição for verdadeira
SendWelcomeEmail::dispatchIf($user->isActive(), $user);
SendWelcomeEmail::dispatchUnless($user->isBanned(), $user);Configurar as queues (.env):
# database/migrations/xxxx_create_jobs_table.php
php artisan queue:table
php artisan migrate
QUEUE_CONNECTION=database
# Ou Redis
QUEUE_CONNECTION=redis
# Ou Sync (sem queue, síncrono)
QUEUE_CONNECTION=syncRodar o Worker:
# Iniciar o worker (processa os jobs)
php artisan queue:work
# Com uma queue específica
php artisan queue:work --queue=emails,default
# Com timeout
php artisan queue:work --timeout=60
# Reinicia quando o código muda
php artisan queue:work --timeout=60 --tries=3
# Parar o worker depois do job atual
php artisan queue:restart
# Um job (para cron)
php artisan queue:work --onceQuando usar
Use Jobs quando:
- Operação longa (enviar email, processar arquivo)
- Não bloquear o HTTP response
- Tarefa pesada
- Integração com API externa
Não use quando:
- Operação simples e rápida
- Precisa do resultado na hora
Exemplo prático
Processar arquivo enviado:
// Job
namespace App\Jobs;
use App\Models\Upload;
use Illuminate\Support\Facades\Storage;
class ProcessUploadedFile implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 300; // 5 minutos
public function __construct(public Upload $upload)
{
}
public function handle(): void
{
// Pegar o arquivo do storage
$filePath = $this->upload->file_path;
$content = Storage::get($filePath);
// Processar o arquivo
$processedData = $this->process($content);
// Salvar o resultado
Storage::put(
str_replace('.csv', '_processed.csv', $filePath),
$processedData
);
// Atualizar o status
$this->upload->update([
'status' => 'completed',
'processed_at' => now(),
]);
}
private function process(string $content): string
{
// Lógica de processamento
return $content;
}
public function failed(\Throwable $exception): void
{
$this->upload->update([
'status' => 'failed',
'error' => $exception->getMessage(),
]);
}
}
// Controller
class UploadController extends Controller
{
public function store(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:csv|max:10240',
]);
// Salvar o arquivo
$path = $request->file('file')->store('uploads');
// Criar o registro
$upload = Upload::create([
'user_id' => $request->user()->id,
'file_path' => $path,
'status' => 'pending',
]);
// Mandar para a queue
ProcessUploadedFile::dispatch($upload);
return response()->json([
'message' => 'Arquivo enviado, processamento iniciado',
'upload_id' => $upload->id,
], 202);
}
}Job Chains (cadeia de jobs):
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessUploadedFile($upload),
new GenerateReport($upload),
new SendReportEmail($upload),
])->dispatch();
// Com tratamento de erro
Bus::chain([
new ProcessUploadedFile($upload),
new GenerateReport($upload),
])->catch(function (\Throwable $e) {
// Roda se qualquer job falhar
Log::error('Job chain failed', ['error' => $e->getMessage()]);
})->dispatch();Job Batches (lote de jobs):
use Illuminate\Support\Facades\Bus;
// Criar o batch
$batch = Bus::batch([
new ProcessUser($user1),
new ProcessUser($user2),
new ProcessUser($user3),
])->then(function () {
// Todos os jobs terminaram
Log::info('All users processed');
})->catch(function () {
// Um dos jobs falhou
})->finally(function () {
// Sempre roda
})->dispatch();
// Checar o status do batch
$batch = Bus::findBatch($batchId);
$batch->finished(); // Todos concluídos
$batch->cancelled(); // Cancelado
$batch->totalJobs; // Total de jobs
$batch->processedJobs(); // Processados
$batch->pendingJobs; // PendentesRate Limiting (limite de frequência):
use Illuminate\Support\Facades\RateLimiter;
class ProcessApiRequest implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
// No máximo 10 jobs por minuto
RateLimiter::attempt(
'api-requests',
$perMinute = 10,
function () {
// Lógica da request
Http::get('https://api.example.com');
}
);
}
// Ou via middleware
public function middleware(): array
{
return [new RateLimited('api-requests')];
}
}Unique Jobs (evitar duplicata):
use Illuminate\Contracts\Queue\ShouldBeUnique;
class ProcessOrder implements ShouldQueue, ShouldBeUnique
{
public function __construct(public Order $order)
{
}
// Chave única (um job por order)
public function uniqueId(): string
{
return $this->order->id;
}
// Tempo de unicidade (segundos)
public $uniqueFor = 3600; // 1 hora
public function handle(): void
{
// Processar o pedido
}
}Job Middleware:
namespace App\Jobs\Middleware;
class RateLimited
{
public function handle($job, $next)
{
RateLimiter::attempt(
'process-orders',
$perMinute = 10,
function () use ($job, $next) {
$next($job);
},
$decaySeconds = 60
);
}
}
// Uso no Job
public function middleware(): array
{
return [new RateLimited()];
}Failed Jobs (tratar os que falharam):
# Criar a tabela de failed jobs
php artisan queue:failed-table
php artisan migrate
# Ver os jobs que falharam
php artisan queue:failed
# Retentar o job
php artisan queue:retry {id}
# Retentar todos
php artisan queue:retry all
# Remover o job que falhou
php artisan queue:forget {id}
# Limpar todos os failed
php artisan queue:flushMonitorar as queues:
// No AppServiceProvider
use Illuminate\Support\Facades\Queue;
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
// Antes de executar o job
Log::info('Job starting', [
'job' => $event->job->resolveName(),
]);
});
Queue::after(function (JobProcessed $event) {
// Depois de executar o job
Log::info('Job completed', [
'job' => $event->job->resolveName(),
]);
});
Queue::failing(function (JobFailed $event) {
// O job falhou
Log::error('Job failed', [
'job' => $event->job->resolveName(),
'exception' => $event->exception->getMessage(),
]);
});
}Supervisor (em production):
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/path/to/worker.log
stopwaitsecs=3600# Reiniciar o supervisor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*Na entrevista
Resposta estruturada:
O que é:
- Jobs — tarefas em background (SendEmail, ProcessFile)
- Implementam
ShouldQueuepara rodar assíncrono - O worker processa via
queue:work
Dispatch:
JobName::dispatch($data); // Na queue
JobName::dispatch($data)->delay(10); // Com delay
JobName::dispatchSync($data); // SíncronoConfiguração:
- Drivers:
database,redis,sync tries— número de tentativastimeout— tempo máximo de execuçãoqueue— nome da queue
Avançado:
- Job Chains — execução em sequência:
Bus::chain([Job1, Job2]) - Job Batches — jobs em paralelo:
Bus::batch([...]) - ShouldBeUnique — evita duplicata
- Rate Limiting — limite de frequência
- Supervisor — em production
- Failed Jobs —
queue:failed,queue:retry
Exercícios práticos
Exercício 1: Job com retry e timeout
Crie um ProcessVideoJob que processa vídeo. 3 tentativas, timeout de 5 minutos, queue videos. Se falhar, grave o log no banco.
Solução
namespace App\Jobs;
use App\Models\Video;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ProcessVideoJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 300; // 5 minutos
public $queue = 'videos';
public function __construct(public Video $video)
{
}
public function handle(): void
{
$this->video->update(['status' => 'processing']);
// Processar o vídeo (ex.: conversão)
$inputPath = Storage::path($this->video->original_path);
$outputPath = Storage::path($this->video->processed_path);
// Aqui entra a lógica de processar o vídeo
// exec("ffmpeg -i {$inputPath} {$outputPath}");
$this->video->update([
'status' => 'completed',
'processed_at' => now(),
]);
}
public function failed(\Throwable $exception): void
{
$this->video->update([
'status' => 'failed',
'error_message' => $exception->getMessage(),
]);
Log::error('Video processing failed', [
'video_id' => $this->video->id,
'error' => $exception->getMessage(),
'attempts' => $this->attempts(),
]);
}
}
// Uso
ProcessVideoJob::dispatch($video);Exercício 2: Job Chain para upload de arquivo
Crie a chain: DownloadFileJob → ProcessFileJob → NotifyUserJob. Se qualquer passo falhar, envie email para o admin.
Solução
namespace App\Jobs;
use App\Models\Import;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
// Job 1: Baixar o arquivo
class DownloadFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Import $import, public string $url)
{
}
public function handle(): void
{
$content = Http::get($this->url)->body();
Storage::put($this->import->file_path, $content);
$this->import->update(['status' => 'downloaded']);
}
}
// Job 2: Processar o arquivo
class ProcessFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Import $import)
{
}
public function handle(): void
{
$content = Storage::get($this->import->file_path);
$rows = array_map('str_getcsv', explode("\n", $content));
// Processar as linhas
foreach ($rows as $row) {
// Lógica de importação
}
$this->import->update([
'status' => 'processed',
'rows_count' => count($rows),
]);
}
}
// Job 3: Notificar o usuário
class NotifyUserJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Import $import)
{
}
public function handle(): void
{
$this->import->user->notify(
new ImportCompletedNotification($this->import)
);
}
}
// Disparar a chain
Bus::chain([
new DownloadFileJob($import, $url),
new ProcessFileJob($import),
new NotifyUserJob($import),
])->catch(function (\Throwable $e) use ($import) {
Log::error('Import chain failed', [
'import_id' => $import->id,
'error' => $e->getMessage(),
]);
Mail::to(config('app.admin_email'))->send(
new ImportFailedMail($import, $e)
);
})->dispatch();Exercício 3: Unique Job para export
Crie um ExportUsersJob que só pode rodar 1 vez por hora para cada usuário. Se o job já está na queue, não adicione outro.
Solução
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class ExportUsersJob implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600; // 10 minutos
public $tries = 2;
// Unicidade por 1 hora
public $uniqueFor = 3600;
public function __construct(public User $requestedBy)
{
}
// Chave única (um export por usuário)
public function uniqueId(): string
{
return "export-users-{$this->requestedBy->id}";
}
public function handle(): void
{
$users = User::with('profile')->get();
$csv = "ID,Nome,Email,Criado em\n";
foreach ($users as $user) {
$csv .= "{$user->id},{$user->name},{$user->email},{$user->created_at}\n";
}
$filename = "exports/users-" . now()->format('Y-m-d-His') . ".csv";
Storage::put($filename, $csv);
$this->requestedBy->notify(
new ExportReadyNotification($filename)
);
}
}
// No controller
public function export(Request $request)
{
// Tenta enfileirar
ExportUsersJob::dispatch($request->user());
return response()->json([
'message' => 'Export iniciado. Você será notificado quando estiver pronto.',
], 202);
}
// Se o job já está na queue, não entra de novoParte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate