Upgrade worker aman tidak dimulai dari proses deploy, tetapi dari desain kontrak antar-komponen. Selama worker lama dan baru bisa hidup bersamaan, sistem Anda berada dalam mode kompatibilitas campuran: payload job bisa dibaca dua versi, cache bisa berisi format lama dan baru, lock bisa diambil oleh proses dengan aturan berbeda, dan retry bisa memperburuk inkonsistensi jika job tidak idempotent.
Prinsip praktisnya sederhana: jangan menganggap queue, cache, dan lock sebagai detail internal aplikasi. Anggap semuanya sebagai interface publik yang harus dijaga kompatibilitasnya selama masa transisi. Inilah inti dari pendekatan “membuat upgrade mudah”: desain perubahan agar versi lama dan baru bisa koeksis, lalu lakukan rollout bertahap dengan observability dan rollback yang jelas.
Mengapa upgrade worker sering gagal meski aplikasi utama tampak aman
Pada sistem web biasa, request baru cenderung langsung masuk ke versi aplikasi yang terbaru. Pada sistem queue terdistribusi, kondisinya berbeda:
- Job yang dibuat versi lama bisa diproses worker versi baru beberapa menit atau jam kemudian.
- Job yang dibuat versi baru bisa terbaca worker lama jika rollout belum merata.
- Retry membuat payload lama tetap beredar lama setelah deploy selesai.
- Dead-letter queue, scheduled job, dan backlog panjang memperpanjang masa kompatibilitas.
Akibatnya, kegagalan upgrade worker biasanya berasal dari kontrak yang berubah diam-diam:
- Payload job berubah bentuk: field wajib baru ditambahkan, field lama dihapus, atau makna field bergeser.
- Schema cache key berubah: worker baru menulis format baru, worker lama masih membaca format lama, hasilnya cache miss atau data salah.
- Strategi lock berubah: nama lock, owner token, TTL, atau scope lock berbeda sehingga dua versi memproses item yang sama.
- Asumsi ordering berubah: worker baru mengandalkan urutan event yang tidak dijamin queue.
Prinsip desain kontrak untuk upgrade worker aman
1. Payload queue harus versioned dan backward compatible
Jangan kirim payload yang hanya “kebetulan cocok” dengan kode saat ini. Tambahkan versi schema atau setidaknya bentuk payload yang bisa dikenali dengan jelas.
{
"type": "invoice.generate",
"schema_version": 2,
"job_id": "job-7f42",
"invoice_id": "inv-123",
"customer_id": "cust-9",
"options": {
"send_email": true
}
}Aturan aman yang umum dipakai:
- Tambahan field baru harus opsional bagi worker lama.
- Jangan langsung menghapus field lama selama masih ada kemungkinan worker lama membacanya.
- Jangan ubah arti field tanpa mengganti nama atau versi schema.
- Gunakan default yang stabil bila field baru belum tersedia.
Jika Anda perlu migrasi bentuk payload, pola paling aman adalah reader toleran, writer konservatif: worker baru mampu membaca v1 dan v2, tetapi producer tetap menulis v1 sampai semua consumer siap.
2. Cache key adalah kontrak, bukan implementasi internal
Banyak incident terjadi karena cache key dianggap detail kecil. Padahal perubahan key format atau struktur value dapat memecahkan kompatibilitas lintas worker.
Contoh masalah umum:
- Versi lama membaca
user:42sebagai objek JSON sederhana, versi baru menulis objek dengan struktur berbeda pada key yang sama. - Versi baru mengganti namespace key, tetapi versi lama masih mengandalkan key lama untuk deduplikasi.
- TTL berubah drastis sehingga lock atau marker idempotensi kedaluwarsa terlalu cepat.
Pola yang lebih aman:
- Versioned key: misalnya
invoice:v1:123daninvoice:v2:123. - Dual-read: baca v2 lebih dulu, lalu fallback ke v1.
- Dual-write sementara: tulis ke v1 dan v2 selama masa migrasi.
- TTL konsisten: jangan mengubah masa berlaku tanpa memahami efek retry dan backlog.
3. Lock harus kompatibel lintas versi
Lock sering dipakai untuk mencegah pekerjaan ganda, tetapi perubahan kecil pada nama lock atau aturan pelepasannya bisa berbahaya.
Contoh failure mode:
- Worker lama memakai lock
sync:account:42, worker baru memakaiaccount-sync:42. Keduanya merasa aman, tetapi sebenarnya dua proses jalan paralel. - Worker baru memperpendek TTL lock agar throughput naik, tetapi job lama ternyata lebih lama dari TTL. Lock habis sebelum proses selesai dan job kedua masuk.
- Worker lama melepas lock tanpa owner token, worker baru mengharuskan token. Jika implementasi campur, lock bisa terhapus oleh proses yang bukan pemiliknya.
Aturan aman untuk lock saat upgrade:
- Nama lock untuk resource yang sama harus tetap identik sepanjang masa transisi.
- Jika ingin mengganti format lock, lakukan migrasi kompatibel seperti pada cache key.
- Gunakan owner token atau mekanisme serupa agar hanya pemilik lock yang dapat melepas lock.
- TTL lock harus lebih panjang dari worst-case processing time atau didukung mekanisme perpanjangan lease yang aman.
Strategi rollout tanpa downtime
Fase 1: Siapkan consumer yang toleran
Langkah pertama hampir selalu sama: deploy worker baru yang bisa membaca format lama dan baru, tetapi belum memproduksi format baru secara default. Ini mengurangi risiko split-brain selama rollout.
function handleJob(payload) {
switch (payload.schema_version ?? 1) {
case 1:
return handleV1(normalizeV1ToCurrent(payload))
case 2:
return handleV2(payload)
default:
sendToDeadLetterQueue(payload, "unsupported_schema_version")
}
}Normalisasi di tepi consumer membantu menjaga inti logika tetap sederhana. Anda tidak ingin seluruh kode bisnis penuh dengan percabangan versi.
Fase 2: Dual-read/dual-write untuk cache dan state turunan
Bila worker berinteraksi dengan cache atau state hasil komputasi, gunakan migrasi bertahap:
- Worker baru dual-read dari key baru lalu lama.
- Worker baru dual-write ke keduanya.
- Setelah semua reader memakai format baru dan metric menunjukkan stabil, hentikan write ke format lama.
- Baru kemudian hapus reader fallback dan lakukan cleanup key lama.
function readUserProjection(userId) {
const v2 = cache.get(`user_projection:v2:${userId}`)
if (v2 != null) return parseV2(v2)
const v1 = cache.get(`user_projection:v1:${userId}`)
if (v1 != null) return convertV1ToV2(parseV1(v1))
return null
}
function writeUserProjection(userId, value) {
cache.set(`user_projection:v2:${userId}`, serializeV2(value), ttl)
cache.set(`user_projection:v1:${userId}`, serializeV1(fromV2ToV1(value)), ttl)
}Trade-off: dual-write menambah biaya I/O dan kompleksitas cleanup. Namun ini hampir selalu lebih murah daripada incident karena reader lama tiba-tiba gagal.
Fase 3: Aktifkan producer baru dengan feature flag
Setelah consumer siap, barulah producer atau bagian aplikasi yang membuat job diizinkan mengirim schema baru. Aktifkan dengan feature flag, bukan deploy serentak.
Keuntungan feature flag:
- Rollback cepat tanpa harus menunggu redeploy penuh.
- Bisa dibatasi per tenant, per queue, atau persentase traffic.
- Bisa dipakai untuk membedakan producer dan consumer rollout.
Fase 4: Canary worker
Sebelum menaikkan seluruh armada worker, jalankan sebagian kecil worker versi baru untuk queue yang sama atau subset workload yang terkontrol. Tujuannya bukan hanya melihat apakah proses “jalan”, tetapi apakah perilakunya konsisten.
Yang perlu dibandingkan saat canary:
- rate sukses/gagal per jenis job,
- latency proses,
- jumlah retry,
- jumlah duplicate processing,
- persentase job masuk DLQ,
- perbedaan hasil side effect, misalnya status database, file, atau event lanjutan.
Fase 5: Cutover dan pembersihan
Setelah producer baru aktif penuh dan backlog format lama habis, Anda bisa:
- mematikan dual-write,
- menghapus fallback dual-read,
- menghapus dukungan schema lama,
- membersihkan cache key dan lock namespace lama bila ada.
Jangan lakukan cleanup ini terlalu cepat. Scheduled job, retry tertunda, dan dead-letter replay sering menjadi sumber payload lama yang datang terlambat.
Idempotensi, retry, dan poison message: fondasi wajib
Idempotensi lebih penting saat dua versi hidup bersamaan
Dalam rollout campuran, Anda harus berasumsi satu pekerjaan bisa diproses lebih dari sekali, oleh versi berbeda, atau setelah timeout ambigu. Karena itu, side effect harus dibuat idempotent semaksimal mungkin.
Pola yang umum dipakai:
- Idempotency key berbasis
job_idatau resource + aksi + versi bisnis yang relevan. - Upsert/compare-and-set di database, bukan insert buta.
- Status machine eksplisit agar transisi invalid bisa terdeteksi.
- Outbox/inbox pattern untuk mencegah event ganda.
-- Contoh sederhana marker idempotensi
INSERT INTO processed_jobs (job_id, processed_at)
VALUES (:job_id, NOW())
ON CONFLICT (job_id) DO NOTHING;Jika insert tidak berhasil karena sudah ada, worker dapat memutuskan bahwa side effect utama kemungkinan sudah pernah dijalankan.
Retry harus membedakan error transien dan permanen
Retry membabi buta memperburuk poison message. Klasifikasi error minimal:
- Transien: koneksi database putus, timeout jaringan, service dependency overload.
- Permanen: schema payload tidak didukung, data wajib hilang, invariant bisnis rusak.
Error permanen sebaiknya tidak diulang tanpa batas. Kirim ke dead-letter queue atau tandai untuk intervensi operator.
Kesalahan umum: payload schema baru diproses worker lama, lalu gagal parse dan terus diretry. Akibatnya queue penuh oleh job yang tidak mungkin sukses.
Poison message harus bisa diisolasi cepat
Poison message adalah job yang konsisten gagal dan mengganggu throughput. Saat upgrade, penyebab umumnya:
- schema version tidak dikenal,
- field baru dianggap wajib oleh worker baru padahal producer lama belum mengirimnya,
- cache entry format lama menyebabkan deserializer gagal,
- job lama memicu path kode yang sudah tidak kompatibel.
Minimal, simpan alasan kegagalan yang bisa dicari operator: jenis job, versi schema, exception class, hash payload, jumlah retry, dan worker version.
Ordering dan konsistensi saat versi campuran
Jangan mengandalkan ordering global kecuali queue Anda benar-benar menjaminnya dan Anda memahami batasannya. Banyak sistem hanya memberi at-least-once delivery dan ordering terbatas per partition atau per key.
Failure mode yang sering muncul:
- Worker baru memproses event
updatedlebih dulu daripada worker lama memproses eventcreated. - Versi baru menambahkan event turunan tambahan sehingga urutan side effect berubah.
- Retry event lama datang terlambat dan menimpa hasil event baru.
Mitigasi yang umum:
- Gunakan sequence number atau version counter pada resource.
- Terapkan last-write-wins hanya jika aman secara bisnis.
- Untuk resource sensitif, gunakan per-resource lock atau partitioning berbasis key.
- Simpan versi state terakhir yang sudah diterapkan agar event usang bisa diabaikan.
function applyEvent(event) {
currentVersion = db.getResourceVersion(event.resource_id)
if (event.sequence <= currentVersion) {
return "stale_event_ignored"
}
db.transaction(() => {
db.updateResourceIfVersionLessThan(event.resource_id, event.sequence, event.payload)
})
}Contoh failure mode operasional saat worker lama dan baru hidup bersamaan
1. Job baru gagal di worker lama
Gejala: spike retry setelah sebagian producer mengirim payload baru.
Penyebab: rollout producer mendahului consumer, atau ada autoscaling yang masih meluncurkan image lama.
Pencegahan: consumer toleran lebih dulu, gate producer dengan feature flag, verifikasi image rollout di seluruh pool worker.
2. Duplicate processing karena lock namespace berubah
Gejala: satu invoice ditagih dua kali, file diproses dua kali, atau dua event lanjutan terkirim.
Penyebab: lock key berbeda antara versi lama dan baru.
Pencegahan: treat lock key sebagai kontrak; uji kompatibilitas lock di environment staging dengan dua versi worker aktif bersamaan.
3. Cache miss besar-besaran setelah deploy
Gejala: throughput turun, database melonjak, latency job naik.
Penyebab: schema cache key berganti tanpa dual-read/dual-write.
Pencegahan: versioned key, migrasi bertahap, monitor cache hit ratio per versi.
4. Poison message memenuhi queue retry
Gejala: backlog tumbuh meski worker scale out.
Penyebab: job gagal permanen tetapi terus diretry, misalnya schema unsupported.
Pencegahan: klasifikasi error permanen vs transien, DLQ, circuit breaker operasional untuk mematikan producer baru bila perlu.
5. Rollback gagal karena data turunan sudah terlanjur format baru
Gejala: setelah rollback kode, worker lama tidak bisa membaca cache/state yang ditulis worker baru.
Penyebab: rollback hanya memikirkan binary aplikasi, bukan format data bersama.
Pencegahan: rollback plan harus mencakup kompatibilitas cache, lock, payload, dan event lanjutan; gunakan dual-write sementara.
Pola implementasi yang praktis
Versioned payload dengan normalizer
Gunakan normalizer agar logika bisnis memproses satu model internal yang stabil.
function normalize(payload) {
const version = payload.schema_version ?? 1
if (version == 1) {
return {
type: payload.type,
jobId: payload.job_id,
invoiceId: payload.invoice_id,
customerId: payload.customer_id ?? null,
sendEmail: payload.send_email ?? false
}
}
if (version == 2) {
return {
type: payload.type,
jobId: payload.job_id,
invoiceId: payload.invoice_id,
customerId: payload.customer_id,
sendEmail: payload.options?.send_email ?? false
}
}
throw UnsupportedSchemaVersion
}Keuntungan pola ini: titik kompatibilitas terpusat, lebih mudah diuji, dan lebih mudah dihapus setelah migrasi selesai.
Feature flag untuk writer baru
Jangan menyalakan writer baru secara global sekaligus. Aktifkan bertahap:
- internal tenant dulu,
- 1% traffic,
- subset queue tertentu,
- baru full rollout.
Pastikan flag dapat dimatikan tanpa deploy ulang.
Canary worker dengan isolasi observability
Canary paling berguna jika metriknya bisa dibedakan dari worker reguler. Tambahkan label seperti:
worker_version,deployment_ring(canary, stable),queue_name,job_type,schema_version.
Observability yang wajib dipantau
Selama upgrade worker aman, metrik dasar saja tidak cukup. Anda perlu bisa menjawab: versi mana yang memproduksi job, versi mana yang mengonsumsi, schema apa yang sedang beredar, dan di mana kegagalan terkonsentrasi.
Metrik utama
- Backlog queue per queue dan per job type.
- Age of oldest message.
- Success rate, failure rate, retry rate, DLQ rate.
- Processing latency dan time-in-queue.
- Duplicate detection/idempotency hit rate.
- Cache hit ratio per key version.
- Lock acquisition failure rate dan lock timeout rate.
- Jumlah job berdasarkan
schema_version.
Log yang harus ada
job_id,trace_idatau correlation id,job_type,schema_version,worker_version,retry_count,- hasil akhir: success, retry, DLQ, dropped.
Tracing dan audit side effect
Jika job memanggil service lain, kirim correlation id agar duplicate processing bisa ditelusuri lintas sistem. Untuk side effect penting seperti pembayaran, email, provisioning, atau sinkronisasi eksternal, simpan audit record yang jelas.
Checklist migrasi upgrade worker tanpa downtime
Sebelum deploy
- Identifikasi semua kontrak bersama: payload queue, cache key/value, lock key/TTL, idempotency marker, event turunan.
- Tentukan apakah perubahan bersifat backward compatible, forward compatible, atau butuh fase transisi.
- Tambahkan
schema_versionatau mekanisme deteksi format yang jelas. - Pastikan consumer baru bisa membaca payload lama.
- Pastikan rollback plan tidak hanya untuk kode, tetapi juga untuk data bersama.
- Siapkan dashboard dan alert per
worker_versiondanschema_version.
Saat deploy
- Deploy consumer toleran terlebih dahulu.
- Jalankan canary worker dan amati metrik inti.
- Aktifkan dual-read/dual-write bila cache atau state berubah.
- Nyalakan producer format baru dengan feature flag bertahap.
- Pantau retry, DLQ, duplicate processing, dan cache miss.
Setelah deploy
- Tunggu backlog payload lama benar-benar habis.
- Verifikasi scheduled job, delayed retry, dan DLQ replay tidak lagi menghasilkan format lama.
- Nonaktifkan dual-write lebih dulu, baru hapus dual-read.
- Hapus dukungan schema lama hanya setelah jendela aman terlewati.
- Bersihkan key lama dan dokumentasikan keputusan migrasi.
Strategi rollback yang realistis
Rollback worker tidak cukup berarti “jalankan image lama”. Jika worker baru sudah menulis format cache v2, membuat event baru, atau memakai idempotency marker baru, worker lama bisa tetap rusak setelah rollback.
Rollback yang realistis biasanya mencakup:
- Matikan producer format baru lewat feature flag.
- Hentikan scale-up worker baru; bila perlu drain worker baru secara bertahap.
- Pertahankan consumer toleran jika memungkinkan, karena ini memperbesar peluang recovery.
- Biarkan dual-read sementara tetap aktif sampai sistem stabil.
- Jangan hapus data format baru secara tergesa-gesa kecuali Anda yakin cleanup aman.
Dalam banyak kasus, rollback terbaik bukan kembali sepenuhnya ke versi lama, melainkan freeze writer baru sambil mempertahankan pembaca yang kompatibel.
Penutup
Upgrade worker aman tanpa downtime bergantung pada disiplin menjaga kontrak bersama. Queue payload, cache key, lock, marker idempotensi, dan urutan event harus diperlakukan sebagai API antar-versi, bukan detail lokal yang bisa diubah sepihak.
Jika Anda mengingat satu hal, ingat ini: deploy consumer yang toleran dulu, aktifkan producer baru belakangan, ukur semuanya, dan siapkan rollback yang memahami data bersama. Dengan pola itu, coexistence antara worker lama dan baru menjadi kondisi yang dirancang, bukan sumber incident yang mengejutkan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!