Desain worker queue untuk sinkronisasi library media yang konsisten dibutuhkan ketika backend harus memproses banyak perubahan file, metadata, dan status pemutaran tanpa kehilangan update atau membuat data ganda. Dalam konteks aplikasi mirip klien terminal untuk Jellyfin atau Subsonic, masalah utamanya bukan hanya bagaimana membaca library, tetapi bagaimana menjaga backend tetap konsisten saat ada scan folder, perubahan metadata, refresh cache, dan sinkronisasi progres play yang datang bersamaan.

Solusi yang umum adalah memecah pekerjaan menjadi job asynchronous yang dijalankan worker queue. Namun queue saja tidak cukup. Tanpa idempotensi, deduplikasi, distributed lock, retry yang benar, dan strategi invalidasi cache yang jelas, sistem akan rentan terhadap metadata usang, progres play yang mundur, job macet, atau update library yang diproses dua kali.

Masalah yang Sering Muncul pada Sinkronisasi Library Media

Library media punya karakteristik yang membuat sinkronisasi lebih rumit dibanding CRUD biasa:

  • Sumber perubahan banyak: file scanner, webhook, polling API server media, aksi user, dan sinkronisasi perangkat.
  • Payload bisa besar: satu scan folder dapat menghasilkan ribuan item.
  • Urutan event tidak selalu terjamin: metadata lama bisa tiba setelah metadata baru.
  • Beberapa data lebih sensitif terhadap urutan: progres play dan last-played lebih berisiko konflik dibanding cache poster.
  • Pemrosesan campuran: ada job ringan seperti refresh cache, ada job berat seperti reindex metadata atau checksum file.

Jika semua perubahan ditangani secara sinkron dalam request utama, latensi naik dan kegagalan kecil mudah merambat. Jika semua dibuang ke queue tanpa desain yang disiplin, hasilnya adalah inkonsistensi yang lebih sulit dilacak.

Model Arsitektur Worker Queue yang Disarankan

Pisahkan alur sinkronisasi menjadi beberapa tipe job dengan tanggung jawab sempit. Hindari satu job besar yang melakukan scan, update metadata, invalidasi cache, dan sinkronisasi progres sekaligus.

Komponen Utama

  • Producer: API, scheduler, file watcher, atau sinkronisasi periodik yang membuat job.
  • Queue broker: Redis streams, RabbitMQ, SQS, atau sistem sejenis.
  • Worker: proses yang mengambil job dan mengeksekusi logika bisnis.
  • Database utama: sumber kebenaran untuk library, item media, user state, dan progres play.
  • Cache: hasil query yang sering dipakai, misalnya daftar album, cover URL, atau ringkasan library.
  • Store observabilitas: metrics, logs, tracing, dan tabel status job bila perlu.

Tipe Job yang Praktis

  1. ScanSourceJob: memindai sumber data, misalnya folder atau endpoint library server, lalu menghasilkan perubahan tingkat item.
  2. UpsertMediaItemJob: membuat atau memperbarui satu item media berdasarkan identifier stabil.
  3. RefreshMetadataJob: memperbarui field metadata yang berasal dari upstream atau parser lokal.
  4. RefreshCacheJob: menghapus atau membangun ulang cache turunan.
  5. SyncPlaybackProgressJob: menyimpan progres play user ke database atau meneruskannya ke server media.
  6. ReconcileLibraryJob: rekonsiliasi berkala untuk menemukan item yang terlewat, orphan record, atau cache yang tidak sinkron.

Pemisahan ini penting karena tiap job memiliki kebutuhan konsistensi, retry, dan prioritas yang berbeda.

Desain Data dan Kunci Idempotensi

Konsistensi worker queue sangat bergantung pada identifier yang tepat. Untuk library media, jangan memakai nama file atau title sebagai satu-satunya kunci bisnis jika sumber data bisa berubah. Gunakan identifier yang stabil, misalnya kombinasi source_id dan media_id, atau path kanonis bila memang itu satu-satunya identitas yang tersedia.

Prinsip Idempotensi

Worker harus aman jika job yang sama dijalankan lebih dari sekali. Ini bukan edge case; pada banyak sistem queue, at-least-once delivery adalah perilaku normal.

  • Upsert, bukan insert buta.
  • Bandingkan versi atau timestamp sebelum menulis.
  • Simpan operation key untuk menolak event yang sudah pernah diproses.
  • Pisahkan side effect agar notifikasi, cache invalidation, dan update indeks tidak terjadi dua kali tanpa kontrol.

Contoh sederhana skema tabel untuk progress sync:

playback_progress
- user_id
- media_id
- position_ms
- updated_at
- source_device_id
- source_event_id
- source_event_time

processed_events
- event_key
- processed_at
- result_hash

event_key dapat dibentuk dari kombinasi yang unik, misalnya device_id:session_id:sequence atau ID event dari upstream. Jika event yang sama datang lagi, worker cukup mengabaikannya.

Pola Job untuk Scan Metadata, Cache, dan Progres Play

1. Scan Metadata

Jangan jadikan scan sebagai satu transaksi besar yang menahan lock lama. Lebih aman memakai dua tahap:

  1. Discovery phase: daftar item ditemukan dari folder atau API.
  2. Materialization phase: tiap item diproses sebagai job terpisah atau batch kecil.

Keuntungan pendekatan ini:

  • Worker bisa diskalakan horizontal.
  • Kegagalan satu item tidak menggagalkan seluruh scan.
  • Retry lebih murah dan lebih terisolasi.
  • Tekanan ke database lebih mudah diatur.

Namun, Anda tetap butuh scan session atau generation marker. Ini dipakai untuk membedakan item yang masih valid dengan item yang seharusnya dihapus atau ditandai hilang setelah scan selesai.

Contoh alur:

  1. Scheduler membuat ScanSourceJob(source_id, scan_id).
  2. Job mengambil daftar item dari sumber.
  3. Untuk setiap item, producer membuat UpsertMediaItemJob(source_id, item_id, scan_id).
  4. Worker item melakukan upsert dan menyimpan last_seen_scan_id.
  5. Setelah semua item selesai, FinalizeScanJob menandai item lama yang tidak terlihat pada scan_id ini sebagai deleted, missing, atau archived.

2. Refresh Cache

Cache untuk library sering tergoda dibuat terlalu agresif: setiap update metadata langsung menghapus banyak key. Akibatnya, saat scan besar berjalan, cache mengalami thrash dan beban database melonjak.

Praktik yang lebih aman:

  • Invalidasi granular: hapus cache item atau koleksi yang benar-benar terdampak.
  • Gunakan versioned key untuk daftar besar, misalnya berbasis collection version atau updated_at bucket.
  • Coalesce refresh job: gabungkan beberapa invalidasi menjadi satu pekerjaan batch.
  • Jangan hitung ulang cache berat di jalur tulis bila tidak wajib.

Untuk library besar, sering lebih baik menerbitkan sinyal cache dirty lalu membiarkan worker terpisah membangun ulang cache secara bertahap.

3. Sinkronisasi Progres Play

Progres play berbeda dari metadata karena datanya sangat sering berubah dan sensitif terhadap urutan. Jika worker memproses event lama setelah event baru, posisi play bisa mundur.

Karena itu, simpan versi monotonik per stream event, misalnya:

  • timestamp event dari sumber yang dipercaya, atau
  • sequence number per device/session, atau
  • kombinasi timestamp dan aturan tie-break.

Aturan praktis:

  • Jangan update progres jika event lebih lama dari status tersimpan.
  • Jika beda waktunya kecil dan sumbernya tidak reliabel, terapkan aturan bisnis yang konservatif.
  • Bedakan event seek, pause, stop, dan finished jika backend membutuhkannya.
if incoming.event_time < stored.event_time:
    ignore()
elif incoming.event_time == stored.event_time and incoming.position_ms < stored.position_ms:
    ignore_or_apply_policy()
else:
    upsert_progress()

Logika ini sederhana, tetapi efektif untuk mencegah progres mundur akibat job terlambat atau diproses ulang.

Duplikasi Job, Race Condition, dan Distributed Lock

Mengapa Duplikasi Terjadi

  • Producer mengirim ulang karena timeout.
  • Worker berhasil memproses tetapi gagal mengakui job ke broker.
  • Scheduler overlap dan membuat scan yang sama dua kali.
  • Webhook upstream dikirim ulang.

Karena itu, desain queue harus mengasumsikan duplikasi akan terjadi.

Kapan Perlu Distributed Lock

Distributed lock berguna untuk operasi yang tidak aman dijalankan paralel pada entitas yang sama, misalnya:

  • scan pada source_id yang sama,
  • finalisasi scan yang mengubah status banyak item,
  • rebuild cache koleksi besar,
  • rekonsiliasi progres play per user-media bila ada lebih dari satu jalur sinkronisasi.

Namun lock bukan pengganti idempotensi. Lock mencegah konkurensi tertentu, tetapi tidak menyelesaikan redelivery atau retry.

Aturan Lock yang Aman

  • Lock per resource, bukan lock global.
  • TTL wajib ada untuk mencegah dead lock saat worker mati.
  • Perpanjang lock dengan hati-hati jika job panjang.
  • Validasi kepemilikan lock saat release.
  • Jangan menahan lock lebih lama dari yang diperlukan.

Contoh resource key:

lock:scan:source:{source_id}
lock:progress:user:{user_id}:media:{media_id}
lock:cache:collection:{collection_id}

Kesalahan umum adalah memakai lock untuk setiap job kecil, lalu throughput turun drastis. Gunakan lock hanya di titik yang memang membutuhkan serialisasi.

Retry, Backoff, Visibility Timeout, dan Dead-Letter Queue

Retry yang Benar

Tidak semua kegagalan layak di-retry. Bedakan:

  • Transient error: jaringan putus, upstream timeout, koneksi database sesaat penuh.
  • Permanent error: payload rusak, item tidak valid, constraint bisnis gagal.

Retry hanya untuk kesalahan transient. Untuk itu, pakai exponential backoff dengan jitter agar worker tidak menembak sistem yang sedang gangguan secara serentak.

attempt 1 -> delay pendek
attempt 2 -> delay lebih panjang
attempt 3 -> delay lebih panjang + jitter
max attempts tercapai -> kirim ke dead-letter queue

Visibility Timeout

Pada sistem queue dengan mekanisme lease, visibility timeout menentukan berapa lama job dianggap sedang diproses sebelum tersedia lagi untuk worker lain. Jika timeout terlalu pendek, job panjang akan diambil worker kedua dan diproses ganda. Jika terlalu panjang, job gagal akan lama terdeteksi.

Praktiknya:

  • set visibility timeout lebih besar dari waktu proses normal plus buffer,
  • untuk job panjang, gunakan perpanjangan lease secara periodik jika broker mendukung,
  • ukur runtime nyata, jangan menebak.

Dead-Letter Queue

Dead-letter queue wajib untuk memisahkan job yang gagal berulang dari alur normal. Tanpa DLQ, job buruk akan terus berputar dan mengganggu throughput.

Job yang masuk DLQ sebaiknya menyimpan:

  • payload asli,
  • attempt count,
  • error terakhir,
  • timestamp,
  • trace atau correlation ID.

DLQ bukan tempat membuang masalah. Anda tetap butuh proses triage, replay terkontrol, dan klasifikasi penyebab.

Eventual Consistency vs Strong Consistency

Tidak semua bagian sinkronisasi library media membutuhkan tingkat konsistensi yang sama. Memaksakan strong consistency pada seluruh alur sering membuat sistem lambat dan rapuh.

AreaEventual Consistency Cocok?Kapan Perlu Lebih Kuat
Metadata album/trackYa, biasanya aman terlambat beberapa detik/menitSaat ada operasi admin yang mengharuskan hasil langsung terlihat
Cache daftar libraryYaJarang perlu strong, kecuali untuk workflow khusus
Status item deleted/missingYa, dengan finalisasi scan yang benarJika downstream sangat sensitif pada item orphan
Progres play userSebagian, tetapi urutan event harus dijagaSaat update real-time antar perangkat harus nyaris seketika dan tidak boleh mundur
Entitlement atau hak aksesTidak idealBiasanya butuh konsistensi lebih kuat

Prinsip praktisnya: gunakan eventual consistency untuk metadata dan cache, tetapi buat aturan ordering dan conflict resolution yang lebih ketat untuk progres play.

Contoh Alur End-to-End yang Aman

Alur Scan Library

  1. Scheduler membuat ScanSourceJob dengan kunci deduplikasi per source.
  2. Worker mengambil distributed lock lock:scan:source:{source_id}.
  3. Job membuat scan_id baru dan mencatat status scan = running.
  4. Daftar item diambil dari upstream secara bertahap.
  5. Untuk tiap item, sistem membuat UpsertMediaItemJob dengan idempotency key berbasis source_id + item_id + scan_id atau versi item.
  6. Worker item menulis ke database dengan upsert dan memperbarui last_seen_scan_id.
  7. Jika metadata berubah, worker menerbitkan RefreshCacheJob yang bisa di-coalesce.
  8. Setelah item selesai, FinalizeScanJob menandai item yang tidak terlihat pada scan ini.
  9. Lock dilepas, scan ditandai complete, metrik diperbarui.

Alur Sinkronisasi Progres Play

  1. Client atau upstream mengirim event progres.
  2. API menyimpan event mentah atau langsung membuat SyncPlaybackProgressJob.
  3. Worker memeriksa apakah event_key sudah pernah diproses.
  4. Jika belum, worker mengambil lock sempit per user_id + media_id bila diperlukan.
  5. Worker membandingkan event_time dengan status tersimpan.
  6. Jika event lebih baru, lakukan upsert progres dan tandai event processed.
  7. Jika perlu, kirim update ke cache sesi atau notifikasi perangkat lain.

Tabel Trade-off Desain

PilihanKelebihanKekuranganCocok Saat
Job besar per scanSederhana secara implementasi awalSulit di-retry, lock lama, blast radius besarLibrary kecil dan kebutuhan operasional rendah
Job kecil per itemSkalabel, retry terisolasi, observabilitas lebih baikButuh deduplikasi dan orkestrasi lebih rapiLibrary besar atau worker horizontal
Lock globalMudah dipahamiMembunuh paralelismeHampir tidak pernah ideal
Lock per resourceLebih efisien dan amanPerlu desain key yang tepatMayoritas kasus produksi
Retry tanpa jitterMudahMemicu thundering herdTidak disarankan
Retry dengan backoff + jitterLebih stabil saat upstream gangguanLatensi pemulihan lebih lamaSistem produksi dengan beban nyata
Invalidate cache langsungData cepat segarThrash saat update massalUpdate kecil dan jarang
Coalesced cache refreshLebih hemat bebanData bisa sedikit lebih lama usangScan besar atau metadata sering berubah

Gejala Insiden dan Cara Membacanya

Gejala: Item Library Muncul Ganda

  • Penyebab umum: identifier tidak stabil, insert tanpa upsert, scan paralel pada source sama.
  • Cek: constraint unik, key deduplikasi, log scan overlap, apakah path berubah format.
  • Perbaikan: normalisasi identifier, gunakan upsert, tambah lock per source.

Gejala: Progres Play Sering Mundur

  • Penyebab umum: event diproses out-of-order, tidak ada version check, lebih dari satu worker menulis entitas sama.
  • Cek: event_time, sequence, runtime job, visibility timeout terlalu pendek.
  • Perbaikan: monotonic ordering, idempotency key, lock sempit, compare-and-set update.

Gejala: Cache Library Tidak Pernah Segar

  • Penyebab umum: invalidasi hilang, refresh job gagal senyap, TTL terlalu panjang, key cache tidak sinkron dengan update model.
  • Cek: rasio cache miss/hit yang aneh, antrean refresh menumpuk, error worker cache.
  • Perbaikan: event invalidasi eksplisit, DLQ untuk refresh, versioned cache key.

Gejala: Queue Terlihat Sehat, tetapi Update Hilang

  • Penyebab umum: job dianggap sukses padahal side effect kedua gagal, transaksi database dan publish event tidak atomik.
  • Cek: apakah update utama dan enqueue lanjutan berada dalam pola outbox atau mekanisme setara.
  • Perbaikan: gunakan transaksi yang jelas, pola outbox untuk event lanjutan, audit trail perubahan.

Metrik yang Wajib Dipantau

Tanpa metrik, Anda hanya tahu queue kosong atau penuh. Itu tidak cukup. Untuk sinkronisasi library media, pantau minimal:

Metrik Queue

  • queue depth per tipe job,
  • oldest job age,
  • throughput job masuk vs selesai,
  • retry rate,
  • DLQ rate,
  • processing latency per job type.

Metrik Konsistensi

  • duplicate suppression count,
  • lock contention rate,
  • stale progress overwrite prevented,
  • cache invalidation lag,
  • scan completion time,
  • items seen vs items finalized per scan.

Metrik Kesehatan Worker

  • crash/restart worker,
  • runtime rata-rata dan p95 job,
  • timeout count,
  • kegagalan koneksi ke DB, cache, dan upstream.

Tambahkan correlation ID atau scan_id di seluruh log agar satu insiden dapat ditelusuri lintas producer, worker, DB, dan cache.

Praktik Implementasi yang Sangat Dianjurkan

  • Gunakan upsert dengan constraint unik pada identifier bisnis yang stabil.
  • Tambahkan idempotency key untuk event yang bisa dikirim ulang.
  • Pisahkan queue berdasarkan karakteristik beban, misalnya metadata berat, cache refresh, dan progres play real-time.
  • Atur concurrency per queue agar scan besar tidak melaparkan job progres play.
  • Tentukan SLA per job type; progres play biasanya lebih sensitif terhadap delay daripada refresh cache.
  • Gunakan backoff dengan jitter untuk retry transient.
  • Definisikan error yang non-retryable dengan tegas.
  • Siapkan rekonsiliasi periodik untuk menemukan update yang lolos dari jalur utama.
  • Uji skenario redelivery, worker crash setelah write sebelum ack, dan lock expiry saat job panjang.
  • Jangan andalkan cache sebagai source of truth.

Checklist Implementasi agar Update Tidak Hilang atau Ganda

  1. Apakah setiap entitas media memiliki identifier stabil dan constraint unik?
  2. Apakah setiap job penting bersifat idempotent?
  3. Apakah event progres play punya ordering field yang dapat dipercaya?
  4. Apakah scan pada source yang sama dicegah overlap dengan lock atau dedupe?
  5. Apakah visibility timeout sesuai dengan runtime nyata job?
  6. Apakah retry dibatasi dan memakai backoff + jitter?
  7. Apakah job gagal permanen dipindahkan ke dead-letter queue?
  8. Apakah cache invalidation punya jalur observabilitas dan tidak diam-diam gagal?
  9. Apakah queue untuk job real-time dipisahkan dari job bulk?
  10. Apakah ada metrik duplicate, retry, DLQ, dan lock contention?
  11. Apakah ada proses rekonsiliasi berkala untuk memperbaiki drift?
  12. Apakah update database dan penerbitan job lanjutan memakai pola yang mencegah kehilangan event?

Penutup

Desain worker queue untuk sinkronisasi library media yang konsisten bukan soal memilih broker queue tertentu, melainkan soal disiplin pada idempotensi, ordering, locking yang sempit, retry yang benar, dan observabilitas. Untuk metadata dan cache, eventual consistency biasanya cukup. Untuk progres play, Anda perlu aturan konflik yang lebih ketat agar status tidak mundur atau tertimpa event lama.

Jika Anda membangun backend untuk ekosistem media seperti Jellyfin atau Subsonic, pisahkan tipe job sejak awal, ukur perilaku runtime secara nyata, dan siapkan jalur pemulihan saat job pasti diduplikasi atau gagal. Dengan begitu, update library tidak hilang, progres play tidak ganda, dan operasional harian jauh lebih mudah dikendalikan.