Saat model backend inference diganti atau di-rollout bertahap, bug yang paling sulit dilacak sering bukan error fatal, melainkan race condition. Gejalanya tampak acak: sebagian request menghasilkan output berbeda untuk input yang sama, sebagian timeout, sebagian job diproses dua kali, dan sebagian lagi membaca cache lama. Dalam praktik backend, masalah ini hampir selalu terkait kombinasi version skew antar worker, idempotensi yang lemah, retry tanpa deduplikasi, dan invalidasi cache yang terlambat.

Artikel ini membahas studi kasus debugging backend untuk skenario tersebut. Sebagai konteks industri, perpindahan talenta AI besar seperti Noam Shazeer ke OpenAI sering mengingatkan bahwa perubahan di lapisan model bukan hanya soal kualitas model, tetapi juga stabilitas sistem produksi saat backend inference berubah. Referensi konteks: post terkait. Fokus artikel ini tetap pada sisi engineering: bagaimana menemukan akar masalah dan memperbaikinya dengan aman.

Gejala nyata saat worker inference terkena race condition

Pada rollout model backend, gejala biasanya tidak muncul sebagai satu insiden tunggal. Yang lebih sering terjadi adalah kombinasi beberapa sinyal berikut:

  • Hasil inferensi tidak konsisten untuk input yang identik.
  • Timeout sporadis pada sebagian request, terutama saat worker lama dan baru sama-sama aktif.
  • Duplikasi job karena retry menganggap job sebelumnya gagal, padahal masih berjalan.
  • Cache lama terbaca sehingga response berasal dari model versi sebelumnya.

Masalah menjadi membingungkan karena tiap gejala bisa terlihat seperti isu terpisah. Tim API mungkin melihat latency spike, tim data melihat drift output, dan tim infra melihat queue depth naik. Padahal semuanya berasal dari titik yang sama: tidak adanya batas konsistensi yang jelas saat transisi backend model berlangsung.

Studi kasus: rollout bertahap model backend memicu hasil acak

Bayangkan arsitektur sederhana berikut:

  • API menerima request inferensi.
  • Request masuk ke queue.
  • Worker mengambil job dan memanggil model backend.
  • Hasil disimpan ke cache agar request identik bisa dijawab lebih cepat.

Tim melakukan rollout bertahap dari model-backend-A ke model-backend-B. Sebagian worker sudah memakai backend baru, sebagian masih mengarah ke backend lama. Secara teori ini aman jika sistem memang dirancang mendukung dua versi berjalan bersamaan. Namun di lapangan, ada beberapa celah:

  • Cache key hanya berbasis hash input, bukan hash input + versi model.
  • Retry job hanya berdasarkan timeout, tanpa dedupe key.
  • Worker lama dan baru menulis ke store hasil yang sama.
  • Invalidasi cache dilakukan asynchronous dan terlambat.
  • Observability tidak menyertakan model_version pada log dan trace.

Akibatnya, request yang sama bisa diproses dua kali oleh worker berbeda, menghasilkan output berbeda, lalu salah satu menimpa hasil yang lain. Jika worker lambat masih menulis setelah worker cepat selesai, state akhir tidak lagi merepresentasikan urutan logis request.

Root cause yang paling sering muncul

1. Version skew antar worker

Version skew terjadi saat sebagian worker sudah memuat konfigurasi atau endpoint model baru, sementara sebagian lain masih memakai yang lama. Ini umum terjadi pada rollout bertahap, deployment dengan drain yang tidak sempurna, atau worker yang mempertahankan koneksi/config terlalu lama.

Masalahnya bukan sekadar ada dua versi aktif. Masalah muncul ketika komponen lain mengasumsikan hanya ada satu versi yang valid. Contoh paling umum adalah cache dan penyimpanan hasil yang tidak menyertakan identitas versi model.

cache_key = sha256(normalized_input)

Key di atas berbahaya pada masa transisi. Worker A bisa menulis hasil dari model lama, lalu worker B membaca hasil tersebut dan menganggapnya valid untuk model baru.

Pendekatan yang lebih aman:

cache_key = sha256(normalized_input + ":" + model_version)

Dengan cara ini, hasil inferensi menjadi version-aware. Trade-off-nya adalah hit ratio cache bisa turun sementara saat rollout, tetapi konsistensi sistem jauh lebih baik.

2. Idempotensi lemah pada eksekusi job

Pada sistem queue, timeout tidak selalu berarti job gagal. Bisa jadi worker masih memproses tetapi koneksi ke caller putus, ACK terlambat, atau visibility timeout habis sebelum proses selesai. Jika job kemudian dikirim ulang tanpa mekanisme idempotensi, Anda mendapat eksekusi ganda.

Kesalahan umum:

  • Menganggap job retry selalu aman.
  • Menggunakan job ID acak baru pada setiap retry.
  • Tidak menyimpan status terminal secara atomik.

Solusi dasarnya adalah menggunakan idempotency key yang stabil untuk operasi bisnis yang sama, lalu memastikan write ke storage bersifat atomik atau memiliki proteksi compare-and-set.

3. Retry tanpa dedupe

Retry memang penting, terutama untuk backend inference yang sesekali timeout. Namun retry tanpa dedupe adalah sumber duplikasi yang klasik. Ketika dua attempt aktif pada waktu yang berdekatan, keduanya bisa sama-sama berhasil dan menulis hasil akhir.

Jika backend model mahal atau latency tinggi, dampaknya bukan hanya inkonsistensi, tetapi juga pemborosan compute.

Pola yang lebih aman:

  • Tetapkan dedupe key per request inferensi.
  • Simpan status in-progress, succeeded, atau failed di store bersama.
  • Tolak eksekusi kedua jika request identik masih berjalan.
  • Jika retry perlu dilakukan, pastikan hasil akhirnya tidak menimpa state yang lebih baru.

4. Invalidasi cache terlambat

Cache invalidation sering menjadi penyebab terselubung. Tim mengganti model backend, tetapi cache lama masih dianggap sah karena TTL belum habis atau proses invalidasi asynchronous tertinggal. Ini menyebabkan gejala “model baru sudah live, tapi output lama masih muncul”.

Masalah semakin parah jika beberapa worker membaca dari cache sementara worker lain langsung memanggil backend baru. Akhirnya output antar request menjadi terlihat acak, padahal sistem sekadar membaca dua sumber kebenaran yang berbeda.

Cara diagnosis: log, trace, dan bukti yang perlu dicari

Debug race condition worker inference saat model backend diganti tidak cukup dengan melihat error rate. Anda perlu menghubungkan satu request dari API, queue, worker, backend model, cache, dan storage hasil.

Tambahkan atribut observability yang tepat

Pada setiap log dan trace span, usahakan minimal ada field berikut:

  • request_id
  • job_id
  • idempotency_key
  • worker_id
  • model_version
  • backend_endpoint
  • cache_key
  • attempt
  • queue_receive_count

Tanpa field tersebut, Anda hanya melihat gejala umum tanpa bisa membuktikan urutan kejadian.

Contoh log yang menunjukkan version skew dan duplikasi

2026-06-26T10:15:21.104Z INFO request_id=req-91 job_id=job-441 attempt=1 worker_id=worker-a model_version=v1 backend_endpoint=model-a cache_key=inf:abc status=cache_miss
2026-06-26T10:15:21.892Z INFO request_id=req-91 job_id=job-441 attempt=1 worker_id=worker-a model_version=v1 status=inference_started
2026-06-26T10:15:24.031Z WARN request_id=req-91 job_id=job-441 attempt=1 worker_id=worker-a model_version=v1 status=timeout_waiting_upstream
2026-06-26T10:15:24.207Z INFO request_id=req-91 job_id=job-441 attempt=2 worker_id=worker-c model_version=v2 backend_endpoint=model-b cache_key=inf:abc status=retry_dispatched
2026-06-26T10:15:25.445Z INFO request_id=req-91 job_id=job-441 attempt=2 worker_id=worker-c model_version=v2 status=inference_succeeded write=result_store
2026-06-26T10:15:25.991Z INFO request_id=req-91 job_id=job-441 attempt=1 worker_id=worker-a model_version=v1 status=late_success write=result_store
2026-06-26T10:15:25.992Z WARN request_id=req-91 job_id=job-441 status=overwrite_detected previous_model_version=v2 new_model_version=v1

Dari log di atas terlihat pola penting:

  1. Attempt pertama berjalan di worker dengan model_version=v1.
  2. Karena timeout, sistem memicu retry ke worker lain yang sudah memakai v2.
  3. Attempt kedua sukses lebih dulu dan menulis hasil.
  4. Attempt pertama ternyata tidak benar-benar gagal; ia sukses terlambat dan menimpa hasil yang lebih baru.

Jika storage Anda tidak punya proteksi terhadap late write, state akhir akan salah.

Pertanyaan diagnosis yang harus dijawab

  • Apakah request identik diproses oleh lebih dari satu worker?
  • Apakah attempt yang lebih tua masih boleh menulis setelah attempt yang lebih baru selesai?
  • Apakah cache key membedakan versi model?
  • Apakah worker lama masih menerima job setelah rollout dimulai?
  • Apakah visibility timeout queue lebih pendek dari durasi inferensi nyata?
  • Apakah invalidasi cache sinkron dengan perubahan routing model?

Perbaikan kode dan arsitektur yang efektif

1. Jadikan hasil inferensi version-aware

Semua artefak yang berhubungan dengan output inferensi perlu mengetahui versi model, minimal:

  • cache key
  • record hasil
  • metadata trace
  • payload event downstream

Contoh struktur record hasil:

{
  "request_id": "req-91",
  "idempotency_key": "idem-2f8c",
  "model_version": "v2",
  "status": "succeeded",
  "output_ref": "s3://bucket/result/req-91.json",
  "attempt": 2,
  "completed_at": "2026-06-26T10:15:25.445Z"
}

Dengan metadata ini, sistem downstream dapat menolak hasil dari versi yang tidak diharapkan atau hasil yang datang terlambat.

2. Gunakan idempotency key dan state machine yang eksplisit

Alih-alih membiarkan setiap worker bebas menulis hasil, buat state machine sederhana:

  • PENDING
  • IN_PROGRESS
  • SUCCEEDED
  • FAILED
  • CANCELLED

Transisi status sebaiknya dijaga secara atomik. Misalnya, worker hanya boleh mengubah IN_PROGRESS menjadi SUCCEEDED jika token eksekusinya masih valid.

UPDATE inference_jobs
SET status = 'SUCCEEDED', model_version = :model_version, output_ref = :output_ref
WHERE idempotency_key = :idempotency_key
  AND status = 'IN_PROGRESS'
  AND lease_token = :lease_token;

Kenapa ini bekerja? Karena worker yang kehilangan lease, retry lama, atau attempt terlambat tidak lagi memenuhi syarat untuk menulis hasil final.

3. Terapkan dedupe sebelum memproses job

Jika menggunakan Redis atau store sejenis, Anda bisa memasang lock singkat berbasis SET NX atau mekanisme lease. Tujuannya bukan membuat sistem sepenuhnya sinkron, tetapi menurunkan probabilitas dua worker memproses request identik pada saat yang sama.

SET inference:lock:{idempotency_key} {lease_token} NX EX 60

Trade-off:

  • Kelebihan: sederhana dan efektif untuk mencegah duplikasi kasar.
  • Kekurangan: lock saja tidak cukup; Anda tetap perlu proteksi di layer storage untuk menghadapi lock expiry, network partition, atau proses yang crash.

4. Cegah late write dengan compare-and-set

Masalah terbesar biasanya bukan duplikasi itu sendiri, melainkan attempt lama yang datang belakangan lalu menimpa hasil benar. Solusi praktisnya adalah menambahkan monotonik pada write, misalnya berdasarkan nomor attempt, generation, atau lease epoch.

UPDATE inference_results
SET output = :output, model_version = :model_version, generation = :generation
WHERE request_id = :request_id
  AND generation < :generation;

Jika attempt lama menulis dengan generation lebih kecil, update akan ditolak.

5. Pisahkan rollout routing dari invalidasi cache

Jangan mengandalkan TTL semata saat mengganti model backend. Beberapa pendekatan yang lebih aman:

  • Gunakan namespace cache per model_version.
  • Aktifkan read-through cache yang selalu menulis ke namespace versi aktif.
  • Saat cutover, alihkan pointer versi aktif, bukan menghapus semua key sekaligus.

Pola ini lebih terkontrol dibanding menghapus massal dan berharap semua worker serempak membaca state baru.

Contoh implementasi alur yang lebih aman

function processInferenceJob(job) {
  const idemKey = job.idempotencyKey;
  const lease = acquireLease(idemKey);
  if (!lease.acquired) {
    return { status: "duplicate_skipped" };
  }

  const activeModelVersion = getActiveModelVersion();
  const cacheKey = hash(job.normalizedInput + ":" + activeModelVersion);

  const cached = cache.get(cacheKey);
  if (cached) {
    markSucceededIfOwned(idemKey, lease.token, activeModelVersion, cached.ref);
    return cached;
  }

  const result = callModelBackend(activeModelVersion, job.payload);

  cache.set(cacheKey, result, { ttl: 300 });
  const updated = markSucceededIfOwned(idemKey, lease.token, activeModelVersion, result.ref);

  if (!updated) {
    logWarn("late_write_rejected", { idemKey, modelVersion: activeModelVersion });
    return { status: "discarded_late_result" };
  }

  return result;
}

Beberapa poin penting dari contoh ini:

  • Versi model diambil eksplisit dan ikut membentuk cache key.
  • Lease/ownership dicek sebelum hasil ditandai final.
  • Jika worker kalah balapan, hasilnya dibuang secara aman dan tidak menimpa state valid.

Ini bukan satu-satunya desain yang benar, tetapi prinsipnya konsisten: ownership eksekusi harus bisa diverifikasi saat write final.

Rollout aman saat model backend diganti

1. Hindari mixed worker terlalu lama

Semakin lama worker lama dan baru hidup bersamaan, semakin besar peluang race condition. Jika harus rollout bertahap, batasi durasi overlap dan pastikan observability lengkap selama fase itu.

2. Drain worker lama dengan tegas

Sebelum worker lama dimatikan:

  • berhenti menerima job baru,
  • selesaikan job yang sudah diambil,
  • cabut lease yang kedaluwarsa,
  • pastikan tidak ada write tertunda.

Kesalahan umum adalah mematikan proses terlalu cepat lalu queue meretry job yang masih sempat berjalan setengah jalan.

3. Gunakan canary berbasis model_version

Canary yang baik bukan hanya membagi traffic, tetapi juga memungkinkan Anda membandingkan:

  • latency per versi model,
  • error dan timeout rate per versi,
  • cache hit ratio per versi,
  • jumlah duplicate attempt per versi.

Jika metrik tidak dipisahkan per versi, rollout akan terlihat “normal” padahal error hanya terjadi pada subset worker.

4. Sinkronkan konfigurasi secara eksplisit

Jangan mengandalkan worker memuat konfigurasi kapan saja. Gunakan mekanisme yang jelas: environment snapshot saat startup, config service dengan versi, atau deployment yang mengikat worker ke satu versi model selama masa hidup proses.

Masalah klasik terjadi ketika worker lama tiba-tiba menyegarkan config di tengah proses dan satu node dapat berperilaku berbeda antar request.

Common mistakes yang sering memperparah insiden

  • Menganggap timeout sama dengan gagal. Pada sistem terdistribusi, timeout sering berarti “tidak tahu”.
  • Menggunakan cache key tanpa model_version. Ini sumber inkonsistensi yang sangat umum.
  • Retry agresif tanpa backoff dan dedupe. Hasilnya justru menambah beban dan duplikasi.
  • Tidak menyimpan attempt/generation. Tanpa ini, late write sulit dicegah.
  • Observability terlalu minim. Jika log tidak memuat versi model dan idempotency key, penyelidikan akan lama.
  • Rollout dan invalidasi cache dilakukan sebagai proses terpisah tanpa koordinasi.

Checklist pencegahan sebelum mengganti model backend

  1. Pastikan cache key menyertakan model_version.
  2. Terapkan idempotency key yang stabil untuk request inferensi.
  3. Simpan attempt number, lease token, atau generation untuk mencegah late write.
  4. Pastikan retry memiliki dedupe dan backoff.
  5. Audit visibility timeout queue agar sesuai dengan durasi inferensi realistis.
  6. Tambahkan log/trace field: request_id, job_id, model_version, worker_id, cache_key, attempt.
  7. Gunakan namespace cache per versi model atau strategi cutover yang eksplisit.
  8. Drain worker lama sebelum cutover penuh.
  9. Monitor metrik per model_version, bukan agregat global saja.
  10. Uji skenario fault injection: timeout upstream, retry overlap, cache stale, dan worker crash.

Penutup

Race condition pada worker inference saat model backend diganti hampir selalu merupakan masalah desain transisi, bukan sekadar bug acak di satu fungsi. Jika hasil inferensi tidak konsisten, timeout sporadis meningkat, job terduplikasi, dan cache lama masih terbaca, periksa empat area utama: version skew, idempotensi, retry tanpa dedupe, dan invalidasi cache.

Pendekatan yang paling efektif biasanya bukan menambah retry atau memperbesar timeout, melainkan memperjelas kepemilikan eksekusi, membuat hasil inferensi sadar versi, dan memastikan attempt lama tidak bisa menimpa state yang lebih baru. Dengan observability yang tepat dan rollout yang disiplin, bug semacam ini bisa dipersempit dari “acak dan sulit direproduksi” menjadi urutan kejadian yang bisa dibuktikan dan diperbaiki.