Graceful Shutdown untuk PHP Worker Jangka Panjang - Selesaikan Job Aktif Sebelum Keluar
Sebuah queue worker sedang menyelesaikan separuh proses pengiriman invoice, pembuatan thumbnail, atau pembaruan search index ketika deployment memintanya berhenti. Keluar seketika dapat meninggalkan job setelah sebagian efeknya telanjur terjadi. Mengabaikan permintaan berhenti dapat menunda deployment sampai supervisor kehilangan kesabaran. Seperti apa jalan tengah yang terkendali untuk PHP CLI worker kecil?
Jawaban yang berguna lebih sempit daripada "jangan pernah kehilangan job". Sebuah proses dapat bekerja sama dengan permintaan terminasi normal, tetapi tidak dapat menghilangkan crash, listrik padam, atau terminasi paksa. Karena itu, graceful shutdown merupakan sebuah protokol: terima permintaan, berhenti mengambil pekerjaan baru, capai batas job yang telah ditentukan, lalu keluar sebelum deadline yang terbatas. Pemulihan queue dan idempotency tetap harus menangani keadaan ketika protokol itu tidak dapat selesai.
Tentukan batasnya sebelum menangani signal
Bayangkan worker yang berulang kali mengambil satu job, menjalankan efeknya, lalu mengakui penyelesaiannya. Tempat berhenti yang paling aman biasanya berada di antara dua job, bukan di tengah salah satunya. Signal terminasi seharusnya mengubah apa yang dilakukan loop berikutnya; signal itu sebaiknya tidak mengubah signal handler menjadi jalur eksekusi kedua yang menutup koneksi database, mengedit status queue, dan menulis beberapa record log.
Untuk artikel ini, graceful stop berarti:
- jangan mengambil job lain setelah permintaan berhenti diterima;
- biarkan job yang sedang berjalan mencapai jalur penyelesaian atau kegagalan normalnya;
- akui pekerjaan hanya sesuai aturan queue yang sudah ditetapkan;
- keluar dalam batas waktu berhenti milik supervisor.
Definisi itu langsung memperlihatkan satu batas penting. Jika sebuah job dapat terus terblokir pada network call, "selesaikan job saat ini" juga dapat berarti "tidak pernah berhenti". Graceful shutdown membutuhkan pekerjaan yang terbatas: timeout koneksi dan request, durasi maksimum job bila memungkinkan, serta deadline supervisor yang mencerminkan kenyataan, bukan harapan.
Biarkan handler meminta proses berhenti
Dokumentasi Process Control PHP menjelaskan PCNTL sebagai process control bergaya Unix dan memperingatkan agar tidak menggunakannya di lingkungan web server. Extension ini juga tidak tersedia di Windows. Pola ini ditujukan untuk proses PHP CLI yang berjalan lama pada sistem mirip Unix, bukan untuk request PHP-FPM biasa.
Mekanisme intinya dapat tetap ringkas:
<?php
declare(strict_types=1);
if (PHP_SAPI !== 'cli' || !extension_loaded('pcntl')) {
fwrite(STDERR, "This worker requires PHP CLI with PCNTL.\n");
exit(1);
}
$stopRequested = false;
pcntl_async_signals(true);
$requestStop = static function () use (&$stopRequested): void {
$stopRequested = true;
};
pcntl_signal(SIGTERM, $requestStop);
pcntl_signal(SIGINT, $requestStop);
while (!$stopRequested) {
$job = reserveNextJob();
if ($job === null) {
sleep(1);
continue;
}
if ($stopRequested) {
releaseJob($job);
break;
}
try {
processJob($job);
acknowledgeJob($job);
} catch (Throwable $error) {
recordJobFailure($job, $error);
}
}
fwrite(STDOUT, "Worker stopped cleanly.\n");
reserveNextJob(), processJob(), dan fungsi queue tersebut sengaja berupa placeholder. Implementasi yang benar bergantung pada apakah queue menggunakan MariaDB, Redis, RabbitMQ, atau sistem lain. Klaim contoh ini hanya menyangkut control flow: handler mengubah sebuah flag, iterasi yang aktif tetap mengikuti jalur keberhasilan atau kegagalan normalnya, dan loop memeriksa flag sebelum mengambil pekerjaan lagi.
Pemeriksaan kedua setelah reserveNextJob() menutup celah praktis lainnya. Signal dapat tiba ketika pemanggilan reservasi sedang menunggu. Jika pemanggilan itu kemudian mengembalikan sebuah job, job tersebut belum dimulai, sehingga contoh ini melepasnya lalu keluar. Queue nyata harus membuat releaseJob() aman, atau menggunakan lease yang membuat reservasi terlantar kembali layak diambil. Tetap tidak ada instruksi ajaib yang membuat pengiriman signal dan status queue menjadi satu kejadian atomik; model pemulihan queue masih sangat penting.
Dokumentasi resmi pcntl_async_signals() menyatakan bahwa argumen true mengaktifkan penanganan signal asynchronous. Dokumentasi pcntl_signal() menyebut asynchronous dispatch sebagai metode dispatch yang direkomendasikan. Dokumentasi itu juga mencatat bahwa blocking function seperti sleep() dapat terinterupsi. Secara lebih umum, Linux menjelaskan bahwa sebagian blocked call dimulai ulang setelah handler selesai, sementara yang lain berhenti lebih awal dengan EINTR, bergantung pada interface dan flag signal handler. Worker yang memakai socket, subprocess, atau client library perlu diuji dengan call yang benar-benar digunakan; flag boolean tidak membuat setiap dependency dapat diinterupsi dengan segera.
Pahami apa yang dilakukan systemd saat berhenti
Service minimal dapat menjalankan worker sebagai proses utamanya:
[Unit]
Description=Example PHP queue worker
After=network.target
[Service]
Type=exec
User=app-worker
WorkingDirectory=/srv/example-app
ExecStart=/usr/bin/php /srv/example-app/bin/worker.php
Restart=on-failure
RestartSec=5s
TimeoutStopSec=45s
[Install]
WantedBy=multi-user.target
Path, account, restart policy, dan deadline 45 detik tersebut merupakan contoh, bukan default universal. Account harus tersedia dan hanya memiliki akses ke queue, aplikasi, dan filesystem yang memang dibutuhkan worker. Deadline sebaiknya lebih panjang daripada waktu yang ditargetkan untuk menyelesaikan atau menggagalkan satu job terbatas dengan aman, tetapi tetap memberi batas nyata pada deployment dan shutdown.
Manual resmi systemd.kill menyatakan bahwa signal awal untuk menghentikan service secara default adalah SIGTERM. Dalam prosedur berhenti normal, systemd menunggu service keluar. Dokumentasi TimeoutStopSec= menjelaskan bahwa service yang masih berjalan setelah deadline secara default dihentikan secara paksa dengan SIGKILL.
Hal ini membentuk urutan yang konkret:
systemctl stopatau restart membuat systemd mengirimSIGTERM.- Handler PHP mengubah
$stopRequestedmenjaditrue. - Job aktif mengikuti jalur penyelesaian atau kegagalan normalnya.
- Loop melihat flag sebelum mengambil job lain, lalu keluar.
- Jika proses melewati deadline, systemd biasanya meningkatkan terminasi menjadi
SIGKILL.
Manual signal(7) Linux menyatakan bahwa SIGKILL tidak dapat ditangkap, diblokir, atau diabaikan. Tidak ada shutdown handler yang dapat melakukan cleanup setelah signal itu. Menonaktifkan final kill milik systemd tidak menyelesaikan masalah dasarnya; tindakan tersebut justru dapat meninggalkan service yang menolak berhenti. Pertanyaan yang lebih baik adalah mengapa worker tidak dapat mencapai batasnya sebelum deadline.
Pisahkan kebenaran queue dari masa hidup proses
Menyelesaikan sebuah fungsi PHP bukanlah bukti bahwa job diproses tepat satu kali. Bayangkan job yang menagih melalui API eksternal lalu mengakui record queue. Proses dapat menghilang setelah penagihan berhasil tetapi sebelum acknowledgment di-commit. Saat pemulihan, queue mungkin menyajikan job itu lagi. Membalik urutannya juga tidak lebih aman: acknowledgment lebih dulu dapat menghilangkan pekerjaan jika proses mati sebelum efek eksternal terjadi.
Kontrol yang tepat merupakan bagian dari desain job dan queue:
- Reservation atau lease: pekerjaan yang telah diambil tersedia lagi ketika worker menghilang atau lease-nya kedaluwarsa.
- Acknowledgment setelah hasil yang ditentukan: queue menandai penyelesaian hanya pada batas yang dipilih.
- Idempotency: mengulangi job logis yang sama tidak menggandakan efek yang tidak dapat dibatalkan, sering kali dengan operation key stabil atau uniqueness rule.
- Local state yang atomik: perubahan database terkait di-commit bersama ketika satu transaction database benar-benar dapat mencakupnya.
- Attempt record dan observability: operator dapat membedakan pekerjaan pending, aktif, selesai, gagal, dan kedaluwarsa.
Broker yang berbeda menyediakan primitive yang berbeda, sehingga artikel ini tidak dapat secara jujur menetapkan satu acknowledgment command. Pelajaran umumnya adalah bahwa signal mengoordinasikan shutdown proses; signal bukan transaction protocol. Penanganan yang graceful mengurangi interupsi yang bisa dihindari selama maintenance biasa, sedangkan recovery semantics menangani interupsi yang tetap terjadi.
Uji batasnya, jangan hanya memercayai konfigurasi
Pengujian awal yang aman memakai pekerjaan disposable, marker yang terlihat, dan tanpa queue produksi. Buat satu job simulasi berjalan beberapa detik, mulai worker, kirim SIGTERM saat job tersebut berjalan, lalu periksa apakah job aktif selesai tanpa memulai job berikutnya:
php graceful-worker-demo.php &
worker_pid=$!
sleep 1
kill -TERM "$worker_pid"
wait "$worker_pid"
Dalam pemeriksaan lokal untuk artikel ini, proses PHP 8.4.27 CLI dengan PCNTL mencetak marker mulai dan selesai untuk job simulasi yang aktif, kemudian marker bahwa worker telah berhenti; proses itu tidak mencetak marker mulai untuk job berikutnya. Hasil tersebut memverifikasi demonstrasi kecil pada lingkungan yang diperiksa, bukan setiap queue client atau deployment PHP.
Setelah loop terisolasi bekerja, uji service sebenarnya pada staging atau queue lain yang aman dibuang. Pengamatan yang berguna mencakup journal di sekitar proses berhenti, exit status proses, status reservasi, dan apakah job yang sama dikirim lagi setelah forced kill. Uji juga timeout dependency yang lebih pendek daripada TimeoutStopSec=. Pengujian SIGTERM pada happy path saja tidak dapat memperlihatkan apa yang terjadi ketika DNS, database, atau HTTP endpoint macet.
Beberapa pola kegagalan perlu diperhatikan secara eksplisit:
- memanggil
exitdi dalam handler dapat melewati batas normal job aktif; - melakukan cleanup kompleks di dalam handler menciptakan control flow yang saling bersaing;
- mengambil batch membuat "berhenti setelah job saat ini" menjadi ambigu, kecuali reservasi yang belum dimulai dapat dilepas atau kedaluwarsa;
- I/O call tanpa batas dapat menghabiskan seluruh deadline berhenti;
- deadline yang lebih pendek daripada job normal menjamin terminasi paksa akan terjadi secara berkala;
- deadline yang jauh lebih panjang daripada job mana pun yang dapat diterima bisa menyembunyikan worker macet selama deployment.
Graceful berarti kooperatif dan terbatas
PHP worker kecil tidak membutuhkan shutdown framework yang rumit untuk bekerja sama dengan systemd. Worker itu membutuhkan batas job yang jelas, signal handler minimal, loop yang berhenti mengambil pekerjaan, operasi dengan batas waktu, dan deadline berhenti yang dipilih berdasarkan perilaku job nyata. Komponen tersebut membuat proses berhenti rutin menjadi tidak terlalu merusak dan lebih mudah diamati.
Semua itu tidak menghapus kebutuhan akan lease, retry, idempotency, atau pemulihan setelah hard failure. Batas tersebut bukan kekurangan pola ini; justru batas itulah yang menjaga janjinya tetap jujur. Pertanyaan desain praktisnya bukan "Bisakah worker ini selalu selesai?", melainkan "Ketika worker tidak dapat selesai, bisakah queue menjelaskan dan memulihkan sisa pekerjaannya dengan aman?"
