Laravel retry_after vs timeout vs backoff: The Three Numbers
Pelanggan dicaj dua kali. E-mel laporan dihantar tiga kali. Log menunjukkan App\Jobs\SyncInvoice telah dicuba terlalu banyak kali walaupun job itu “hanya dijalankan sekali” setakat yang dapat dikenal pasti oleh sesiapa.
Apabila saya meneliti isu-isu ini, kod di dalam handle() hampir sentiasa bukan punca masalah. Masalahnya ialah tiga nombor yang orang anggap boleh ditukar ganti: retry_after, timeout, dan backoff. Ia berada dalam fail yang berbeza, dimiliki oleh bahagian Laravel yang berbeza, dan bermaksud perkara yang sama sekali berbeza.
Berikut ialah apa yang sebenarnya dikawal oleh setiap satu, dan satu peraturan yang menghalangnya daripada saling bercanggah.
1. retry_after : “Saya mengandaikan worker ini telah mati”
retry_after berada pada sambungan queue dalam config/queue.php :
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
Ia bukan kelewatan percubaan semula. Ia ialah tempoh sewa. Apabila worker memperuntukkan satu job, queue memberikannya retry_after saat. Jika job itu belum dipadamkan atau dilepaskan menjelang masa itu, queue mengandaikan worker itu terhenti dan menjadikan job itu tersedia sekali lagi, supaya worker lain boleh mengambilnya.
Perhatikan maksudnya: jika job anda sememangnya mengambil masa 120 saat dan retry_after ialah 90, worker kedua akan mengambil job yang sama pada saat ke-90 sementara worker pertama masih berjalan . Itu ialah caj berganda anda. Setiap pengambilan juga dikira sebagai percubaan, dan itulah cara anda berakhir dengan “dicuba terlalu banyak kali” pada job yang logik perniagaannya tidak pernah selesai.
(SQS tidak menggunakan retry_after ; timeout keterlihatannya memainkan peranan yang sama dan dikonfigurasikan dalam AWS.)
2. timeout : “bunuh job ini jika ia berjalan terlalu lama”
timeout dimiliki oleh proses worker , bukan sambungan. Anda menetapkannya pada arahan:
php artisan queue:work redis --timeout=60
atau mengikut job:
class SyncInvoice implements ShouldQueue
{
public $timeout = 60;
}
Apabila had dicapai, proses worker dibunuh (ini memerlukan extension pcntl PHP; tanpa extension itu, timeout diabaikan secara senyap). Job itu kemudian kembali melalui peraturan percubaan biasa. Jika anda lebih suka job yang timeout gagal serta-merta dan bukan dicuba semula, tetapkan:
public $failOnTimeout = true;
3. backoff : “tunggu sebelum percubaan seterusnya”
backoff ialah yang orang anggap sebagai retry_after. Ia ialah kelewatan sebelum job yang menimbulkan exception dicuba semula:
class SyncInvoice implements ShouldQueue
{
public $tries = 4;
// 10s, then 60s, then 5 minutes
public $backoff = [10, 60, 300];
}
Tanpa backoff, job yang gagal kerana API luaran sedang tidak berfungsi akan dicuba semula hampir serta-merta, menghabiskan semua percubaannya dalam beberapa saat, dan berakhir dalam failed_jobs sebelum API itu sempat pulih.
Peraturannya: timeout < retry_after
Dokumentasi Laravel menyatakan dengan jelas, dan inilah satu-satunya baris yang akan saya tatu pada setiap konfigurasi queue:
Nilai –timeout sentiasa perlu sekurang-kurangnya beberapa saat lebih rendah daripada nilai konfigurasi retry_after anda.
Susunan itu menjamin worker membunuh job yang tersekat sebelum queue menyerahkannya kepada pihak lain. Jika susunan itu terbalik, anda telah membina mesin pemprosesan berganda.
Titik permulaan yang munasabah untuk job yang biasanya selesai dalam masa kurang daripada 30 saat:
Jika anda mempunyai satu job yang benar-benar panjang (eksport besar, pemprosesan video), jangan menaikkan retry_after untuk semuanya. Letakkan job itu pada connection tersendiri dengan retry_after yang lebih besar, dan jalankan worker khusus dengan –timeout yang lebih besar dan sepadan.
Pengguna Horizon
Horizon menetapkan timeout worker bagi setiap supervisor dalam config/horizon.php:
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'timeout' => 60,
'tries' => 3,
],
Peraturan yang sama terpakai: timeout itu mesti kekal di bawah retry_after bagi connection yang digunakannya. Mudah untuk menaikkan satu dan melupakan yang lain semasa insiden.
Jadikan job selamat untuk dijalankan dua kali juga
Walaupun dengan angka yang sempurna, worker boleh mati selepas mengecas kad tetapi sebelum memadam job. Perlakukan “runs at least once” sebagai kontrak dan jadikan side effect idempotent:
public function handle(PaymentGateway $gateway): void
{
$invoice = Invoice::findOrFail($this->invoiceId);
if ($invoice->paid_at !== null) {
return; // already done on a previous attempt
}
$gateway->charge($invoice, idempotencyKey: "invoice-{$invoice->id}");
$invoice->update(['paid_at' => now()]);
}
Kebanyakan payment provider menerima idempotency key, yang menjadikan gateway itu sendiri menolak pengecasan kedua yang sama.
Audit 2 minit
Sebelum anda mengubah apa-apa yang lain, periksa perkara berikut mengikut urutan:
- Buka config/queue.php dan catatkan retry_after bagi connection yang sebenarnya anda gunakan. Sahkan bahawa QUEUE_CONNECTION dalam .env menunjuk kepada connection tersebut.
- Cari timeout efektif: $timeout bagi job jika ditetapkan, jika tidak, –timeout bagi worker (default 60), atau timeout bagi supervisor Horizon.
- Sahkan bahawa timeout sekurang-kurangnya beberapa saat lebih rendah daripada retry_after.
- Periksa bahawa job mempunyai $backoff jika ia membuat sebarang panggilan melalui network.
- Jalankan php -m | grep pcntl pada pelayan worker.
- Selepas mengubah config, jalankan php artisan queue:restart (atau horizon:terminate). Worker yang berjalan lama mengekalkan config dan code lama dalam memory sehingga ia dimulakan semula.
Yang terakhir itu sering memerangkap ramai orang: anda betulkan nombor, deploy, dan tiada apa-apa berubah kerana workers tidak pernah reload.
Apabila retries masih tidak berkelakuan dengan betul
Entri ini membincangkan mengapa jobs berjalan terlalu banyak kali. Masalah sebaliknya, iaitu job yang gagal, yang tidak retry langsung, mempunyai set punca yang berbeza: interaksi antara tries dan retryUntil(), exceptions ditelan di dalam handle(), queue:retry menghantar ke queue yang tidak didengari oleh mana-mana worker, dan keanehan Horizon. Saya telah menulisnya secara terperinci, dengan perbezaan Laravel 11 hingga 13, di sini: Laravel Queue Failed Job Retry Not Working: Causes & Fixes.
Dan jika anda perlu menghantar .env anda kepada rakan sepasukan semasa debugging tetapan queue, jangan tampal secrets mentah ke dalam chat. Pengesah keselamatan Laravel .env percuma di Blogs World menyembunyikan kata laluan dan kunci dalam pelayar (tiada apa-apa dimuat naik) dan menandakan risiko production seperti APP_DEBUG=true sepanjang proses tersebut.
Apakah bug duplicate-job paling teruk yang pernah anda hadapi? Saya ingin tahu sama ada puncanya nombor atau pemeriksaan idempotency yang tidak ada.
Sumber Rujukan:
- Artikel asal: Dev.to PHP
- Diterjemah daripada artikel asal ke Bahasa Melayu oleh AI Editorial – haqis.com