· Admin

Docker'da Laravel Queue: Job Oluşturma, Worker Yönetimi ve Hata Kontrolü

Önceki yazıda Docker'da scheduler kurmuştuk. Bu yazıda queue sistemini ele alacağız — ağır işleri arka plana atıp kullanıcıyı bekletmemek için.

PDF oluşturma, mail gönderme, resim işleme, API çağrısı — bunları request içinde yapmak kullanıcıyı saniyeler boyunca bekletir. Queue ile bu işleri arka planda çalışan worker'lara devredirsiniz. Kullanıcı "işleminiz alındı" mesajını görür, iş arka planda tamamlanır.

Queue Nedir, Neden Lazım?

Şöyle düşünün: bir restoranda garson siparişi alır, mutfağa iletir ve hemen diğer masaya geçer. Yemeğin pişmesini beklemez. Queue da aynı mantık — request gelir, iş kuyruğa atılır, response hemen döner. Worker (aşçı) kuyruktaki işleri sırayla halleder.

Tipik queue kullanım alanları:

  • Mail gönderimi — SMTP bağlantısı 2-5 saniye sürebilir
  • PDF/Excel oluşturma — CPU yoğun işlem
  • Resim işleme — Thumbnail oluşturma, boyutlandırma
  • Dış API çağrıları — Ödeme, kargo, bildirim servisleri
  • Toplu veri işleme — Import, export, rapor oluşturma

Job Oluşturma

php artisan make:job ProcessOrderJob
namespace App\Jobs;

use App\Mail\OrderConfirmation;
use App\Models\Order;
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\Mail;

class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 30;
    public int $timeout = 120;

    public function __construct(
        private Order $order
    ) {}

    public function handle(): void
    {
        // Fatura PDF oluştur
        $pdf = $this->order->generateInvoicePdf();

        // Müşteriye onay maili at
        Mail::to($this->order->user)
            ->send(new OrderConfirmation($this->order, $pdf));

        // Siparişi "işlendi" olarak güncelle
        $this->order->update(['processed_at' => now()]);
    }

    public function failed(\Throwable $exception): void
    {
        // Job başarısız olduğunda çalışır
        logger()->error('Sipariş işlenemedi', [
            'order_id' => $this->order->id,
            'error' => $exception->getMessage(),
        ]);

        // Admin'e bildirim at
        $this->order->user->notify(new OrderProcessingFailed($this->order));
    }
}

Property'leri açıklayalım:

  • $tries = 3 — Başarısız olursa 3 kez dener
  • $backoff = 30 — Her deneme arasında 30 saniye bekler
  • $timeout = 120 — 2 dakikadan uzun sürerse timeout olur
  • failed() metodu — Tüm denemeler başarısız olursa çalışır

Job'ı Dispatch Etmek

// Controller'dan
public function store(OrderRequest $request)
{
    $order = Order::create($request->validated());

    ProcessOrderJob::dispatch($order);

    return redirect()->route('orders.show', $order)
        ->with('success', 'Siparişiniz alındı, işleniyor.');
}

// Gecikmeli dispatch — 5 dakika sonra çalışsın
ProcessOrderJob::dispatch($order)->delay(now()->addMinutes(5));

// Belirli bir queue'ya gönder
ProcessOrderJob::dispatch($order)->onQueue('orders');

// Zincir halinde — sırayla çalışsın
Bus::chain([
    new ProcessOrderJob($order),
    new SendInvoiceJob($order),
    new NotifyWarehouseJob($order),
])->dispatch();

Bus::chain() çok kullanışlı — ilk job başarılı olursa ikinci çalışır, ikinci başarılı olursa üçüncü. Birisi başarısız olursa zincir durur.

Queue Driver Seçimi

.env'de queue driver'ı ayarlarsınız:

QUEUE_CONNECTION=redis
Driver Ne Zaman?
sync Development — job anında çalışır, debug kolay
database Küçük projeler — Redis kurmak istemiyorsanız
redis Production — hızlı, güvenilir, önerilen
sqs AWS kullanan büyük projeler

database driver için migration gerekiyor:

php artisan queue:table
php artisan migrate

Redis tercih ediyorum çünkü database'e ek yük bindirmiyor ve çok daha hızlı.

Docker Yapılandırması

docker-compose.yml

services:
  app:
    build: .
    volumes:
      - .:/var/www/html
    depends_on:
      - db
      - redis

  nginx:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - .:/var/www/html
      - ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - app

  queue:
    build: .
    volumes:
      - .:/var/www/html
    command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
    restart: unless-stopped
    depends_on:
      - db
      - redis

  queue-orders:
    build: .
    volumes:
      - .:/var/www/html
    command: php artisan queue:work redis --queue=orders --sleep=3 --tries=3
    restart: unless-stopped
    depends_on:
      - db
      - redis

  scheduler:
    build: .
    volumes:
      - .:/var/www/html
    command: >
      sh -c "while true; do
        php artisan schedule:run --verbose --no-interaction;
        sleep 60;
      done"
    depends_on:
      - db
      - redis

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secret
    volumes:
      - dbdata:/var/lib/mysql

  redis:
    image: redis:alpine

volumes:
  dbdata:

İki ayrı queue worker var — biri default queue'yu, biri orders queue'sunu dinliyor. Siparişlerin ayrı worker'da çalışması demek: mail gönderimi yoğunluğu sipariş işlemeyi yavaşlatmaz.

Worker Argümanları

php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
  • --sleep=3 — Queue boşken 3 saniye bekle (CPU israfını önler)
  • --tries=3 — Her job'ı 3 kez dene
  • --max-time=3600 — 1 saat sonra worker'ı yeniden başlat (memory leak önlemi)
  • restart: unless-stopped — Worker çökerse Docker otomatik yeniden başlatır

--max-time yerine --max-jobs=1000 da kullanabilirsiniz — 1000 job işledikten sonra yeniden başla. Amaç aynı: uzun süre çalışan PHP process'in bellek sızıntısını önlemek.

Failed Jobs

Her şey her zaman çalışmaz. SMTP sunucusu erişilemez, API timeout olur, disk dolar. Failed job'ları yönetmek şart.

Migration

php artisan make:queue-failed-table
php artisan migrate

Kontrol Komutları

# Başarısız job'ları listele
php artisan queue:failed

# Belirli bir job'ı tekrar dene
php artisan queue:retry 5

# Tüm başarısız job'ları tekrar dene
php artisan queue:retry all

# Belirli bir job'ı sil
php artisan queue:forget 5

# Tüm başarısız job'ları temizle
php artisan queue:flush

Otomatik Temizlik (Scheduler ile)

// routes/console.php
Schedule::command('queue:prune-failed --hours=168')->daily(); // 7 günden eski failed job'ları sil

Job Batching — Toplu İşlemler

Laravel 8'den beri gelen batch özelliği, birden fazla job'ı grup halinde çalıştırıp topluca izlemenizi sağlar:

php artisan make:queue-batches-table
php artisan migrate
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$users = User::where('newsletter', true)->get();

$jobs = $users->map(fn ($user) => new SendNewsletterJob($user));

$batch = Bus::batch($jobs->toArray())
    ->then(function (Batch $batch) {
        logger()->info("Newsletter gönderildi: {$batch->totalJobs} kullanıcı");
    })
    ->catch(function (Batch $batch, \Throwable $e) {
        logger()->error("Newsletter hatası: {$e->getMessage()}");
    })
    ->finally(function (Batch $batch) {
        // Başarılı veya başarısız, her durumda çalışır
    })
    ->allowFailures()
    ->dispatch();

// İlerlemeyi kontrol et
$batch = Bus::findBatch($batchId);
echo $batch->progress(); // 0-100

10.000 kullanıcıya newsletter göndermek istiyorsanız, her biri için ayrı job oluşturup batch'e atarsınız. Batch'in ilerleme durumunu frontend'de gösterebilirsiniz.

Horizon — Queue Monitoring

Production'da queue'larınızı izlemek için Laravel Horizon vazgeçilmez:

composer require laravel/horizon
php artisan horizon:install

Docker'da Horizon çalıştırmak:

  horizon:
    build: .
    volumes:
      - .:/var/www/html
    command: php artisan horizon
    restart: unless-stopped
    depends_on:
      - db
      - redis

Horizon, queue:work yerine geçer — worker'ları kendisi yönetir. Dashboard'dan job'ları, failed job'ları, throughput'u, wait time'ı görebilirsiniz. /horizon adresinden erişilir.

Horizon kullanıyorsanız queue ve queue-orders container'larını kaldırın, tek horizon container'ı yeterli.

Development'ta Queue

Local development'ta iki seçenek:

1. Sync driver — Job anında çalışır, queue'ya gitmez:

QUEUE_CONNECTION=sync

Debug için en kolay yol. Ama gerçek queue davranışını test edemezsiniz.

2. docker-compose ile — Gerçek queue environment'ı:

docker-compose up -d

Production'a en yakın test ortamı. Önerdiğim yaklaşım bu.

Test Yazma

use Illuminate\Support\Facades\Queue;

public function test_order_dispatches_processing_job(): void
{
    Queue::fake();

    $order = Order::factory()->create();

    $this->postJson('/api/orders', $order->toArray());

    Queue::assertPushed(ProcessOrderJob::class, function ($job) use ($order) {
        return $job->order->id === $order->id;
    });
}

public function test_order_job_sends_confirmation_email(): void
{
    Mail::fake();

    $order = Order::factory()->create();

    (new ProcessOrderJob($order))->handle();

    Mail::assertSent(OrderConfirmation::class, function ($mail) use ($order) {
        return $mail->hasTo($order->user->email);
    });
}

İlk test job'ın dispatch edildiğini kontrol eder (Queue::fake ile gerçek çalıştırmadan). İkinci test job'ın handle metodunu direkt çağırıp mail gönderildiğini kontrol eder.

Checklist

Production'a çıkmadan önce:

  • QUEUE_CONNECTION=redis (sync değil!)
  • Failed jobs tablosu var mı?
  • Worker container'da restart: unless-stopped var mı?
  • --max-time veya --max-jobs ile memory leak önlemi var mı?
  • failed() metodu job'larda tanımlı mı?
  • Failed job temizliği scheduler'da var mı?
  • Horizon veya başka bir monitoring var mı?
  • Job timeout'ları makul mü? (default 60 saniye)

Queue sistemi doğru kurulduğunda uygulamanız çok daha hızlı ve dayanıklı olur. Kullanıcı beklemez, işler sırayla ve güvenle tamamlanır, hata olursa tekrar denenir. Docker ile bu yapıyı kurmak da sadece bir command satırı — geri kalanını Laravel hallediyor.