Queue worker minimalis di Linux kecil masih relevan ketika Anda butuh pemrosesan job yang sederhana, hemat memori, mudah dipasang, dan tidak bergantung pada layanan tambahan seperti Redis atau RabbitMQ. Pola ini cocok untuk sistem embedded, appliance, VM kecil, server utilitas, atau lingkungan recovery yang hanya punya shell, filesystem, dan mungkin SQLite.
Masalah utamanya bukan cara mengambil job dari antrian, melainkan bagaimana memastikan job tidak diproses ganda, bisa di-retry dengan aman, tidak macet karena lock stale, dan tetap bisa dioperasikan saat observabilitas terbatas. Artikel ini membahas rancangan praktis yang sengaja sederhana, tetapi tetap disiplin dalam hal locking, retry, dead-letter, idempotensi, dan cache kecil untuk mengurangi I/O.
Tujuan dan batasan desain
Desain queue worker minimalis biasanya mengejar empat hal:
- Dependensi rendah: cukup shell, utilitas POSIX dasar, filesystem, atau SQLite.
- Footprint kecil: cocok untuk RAM dan storage terbatas.
- Recovery mudah: state job bisa dilihat langsung dari file atau tabel.
- Operasional sederhana: mudah dijalankan lewat cron, init script, atau supervisor ringan.
Namun ada batasannya:
- Throughput biasanya lebih rendah dibanding broker khusus.
- Distribusi ke banyak node lebih sulit.
- Jaminan ordering dan delivery semantics lebih terbatas.
- Observabilitas, metrik, dan tooling biasanya minim.
Karena itu, pendekatan ini cocok bila kebutuhan Anda adalah cukup andal, bukan fitur paling lengkap.
Memilih backend antrian: file spool atau SQLite
Opsi 1: spool berbasis file
Pola paling sederhana adalah menyimpan satu job per file dalam direktori spool.
/var/spool/myq/
incoming/
processing/
done/
failed/
dlq/
locks/
Job baru ditulis ke incoming/. Worker memindahkan file ke processing/ sebagai bagian dari klaim job. Jika sukses, file dipindah ke done/. Jika gagal, metadata retry diperbarui lalu job dikembalikan ke antrean atau dipindah ke failed//dlq/.
Kelebihan:
- Sangat transparan dan mudah diperiksa manual.
- Tidak butuh daemon database.
- Operasi rename di filesystem yang sama umumnya atomik, sehingga berguna untuk claim job.
Kekurangan:
- Manajemen metadata retry dan status menjadi lebih rumit.
- Direktori dengan file sangat banyak bisa menurunkan performa.
- Pencarian job siap proses berdasarkan waktu bisa tidak efisien tanpa struktur tambahan.
Opsi 2: SQLite
SQLite memberi model state yang lebih terstruktur tanpa menambah service terpisah. Cocok jika sistem Anda masih cukup nyaman menjalankan satu file database lokal.
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
queue TEXT NOT NULL,
payload TEXT NOT NULL,
status TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 5,
available_at INTEGER NOT NULL,
leased_until INTEGER,
worker_id TEXT,
last_error TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_jobs_ready
ON jobs(queue, status, available_at);
Kelebihan:
- Metadata retry, lease, dan pencarian job lebih mudah.
- Query untuk job siap proses lebih jelas.
- Lebih mudah membuat dead-letter queue dan audit dasar.
Kekurangan:
- Perlu disiplin pada mode locking dan pola akses konkurensi.
- Tetap bukan broker terdistribusi.
- Jika storage sangat rapuh atau ruang sangat sempit, file database bisa menjadi titik sensitif.
Jika sistem benar-benar minimal, spool file biasanya paling mudah. Jika butuh status dan retry yang lebih rapi, SQLite sering menjadi titik tengah terbaik.
Arsitektur queue worker minimalis
Arsitektur dasarnya dapat dibuat seperti ini:
- Producer membuat job dengan ID unik dan payload.
- Queue store menyimpan job di spool file atau SQLite.
- Worker mengambil satu job yang siap diproses.
- Lock atau lease mencegah worker lain memproses job yang sama.
- Executor menjalankan handler bisnis.
- Retry scheduler menentukan kapan job gagal boleh dicoba lagi.
- DLQ menampung job yang sudah melewati batas retry.
- Small cache menyimpan state kecil yang sering dibaca untuk menekan I/O.
Pada sistem kecil, satu proses sering merangkap sebagai worker, retry scheduler, dan garbage collector lock stale. Itu tidak elegan seperti sistem besar, tetapi sering cukup.
Locking: mencegah duplicate processing
Prinsip dasar
Ada dua pendekatan umum:
- Exclusive claim: job dipindah atau diubah statusnya dari ready menjadi processing dalam langkah atomik.
- Lease berbatas waktu: worker memegang hak proses sampai waktu tertentu; jika mati, job bisa diambil ulang setelah lease habis.
Untuk lingkungan minimal, lease lebih aman daripada lock permanen, karena worker bisa crash kapan saja. Tanpa lease timeout, job bisa macet selamanya.
Locking pada spool file
Pola yang cukup kuat adalah:
- Scan satu job yang eligible di
incoming/. rename()file keprocessing/dengan nama yang sama.- Tulis metadata lease di file pendamping atau header file, misalnya
leased_untildanworker_id.
Di filesystem yang sama, rename umumnya atomik. Artinya dua worker tidak akan sama-sama berhasil memindahkan file yang sama.
Hindari mengandalkan hanya file lock advisory jika Anda juga ingin recovery yang mudah setelah crash. File lock berguna, tetapi lease eksplisit biasanya lebih mudah diaudit dan dipulihkan.
Locking pada SQLite
Pola aman adalah melakukan klaim job dengan satu transaksi singkat. Ide dasarnya:
- Pilih satu job dengan
status='ready'danavailable_at <= now. - Ubah status menjadi
processing, isiworker_id, dan setleased_until. - Pastikan perubahan hanya sukses jika status sebelumnya masih
ready.
-- Pseudocode SQL
BEGIN IMMEDIATE;
SELECT id FROM jobs
WHERE queue = ?
AND status = 'ready'
AND available_at <= ?
ORDER BY available_at, created_at
LIMIT 1;
UPDATE jobs
SET status = 'processing',
worker_id = ?,
leased_until = ?,
updated_at = ?
WHERE id = ? AND status = 'ready';
COMMIT;
Setelah UPDATE, cek apakah benar ada baris yang berubah. Jika nol, berarti ada race dan worker harus mencoba lagi.
Lock stale dan pemulihan
Stale lock terjadi ketika worker mati setelah mengklaim job. Solusinya bukan menonaktifkan lock, melainkan membuat lease timeout dan proses requeue.
Contoh aturan sederhana:
- Job normal diberi lease 60 detik.
- Worker memperpanjang lease secara periodik jika job masih berjalan.
- Cleaner atau worker lain menandai job kembali ready bila
leased_untilsudah lewat.
Trade-off-nya jelas: lease terlalu pendek meningkatkan risiko duplicate processing jika job sah masih berjalan tetapi lease tidak sempat diperpanjang. Lease terlalu panjang membuat recovery dari crash menjadi lambat.
Retry dengan backoff dan dead-letter sederhana
Mengapa retry butuh backoff
Jika semua kegagalan langsung diulang, Anda bisa menciptakan retry storm: CPU, disk, dan layanan dependen justru makin terbebani. Backoff memberi jarak antar percobaan sehingga sistem punya waktu pulih.
Rumus sederhana yang umum dipakai:
delay = min(base_delay * 2^(attempts-1), max_delay)Contoh:
- Attempt 1 gagal - retry 5 detik
- Attempt 2 gagal - retry 10 detik
- Attempt 3 gagal - retry 20 detik
- dan seterusnya sampai batas maksimum
Jika memungkinkan, tambahkan jitter kecil agar banyak job gagal tidak bangun bersamaan pada detik yang sama.
Membedakan kegagalan sementara dan permanen
Jangan semua error diperlakukan sama.
- Sementara: timeout jaringan, resource sibuk, koneksi target putus.
- Permanen: payload invalid, file sumber hilang permanen, validasi bisnis gagal.
Error permanen biasanya lebih baik langsung masuk DLQ daripada menghabiskan retry yang tidak akan pernah berhasil.
Dead-letter queue minimalis
DLQ tidak harus rumit. Tujuannya hanya satu: memisahkan job yang butuh intervensi manusia atau analisis lebih lanjut.
Pada spool file, DLQ bisa berupa direktori dlq/ berisi file job beserta metadata error. Pada SQLite, cukup status dead plus kolom last_error.
{
"id": "job-20260720-001",
"type": "send_webhook",
"attempts": 5,
"max_attempts": 5,
"last_error": "HTTP timeout",
"failed_at": 1753000000,
"payload": { "url": "https://example.net/hook", "event": "order.paid" }
}
Yang penting, job di DLQ tidak ikut diproses ulang otomatis tanpa inspeksi.
Idempotensi: perlindungan terakhir terhadap duplikasi
Dalam queue minimalis, jangan berasumsi exactly-once. Yang realistis biasanya adalah at-least-once processing. Artinya satu job bisa saja diproses lebih dari sekali, terutama saat crash terjadi di titik yang salah.
Karena itu, handler job harus sedapat mungkin idempotent. Menjalankan job yang sama dua kali tidak boleh menyebabkan efek samping ganda yang berbahaya.
Teknik idempotensi yang praktis
- Job ID unik: simpan ID job dan cek apakah efeknya sudah pernah diterapkan.
- Upsert ketimbang insert buta, jika operasi menyentuh database.
- External idempotency key saat memanggil API pihak ketiga, bila didukung.
- State check sebelum aksi: misalnya jangan kirim ulang email jika status sudah terkirim.
Contoh pola sederhana di database aplikasi:
CREATE TABLE processed_jobs (
job_id TEXT PRIMARY KEY,
processed_at INTEGER NOT NULL
);
Alurnya:
- Mulai transaksi aplikasi.
- Cek apakah
job_idsudah ada diprocessed_jobs. - Jika sudah ada, anggap sukses dan hentikan.
- Jika belum, jalankan efek bisnis lalu catat
job_id. - Commit transaksi.
Jika efek bisnis berada di sistem eksternal yang tidak mendukung transaksi, problemnya memang lebih sulit. Dalam kasus itu, idempotency key dan desain operasi yang aman menjadi sangat penting.
Cache kecil untuk menekan I/O
Pada Linux kecil, bottleneck sering bukan CPU, melainkan I/O disk dan sinkronisasi metadata filesystem. Cache kecil bisa membantu, asalkan tidak mengorbankan kebenaran state job.
Apa yang layak di-cache
- Daftar singkat job siap proses yang baru dipindai.
- Konfigurasi statis seperti base delay atau batas retry.
- Lookup kecil yang sering dipakai handler, misalnya mapping endpoint lokal.
- Status deduplikasi jangka sangat pendek untuk mencegah pembacaan ulang yang sama.
Apa yang jangan di-cache terlalu agresif
- Status lock atau lease sebagai sumber kebenaran utama.
- Counter retry tanpa persistensi.
- State final job yang menentukan apakah efek bisnis sudah dilakukan.
Aturan praktisnya: cache boleh mempercepat baca, tetapi keputusan final tetap mengacu pada state persisten.
Pada spool file, cache bisa sesederhana memori proses yang menyimpan hasil scan direktori selama beberapa ratus milidetik atau beberapa detik. Pada SQLite, cache bisa berupa statement yang dipersiapkan dengan baik dan pembatasan frekuensi polling, bukan selalu menambah lapisan cache terpisah.
Contoh alur kerja end-to-end
Skema spool file
- Producer membuat file job di
incoming/dengan penulisan atomik: tulis ke file sementara, lalu rename ke nama final. - Worker memindai job dengan
available_at <= now. - Worker mencoba rename file dari
incoming/keprocessing/. - Jika berhasil, worker menulis lease metadata.
- Handler mengeksekusi payload.
- Jika sukses, file dipindah ke
done/atau dihapus setelah dicatat. - Jika gagal sementara, attempts ditambah,
available_atdiubah sesuai backoff, lalu job dikembalikan ke antrean. - Jika attempts melebihi batas atau error permanen, job dipindah ke
dlq/.
Skema SQLite
- Producer melakukan insert job dengan
status='ready'. - Worker mengklaim satu job dalam transaksi dan menetapkan lease.
- Handler memproses payload.
- Worker memperpanjang lease jika job lama berjalan.
- Jika sukses, status menjadi
done. - Jika gagal sementara, status kembali
ready, attempts bertambah, danavailable_atdiatur ulang. - Jika gagal permanen atau attempts habis, status menjadi
dead.
Masalah operasional umum dan cara menanganinya
1. Job macet
Gejalanya: job lama berada di processing atau status processing terlalu lama.
Penyebab umum:
- Worker crash saat memegang lease.
- Handler hang karena I/O blocking.
- Lease tidak diperbarui.
Tindakan:
- Terapkan timeout eksekusi per job.
- Gunakan lease timeout dan mekanisme requeue.
- Simpan
started_at,leased_until, danworker_iduntuk diagnosis.
2. Duplicate processing
Gejalanya: efek bisnis muncul dua kali, misalnya webhook terkirim ganda atau record tercipta dua kali.
Penyebab umum:
- Lease habis saat job sebenarnya masih berjalan.
- Worker crash setelah efek bisnis sukses tetapi sebelum status job tersimpan.
- Lock tidak benar-benar atomik.
Tindakan:
- Pastikan claim job atomik.
- Gunakan idempotensi di level handler.
- Jangan set lease terlalu pendek tanpa heartbeat.
3. Lock stale
Gejalanya: job tidak pernah kembali ready walau worker sudah mati.
Tindakan:
- Semua lock harus punya TTL atau lease expiry.
- Sediakan proses pembersih yang konservatif.
- Catat PID atau worker ID, tetapi jangan hanya mengandalkan PID untuk validasi, karena PID bisa dipakai ulang.
4. Disk penuh
Ini masalah yang sering diremehkan pada sistem kecil. Jika disk penuh, producer mungkin gagal menulis job, worker gagal menulis state retry, atau SQLite gagal commit.
Tindakan:
- Pantau free space minimum.
- Batasi retensi direktori
done/dan log lokal. - Gunakan DLQ dan payload ringkas, jangan simpan blob besar bila tidak perlu.
- Pastikan penulisan job baru bersifat atomik agar file setengah jadi tidak dianggap valid.
5. Clock drift
Jika retry dan lease bergantung pada wall clock, drift waktu dapat menyebabkan job dijalankan terlalu cepat atau terlalu lambat.
Tindakan:
- Gunakan sumber waktu yang konsisten di satu host.
- Jangan mendesain logika yang mengandalkan sinkronisasi waktu presisi tinggi jika worker hanya berjalan pada satu mesin kecil.
- Jika memungkinkan, simpan durasi dan deadline sederhana, bukan perhitungan waktu yang rumit lintas node.
Untuk sistem satu host, drift biasanya lebih mudah ditoleransi daripada pada cluster multi-node.
6. Observabilitas minim
Tanpa Prometheus, tracing, atau dashboard besar, Anda tetap perlu tahu apa yang terjadi.
Minimal, simpan hal berikut:
- Jumlah job ready, processing, done, dead.
- Jumlah retry per periode.
- Usia job tertua yang belum selesai.
- Error terakhir per worker.
Ini bisa diekspor lewat skrip shell sederhana atau log periodik.
2026-07-20T10:15:00Z queue=myq ready=12 processing=1 done=204 dead=3 oldest_ready_s=48 retries_1m=5Format log yang konsisten jauh lebih berguna daripada log verbose yang tidak terstruktur.
Contoh implementasi sederhana
Contoh job file
Pada spool file, satu job bisa disimpan sebagai JSON kecil:
{
"id": "job-20260720-001",
"type": "send_webhook",
"payload": {
"url": "https://example.net/hook",
"body": {"event": "order.paid", "order_id": 123}
},
"attempts": 0,
"max_attempts": 5,
"available_at": 1753000000,
"created_at": 1752999990
}
Pseudocode loop worker
while true:
job = claim_one_ready_job()
if not job:
sleep(poll_interval)
continue
start_lease_heartbeat(job)
try:
if already_processed(job.id):
mark_done(job)
continue
handle(job)
mark_processed(job.id)
mark_done(job)
except PermanentError as e:
move_to_dlq(job, e)
except Exception as e:
attempts = job.attempts + 1
if attempts >= job.max_attempts:
move_to_dlq(job, e)
else:
schedule_retry(job, attempts, backoff(attempts), e)
finally:
stop_lease_heartbeat(job)
Poin penting dari pseudocode ini:
already_processed(job.id)adalah lapisan idempotensi.- Retry dan DLQ diputuskan berdasarkan jenis error dan jumlah attempt.
- Heartbeat lease mencegah job diambil worker lain jika handler berjalan cukup lama.
Consistency vs simplicity: trade-off yang nyata
Queue worker minimalis selalu berada di antara dua kebutuhan yang saling tarik-menarik:
- Simplicity: lebih sedikit komponen, lebih mudah dipasang dan dipulihkan.
- Consistency: lebih banyak state, transaksi, lease, dan validasi untuk mengurangi edge case.
Beberapa trade-off praktis:
- Spool file lebih sederhana untuk inspeksi manual, tetapi metadata dan query state lebih sulit.
- SQLite memberi konsistensi state lebih baik, tetapi menambah kompleksitas transaksi dan locking database.
- Lease pendek mempercepat recovery crash, tetapi meningkatkan risiko duplikasi.
- Lease panjang mengurangi duplikasi, tetapi membuat job macet lebih lama.
- Hapus job sukses segera menghemat disk, tetapi mengurangi audit trail.
- Simpan arsip done membantu debugging, tetapi berisiko memenuhi disk.
Tidak ada konfigurasi yang benar untuk semua kasus. Pilihan terbaik biasanya mengikuti nilai bisnis job, durasi tipikal, dan kemampuan operasi tim Anda.
Checklist implementasi
- Buat job ID unik dan gunakan untuk idempotensi.
- Pilih backend: spool file untuk kesederhanaan, SQLite untuk state lebih rapi.
- Pastikan claim job atomik.
- Gunakan lease dengan expiry, bukan lock tanpa batas.
- Terapkan retry dengan exponential backoff dan, bila mungkin, jitter.
- Bedakan error sementara dan permanen.
- Sediakan DLQ yang mudah diperiksa.
- Tambahkan timeout handler agar job tidak menggantung selamanya.
- Batasi ukuran payload dan retensi file/log agar disk tidak cepat penuh.
- Simpan log operasional minimum: status, attempt, lease, error terakhir.
- Uji skenario crash: mati sebelum commit, mati setelah efek bisnis, lease habis di tengah proses.
- Tambahkan housekeeping untuk stale lock, arsip
done, dan pembersihanfailed/dlq.
Kapan pendekatan ini cukup?
Pendekatan queue worker minimalis biasanya cukup jika:
- Semua worker berjalan di satu host atau lingkungan sangat kecil.
- Volume job relatif rendah sampai menengah.
- Job tidak memerlukan routing kompleks, priority rumit, atau fan-out besar.
- Tim Anda lebih diuntungkan oleh sedikit komponen daripada fitur broker lengkap.
- Debug manual lewat file atau SQLite justru menjadi kelebihan.
Contoh yang cocok:
- Pengiriman webhook internal berkecepatan rendah.
- Sinkronisasi file atau metadata berkala.
- Tugas latar belakang di appliance atau perangkat edge.
- Pipeline utilitas sederhana untuk email, notifikasi, atau eksekusi script.
Kapan harus pindah ke Redis, RabbitMQ, atau Kafka?
Pertimbangkan migrasi saat salah satu kondisi ini mulai dominan:
- Anda butuh multi-worker lintas banyak host dengan koordinasi lebih andal.
- Throughput meningkat dan polling filesystem/SQLite mulai menjadi bottleneck.
- Anda memerlukan fitur seperti consumer groups, routing, delayed delivery yang lebih kaya, priority, atau replay yang lebih baik.
- Observabilitas dan tooling operasi menjadi kebutuhan utama.
- Kehilangan atau duplikasi job memiliki dampak bisnis yang makin mahal.
Pilihan ringkas
- Redis: cocok untuk queue cepat dan operasional relatif sederhana, tetapi perlu desain durability dan retry yang disiplin.
- RabbitMQ: cocok jika butuh routing, ack, dan pola message broker klasik yang lebih matang.
- Kafka: cocok untuk stream event skala lebih besar, replay, dan konsumsi terdistribusi, tetapi kompleksitasnya jauh lebih tinggi.
Pindah ke broker khusus bukan berarti desain idempotensi dan retry boleh diabaikan. Prinsip-prinsip itu tetap berlaku.
Penutup
Queue worker minimalis di Linux kecil bukan solusi kuno. Untuk banyak kasus, ini justru pendekatan yang paling rasional: sedikit komponen, mudah dipahami, dan cukup andal jika fondasinya benar. Kuncinya ada pada claim job yang atomik, lease yang bisa kedaluwarsa, retry dengan backoff, DLQ sederhana, dan idempotensi sebagai pagar terakhir terhadap duplikasi.
Jika Anda mulai dari sistem yang sangat kecil, bangun dulu versi yang jelas dan dapat dipulihkan. Jangan buru-buru mengejar fitur broker besar. Tetapi saat volume, distribusi, dan tuntutan operasi tumbuh, bersiaplah mengakui batas desain sederhana ini dan pindah ke Redis, RabbitMQ, atau Kafka dengan alasan yang konkret, bukan sekadar tren.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!