Debug race condition upload chunk pada editor video browser biasanya tidak gagal secara total. Justru yang sering muncul adalah gejala parsial: beberapa chunk hilang, proses render berhenti di status tertentu, file gabungan menjadi korup, atau bug hanya muncul sesekali saat trafik tinggi atau saat browser diotomasi oleh agent. Itu sebabnya masalah ini sulit direproduksi dan sering disangka bug acak di jaringan atau storage.

Pada studi kasus ini, kita bahas pola bug backend yang terinspirasi dari alur editor video browser seperti FableCut: klien mengirim banyak chunk untuk satu aset video, server menyimpan chunk, menandai progres, lalu memicu proses merge dan render. Akar masalahnya ternyata kombinasi dari idempotensi lemah, nomor chunk yang datang out-of-order, retry ganda dari agent/otomasi, dan update status yang tidak atomik di database.

Gejala Nyata di Produksi

Race condition pada upload chunk jarang terlihat seperti error tunggal yang jelas. Gejala yang muncul biasanya tersebar di beberapa lapisan sistem.

1. Chunk hilang padahal klien merasa semua request sukses

Di sisi klien, semua request upload bisa saja menerima HTTP 200 atau 201. Namun saat file akhir dirakit, server menemukan ada indeks chunk yang belum pernah tersimpan, atau satu chunk tertimpa data berbeda akibat retry yang tidak dideduplikasi.

2. Status render macet

Pipeline umum biasanya bergerak dari uploadinguploadedmergingready_for_renderrendering. Jika update status dilakukan dari beberapa worker atau endpoint tanpa guard yang konsisten, status dapat mundur atau tertahan. Contoh: satu proses menandai aset uploaded setelah menghitung jumlah chunk, sementara proses lain baru menyimpan chunk terakhir beberapa milidetik kemudian. Hasilnya, job merge jalan dengan data belum lengkap atau justru tidak jalan sama sekali.

3. File akhir korup

Korupsi file sering terjadi bukan karena encoding, melainkan karena urutan penggabungan chunk salah, ada chunk duplikat, atau satu chunk ditulis parsial lalu dianggap final. Jika chunk ke-12 tiba lebih dulu dari chunk ke-11 dan sistem menyusun berdasarkan waktu simpan, bukan berdasarkan indeks chunk, hasil akhirnya akan rusak meskipun semua bagian tampak ada.

4. Bug sulit direproduksi

Pada laptop developer dengan jaringan stabil dan satu tab browser, bug hampir tidak muncul. Namun saat editor dijalankan agent, headless browser, worker paralel, atau ada mekanisme retry otomatis, urutan request menjadi jauh lebih bervariasi. Inilah kondisi yang memunculkan race condition.

Arsitektur Singkat dan Titik Rawan

Alur sederhananya biasanya seperti ini:

  1. Klien membuat sesi upload untuk satu video atau asset.
  2. Klien memecah file menjadi banyak chunk.
  3. Setiap chunk dikirim dengan metadata seperti upload_id, chunk_index, dan jumlah total chunk.
  4. Backend menyimpan chunk ke disk atau object storage.
  5. Backend memperbarui progres upload di database.
  6. Setelah dianggap lengkap, backend memicu merge dan render.

Titik rawan muncul ketika beberapa request untuk upload_id yang sama datang hampir bersamaan dan backend mengasumsikan bahwa:

  • request datang berurutan,
  • satu chunk hanya dikirim sekali,
  • status bisa diperbarui kapan saja tanpa konflik,
  • penghitungan jumlah chunk yang tersimpan selalu konsisten pada saat dicek.

Asumsi-asumsi itu sering salah di sistem nyata.

Root Cause yang Sering Muncul

Idempotensi endpoint lemah

Jika endpoint POST /upload/chunk selalu membuat record baru tanpa kunci unik untuk kombinasi upload_id + chunk_index, maka retry dari klien akan menghasilkan duplikasi. Lebih buruk lagi, jika dua request untuk chunk yang sama menulis ke path file yang sama secara bersamaan, hasil file bisa tidak valid.

Chunk datang out-of-order

Browser, jaringan, dan agent otomasi bisa mengirim atau menyelesaikan request tidak sesuai urutan indeks chunk. Sistem yang benar harus menganggap urutan kedatangan tidak penting. Yang penting adalah integritas setiap chunk dan kelengkapan himpunan chunk berdasarkan indeks.

Retry ganda dari agent/otomasi

Agent otomatis sering punya lapisan retry sendiri. Misalnya, browser retry karena timeout, reverse proxy retry koneksi upstream, lalu worker internal juga mencoba ulang. Jika backend tidak memakai token idempotensi atau deduplikasi berbasis kunci unik, satu chunk dapat diproses dua atau tiga kali.

Update status non-atomik

Pola berikut sering menjadi sumber bug:

  1. Simpan chunk.
  2. Hitung jumlah chunk tersimpan.
  3. Jika jumlah sama dengan total, update status menjadi uploaded.
  4. Trigger job merge.

Masalahnya, langkah-langkah itu sering dijalankan di transaksi terpisah atau bahkan di proses berbeda. Dua request terakhir bisa sama-sama merasa menjadi chunk final dan memicu merge dua kali. Atau sebaliknya, masing-masing melihat snapshot data yang belum lengkap lalu tidak ada yang memicu merge.

Strategi Investigasi: Jangan Mulai dari Dugaan, Mulai dari Korelasi

Untuk bug race condition, log biasa seperti “chunk uploaded successfully” hampir tidak berguna. Kita perlu merekonstruksi urutan kejadian secara deterministik.

1. Tambahkan correlation ID dan idempotency key

Minimal, setiap request chunk perlu membawa atau dibangkitkan dengan field berikut di log:

  • request_id
  • upload_id
  • asset_id
  • chunk_index
  • total_chunks
  • chunk_checksum jika tersedia
  • attempt_no atau indikator retry
  • agent_session_id bila berasal dari otomasi

Dengan ini kita bisa membedakan antara chunk benar-benar hilang, chunk datang dua kali, atau chunk diproses tetapi status akhirnya tertimpa event lain.

2. Log lifecycle, bukan hanya hasil akhir

Catat beberapa event penting:

  • request diterima,
  • validasi metadata lulus/gagal,
  • write ke storage dimulai/selesai,
  • upsert metadata chunk berhasil,
  • cek kelengkapan chunk,
  • enqueue merge job,
  • status aset berubah dari A ke B.

Tanpa event-event ini, Anda hanya melihat gejala akhir, bukan momen balapannya.

3. Query validasi data di database

Jika Anda menyimpan metadata chunk di tabel relasional, beberapa query berikut sangat membantu untuk audit integritas.

-- Deteksi chunk duplikat untuk satu upload_id dan chunk_index tertentu
SELECT upload_id, chunk_index, COUNT(*) AS n
FROM upload_chunks
GROUP BY upload_id, chunk_index
HAVING COUNT(*) > 1;

-- Cari upload yang jumlah chunk uniknya tidak sama dengan total yang diharapkan
SELECT u.upload_id,
       u.expected_total_chunks,
       COUNT(DISTINCT c.chunk_index) AS stored_chunks
FROM uploads u
LEFT JOIN upload_chunks c ON c.upload_id = u.upload_id
GROUP BY u.upload_id, u.expected_total_chunks
HAVING COUNT(DISTINCT c.chunk_index) <> u.expected_total_chunks;

-- Cari chunk index yang hilang untuk satu upload tertentu
-- Implementasinya bergantung pada database, tapi idenya adalah membandingkan
-- rentang 0..N-1 dengan chunk yang benar-benar tersimpan.

Jika metadata chunk disimpan di object storage saja, buat indeks metadata terpisah di database atau key-value store. Mengandalkan list objects untuk validasi real-time biasanya lambat dan rentan inkonsistensi sementara.

4. Cocokkan log API dengan job queue

Kasus status render macet sering terjadi karena job merge tidak pernah benar-benar masuk ke antrean, atau masuk dua kali lalu salah satunya gagal dan menimpa status. Korelasikan upload_id yang sama di:

  • access log / API log,
  • log worker queue,
  • event status di database,
  • log storage write / object storage callback jika ada.

5. Reproduksi dengan parallel request terkontrol

Bug yang tidak muncul secara manual bisa dipancing dengan test harness sederhana yang:

  • mengirim chunk out-of-order,
  • mengirim chunk yang sama dua kali hampir bersamaan,
  • menyuntikkan delay acak sebelum update status,
  • memutus koneksi setelah body terkirim sebagian untuk memicu retry.

Tujuannya bukan membuat simulasi sempurna, melainkan menabrakkan asumsi backend Anda.

Contoh Desain Endpoint yang Lebih Aman

Perbaikan paling penting adalah menjadikan penyimpanan chunk idempotent dan transisi status atomik.

Prinsip desain

  • Satu chunk diidentifikasi unik oleh upload_id + chunk_index.
  • Retry untuk chunk yang sama tidak boleh membuat duplikasi atau menimpa data berbeda tanpa deteksi.
  • Merge hanya boleh dipicu sekali untuk satu upload_id.
  • Status tidak boleh maju atau mundur sembarangan dari banyak proses.

Skema metadata minimal

uploads
- upload_id (unique)
- asset_id
- expected_total_chunks
- received_chunk_count
- status
- merge_started_at
- merge_completed_at
- version

upload_chunks
- upload_id
- chunk_index
- storage_key
- size_bytes
- checksum
- created_at
- PRIMARY KEY (upload_id, chunk_index)

Kunci utama atau unique constraint pada (upload_id, chunk_index) adalah fondasi deduplikasi. Tanpa ini, retry ganda akan terus bocor ke sistem.

Pola upsert chunk

Alih-alih selalu INSERT, lakukan logika seperti ini:

  1. Validasi metadata chunk.
  2. Tulis isi chunk ke lokasi sementara yang deterministik, misalnya berdasarkan upload_id/chunk_index.
  3. Catat metadata chunk dengan upsert.
  4. Jika record sudah ada, verifikasi checksum atau ukuran. Jika berbeda, anggap konflik data, bukan sukses biasa.
// Pseudocode
function handleChunkUpload(req) {
  const key = `${req.uploadId}/${req.chunkIndex}`;

  writeChunkAtomically(key, req.body);

  const existing = findChunk(req.uploadId, req.chunkIndex);
  if (!existing) {
    insertChunk({
      uploadId: req.uploadId,
      chunkIndex: req.chunkIndex,
      storageKey: key,
      checksum: req.checksum,
      sizeBytes: req.sizeBytes
    });
    incrementReceivedChunkCountIfNeeded(req.uploadId, req.chunkIndex);
  } else {
    if (existing.checksum !== req.checksum || existing.sizeBytes !== req.sizeBytes) {
      throw ConflictError('Chunk index sama tetapi konten berbeda');
    }
    // idempotent retry: balas sukses tanpa efek samping baru
  }

  tryScheduleMerge(req.uploadId);
}

Catatan penting: incrementReceivedChunkCountIfNeeded tidak boleh selalu menambah counter. Counter hanya boleh bertambah saat chunk baru pertama kali tercatat. Jika tidak, retry akan membuat progres palsu.

Locking, Deduplikasi, dan Transisi Status Atomik

Kapan perlu locking

Jika beberapa request terakhir bisa sama-sama mencoba memicu merge, gunakan salah satu pendekatan berikut:

  • Database row lock pada record uploads saat memutuskan apakah merge perlu dijadwalkan.
  • Optimistic concurrency dengan kolom version atau update bersyarat.
  • Distributed lock jika keputusan tersebar di beberapa instance dan tidak nyaman dipegang sepenuhnya oleh database.

Pilih solusi sesederhana mungkin. Jika semua metadata upload memang sudah hidup di database relasional, row lock atau update bersyarat biasanya cukup dan lebih mudah diaudit.

Contoh update bersyarat untuk trigger merge sekali saja

-- Gagasan umum, sintaks bisa berbeda antar database
UPDATE uploads
SET status = 'merging', merge_started_at = NOW()
WHERE upload_id = :upload_id
  AND status = 'uploaded'
  AND received_chunk_count = expected_total_chunks;

Jika jumlah baris yang ter-update adalah 1, berarti proses ini resmi memenangkan hak untuk memulai merge. Jika 0, maka ada dua kemungkinan: data belum lengkap, atau proses lain sudah lebih dulu memulai merge. Dengan pola ini, kita menghindari enqueue merge dua kali.

Jangan gunakan status sebagai counter implisit

Kesalahan umum adalah menganggap jika status sudah uploaded, berarti semua chunk pasti ada. Status hanyalah ringkasan, bukan sumber kebenaran tunggal. Sumber kebenaran tetap metadata chunk yang tervalidasi.

Tulis file secara atomik

Jika chunk ditulis ke filesystem lokal atau volume bersama, hindari menulis langsung ke path final tanpa mekanisme atomik. Tulis ke file sementara, flush bila perlu, lalu rename/move ke nama final. Ini mengurangi peluang proses lain membaca file yang belum selesai ditulis.

Catatan: Atomic rename umumnya andal pada filesystem lokal yang sama, tetapi perilakunya dapat berbeda pada storage jaringan atau object storage. Jika memakai object storage, lebih aman memperlakukan object sebagai immutable dan memvalidasi metadata setelah upload selesai.

Perbaikan pada Pipeline Merge dan Render

Verifikasi kelengkapan sebelum merge

Jangan memulai merge hanya berdasarkan received_chunk_count == expected_total_chunks. Counter bisa salah akibat bug lama atau retry. Lakukan verifikasi ringan:

  • cek semua indeks 0..N-1 tersedia,
  • cek tidak ada duplikat,
  • cek ukuran atau checksum jika tersedia.

Counter tetap berguna sebagai sinyal cepat, tetapi bukan satu-satunya syarat.

Urutkan berdasarkan chunk_index, bukan waktu kedatangan

Ini terdengar sepele, tetapi banyak bug file korup berasal dari daftar chunk yang diambil berdasarkan created_at atau urutan nama object yang tidak sesuai indeks numerik. Pastikan merge memakai ORDER BY chunk_index ASC.

Job merge harus idempotent

Jika queue mengantarkan job dua kali, worker merge tidak boleh menghasilkan dua file final yang saling balapan memperbarui status. Simpan penanda seperti merge_started_at atau status='merging' dengan update bersyarat sebelum kerja berat dimulai.

Contoh Alur yang Lebih Tahan Race Condition

  1. Klien membuat upload_id.
  2. Setiap chunk dikirim dengan upload_id, chunk_index, total_chunks, dan checksum opsional.
  3. Server menulis chunk ke key deterministik upload_id/chunk_index.
  4. Server melakukan upsert metadata chunk dengan unique key (upload_id, chunk_index).
  5. Jika chunk baru berhasil dicatat, server memperbarui received_chunk_count secara aman.
  6. Server mencoba transisi uploaded -> merging hanya jika semua syarat terpenuhi dan update bersyarat berhasil.
  7. Worker merge memverifikasi ulang daftar chunk, menggabungkan berdasarkan indeks, lalu memperbarui status ke tahap berikutnya secara atomik.

Pola ini bekerja karena setiap keputusan penting memiliki sumber kebenaran tunggal dan guard terhadap eksekusi ganda.

Kesalahan Umum yang Sering Lolos Review

  • Menggunakan nama file chunk berbasis timestamp sehingga retry menghasilkan file berbeda untuk chunk yang sama.
  • Menganggap HTTP sukses berarti chunk sudah persisten, padahal write ke storage gagal setelah response dikirim.
  • Menyimpan progres upload hanya sebagai persentase, tanpa metadata chunk per indeks.
  • Memicu merge dari lebih dari satu tempat: endpoint upload, scheduler, dan callback lain tanpa koordinasi.
  • Menjalankan cleanup chunk terlalu agresif sehingga chunk valid terhapus sebelum merge benar-benar mulai.
  • Tidak membedakan duplicate retry yang aman dengan konflik data pada chunk index yang sama.

Checklist Investigasi di Produksi

  • Apakah ada unique constraint untuk upload_id + chunk_index?
  • Apakah retry untuk chunk yang sama menghasilkan efek samping baru?
  • Apakah urutan merge didasarkan pada indeks chunk?
  • Apakah status upload bisa ditulis dari beberapa proses tanpa guard?
  • Apakah enqueue merge bisa terjadi lebih dari sekali?
  • Apakah log memiliki request_id dan upload_id yang konsisten?
  • Apakah ada mismatch antara jumlah chunk tersimpan dan jumlah chunk unik?
  • Apakah worker atau reverse proxy melakukan retry yang tidak terlihat di level aplikasi?

Monitoring Pasca-Fix

Setelah perbaikan dirilis, jangan hanya menunggu komplain pengguna. Tambahkan metrik yang langsung mengarah ke regresi race condition.

Metrik yang layak dipantau

  • jumlah konflik unique key pada (upload_id, chunk_index),
  • jumlah retry idempotent yang diterima,
  • jumlah upload dengan missing chunk,
  • jumlah merge job duplikat yang ditolak,
  • distribusi durasi status uploading, uploaded, dan merging,
  • jumlah file final yang gagal validasi checksum atau durasi media.

Alert yang praktis

  • Status uploading terlalu lama untuk upload yang seharusnya kecil.
  • Lonjakan konflik chunk index yang sama dalam interval singkat.
  • Merge gagal dengan alasan indeks chunk hilang.
  • Rasio upload selesai tetapi render tidak pernah dimulai.

Tambahkan audit sampling

Ambil sampel acak dari upload yang baru selesai, lalu verifikasi:

  • jumlah chunk cocok dengan metadata,
  • tidak ada indeks hilang,
  • durasi file final masuk akal dibanding metadata klien.

Sampling ini berguna untuk menangkap korupsi halus yang tidak selalu meledak sebagai error eksplisit.

Penutup

Bug race condition pada upload chunk editor video browser hampir selalu tampak seperti masalah acak, padahal akar penyebabnya sistematis: idempotensi yang lemah, urutan chunk yang tidak dijadikan invariant, retry ganda, dan transisi status yang tidak atomik. Solusi yang efektif bukan sekadar menambah retry atau timeout, melainkan memperkuat model data dan aturan state transition.

Jika Anda sedang melakukan debug race condition upload chunk pada editor video browser, mulailah dari korelasi request, validasi metadata chunk, dan audit transisi status. Setelah itu, perbaiki endpoint agar idempotent, gunakan deduplikasi berbasis kunci unik, dan pastikan merge hanya bisa dimulai sekali. Dengan langkah ini, gejala seperti chunk hilang, status render macet, dan file akhir korup biasanya berhenti menjadi bug misterius dan berubah menjadi kasus yang bisa dijelaskan, diuji, dan dicegah.