Queue dan lock untuk sinkronisasi file kolaboratif digunakan untuk memisahkan pekerjaan berat dari request pengguna, menjaga urutan perubahan penting, dan mencegah race condition ketika beberapa klien mengubah file atau metadata yang sama. Tanpa desain ini, operasi seperti upload versi baru, rename, share, komentar, notifikasi, dan update cache dapat saling menimpa atau terlihat tidak konsisten antar perangkat.
Konteksnya relevan dengan tren aplikasi kolaborasi seperti Nextcloud Hub 26 Spring, yang menggabungkan file sharing, kolaborasi dokumen, komunikasi, dan notifikasi dalam satu pengalaman. Artikel ini bukan rangkuman fitur Nextcloud, melainkan panduan desain backend untuk sistem serupa: bagaimana membuat sinkronisasi file, notifikasi, dan perubahan metadata tetap aman, terurut, dan dapat dioperasikan.
Masalah yang Perlu Diselesaikan
Pada sistem file kolaboratif, satu tindakan pengguna sering memicu banyak efek samping. Misalnya, upload file dapat memperbarui versi objek, metadata database, indeks pencarian, cache direktori, kuota pengguna, aktivitas terbaru, dan notifikasi ke kolaborator.
Masalah umum yang muncul:
- Race condition: dua klien mengunggah versi berbeda pada file yang sama hampir bersamaan.
- Out-of-order event: event rename diproses sebelum event create, sehingga worker gagal menemukan objek.
- Duplikasi job: producer mengirim job dua kali karena retry HTTP atau timeout.
- Cache stale: daftar file masih menampilkan nama lama setelah rename berhasil.
- Notifikasi ganda: pengguna menerima beberapa notifikasi untuk perubahan yang sama.
- Worker lambat: queue menumpuk, lock tertahan, dan UI tampak tidak sinkron.
Queue membantu mengatur pekerjaan asynchronous, sedangkan lock membatasi bagian kritis agar hanya satu proses yang mengubah state tertentu dalam satu waktu.
Arsitektur Backend yang Praktis
Desain yang umum dan cukup aman adalah memisahkan operasi sinkron dan asynchronous:
- API menerima request dari klien, melakukan validasi izin, dan menulis state utama ke database dalam transaksi.
- API membuat event ke tabel outbox dalam transaksi yang sama.
- Publisher membaca outbox dan mengirim job ke queue.
- Worker memproses job: update indeks, invalidasi cache, kirim notifikasi, hitung kuota, atau sinkronisasi metadata turunan.
- Worker mencatat status idempotency agar job yang sama tidak diproses dua kali.
Prinsip penting: jangan mengandalkan queue sebagai satu-satunya sumber kebenaran. State utama tetap berada di database atau storage metadata yang transaksional. Queue adalah mekanisme pengiriman kerja, bukan pengganti model data yang konsisten.
Outbox Pattern untuk Konsistensi
Jika API menulis metadata ke database lalu mengirim pesan ke queue di luar transaksi, ada risiko metadata tersimpan tetapi job gagal terkirim. Sebaliknya, jika queue terkirim lebih dulu lalu transaksi database gagal, worker menerima event untuk state yang tidak pernah ada. Outbox pattern mengurangi masalah ini dengan menyimpan event di tabel yang sama dengan transaksi domain.
transaction:
update files set name = 'proposal-final.docx', version = version + 1
insert into outbox_events(
event_id,
aggregate_type,
aggregate_id,
event_type,
version,
payload,
created_at
) values (...)
commit
publisher:
read unpublished outbox_events ordered by created_at
publish to queue
mark event as publishedWorker tetap harus idempotent karena publisher dapat mengirim pesan lebih dari sekali, terutama jika crash setelah publish tetapi sebelum menandai outbox sebagai published.
Memilih Queue: Apa yang Perlu Dipertimbangkan
Pemilihan queue bergantung pada kebutuhan durability, ordering, throughput, dan operasional. Tidak ada satu pilihan yang selalu benar.
- Redis-based queue: sederhana dan cepat untuk job background umum. Cocok jika tim sudah mengoperasikan Redis dan kebutuhan durability tidak seketat log event permanen.
- RabbitMQ atau broker AMQP: cocok untuk routing pesan yang lebih ekspresif, acknowledgement, retry, dan pemisahan antrean berdasarkan jenis kerja.
- Kafka atau log terpartisi: cocok ketika ordering per key, replay event, dan audit log jangka panjang menjadi kebutuhan utama.
- Managed queue cloud: mengurangi beban operasional, tetapi perhatikan batas ukuran pesan, semantic delivery, visibility timeout, dan biaya.
Untuk sinkronisasi file kolaboratif, pertanyaan yang lebih penting dari sekadar nama tool adalah:
- Apakah queue mendukung acknowledgement sehingga job tidak hilang saat worker mati?
- Apakah ada mekanisme retry dan dead letter queue?
- Bisakah ordering dijaga per
file_idatau peruser_id? - Apakah job bisa diproses ulang tanpa merusak state?
- Bagaimana cara mengukur lag, depth, dan waktu proses job?
Partisi Berdasarkan File atau User
Jika semua event masuk satu queue global, throughput rendah. Jika semua event diproses paralel tanpa aturan, ordering bisa rusak. Kompromi yang umum adalah menggunakan partition key.
- Gunakan
file_idsebagai key untuk event konten dan metadata file, seperti upload versi, rename, move, delete, restore. - Gunakan
user_idsebagai key untuk state personal, seperti notifikasi terbaca, daftar recent files, atau quota recalculation. - Gunakan
share_idataufolder_idjika konflik terutama terjadi pada share atau folder tertentu.
Dengan partisi, event untuk file yang sama diproses berurutan, sementara file lain tetap dapat diproses paralel. Namun, ordering lintas file tidak otomatis terjamin. Jika operasi folder memengaruhi banyak file, desain perlu event khusus atau transaksi metadata yang jelas.
Desain Payload Job yang Aman
Payload job sebaiknya kecil, stabil, dan cukup untuk mengambil state terbaru dari database. Hindari memasukkan seluruh isi metadata yang mudah usang kecuali memang dibutuhkan sebagai snapshot audit.
{
event_id: 'evt_01H...',
job_type: 'file.metadata.changed',
aggregate_type: 'file',
aggregate_id: 'file_123',
actor_user_id: 'user_9',
tenant_id: 'workspace_7',
version: 42,
idempotency_key: 'file_123:metadata:42',
dedupe_key: 'file_123:rename:proposal-final.docx',
occurred_at: 'server timestamp',
correlation_id: 'request trace id'
}Field yang biasanya berguna:
- event_id: identitas unik event. Berguna untuk tracing dan idempotency.
- aggregate_id: ID entitas utama, misalnya file, folder, share, atau user.
- version: versi monotonik dari entitas. Membantu mendeteksi event lama.
- idempotency_key: kunci untuk memastikan efek samping tidak terjadi dua kali.
- dedupe_key: kunci untuk menggabungkan event sejenis dalam jendela waktu tertentu.
- correlation_id: menghubungkan request API, outbox, job, log worker, dan notifikasi.
Idempotency Key
Worker harus aman jika job yang sama diproses ulang. Caranya adalah menyimpan idempotency_key pada tabel atau storage yang memiliki unique constraint. Jika insert gagal karena key sudah ada, worker tahu efek job tersebut sudah pernah dilakukan atau sedang diproses.
function processJob(job):
if processed_jobs.exists(job.idempotency_key):
return ack
begin transaction
insert into processed_jobs(idempotency_key, status) values(job.idempotency_key, 'processing')
current = select file where id = job.aggregate_id
if current.version < job.version:
raise RetryLater('metadata not visible yet')
if current.version > job.version:
mark processed_jobs as skipped_old_event
commit
return ack
apply_side_effects(job, current)
mark processed_jobs as completed
commitPerhatikan bahwa idempotency tidak hanya berarti menghindari duplikasi baris log. Efek samping eksternal seperti push notification, email, webhook, dan indexing juga harus memiliki kunci unik atau mekanisme upsert.
Deduplication
Deduplication berbeda dari idempotency. Idempotency mencegah job yang sama dieksekusi dua kali. Deduplication menggabungkan beberapa job berbeda yang hasil akhirnya sama atau cukup diproses sekali.
Contoh: pengguna mengganti nama file tiga kali dalam dua detik. Sistem mungkin tidak perlu mengirim tiga notifikasi rename. Worker dapat menunda sebentar atau aggregator dapat menggabungkan event berdasarkan dedupe_key dan mengambil state terbaru dari database sebelum mengirim notifikasi.
Trade-off-nya adalah latensi. Semakin agresif deduplication, semakin besar kemungkinan UI atau notifikasi tertunda. Untuk perubahan konten file, deduplication harus hati-hati agar tidak menghilangkan versi yang perlu disimpan.
Locking: Per-File dan Per-User
Lock digunakan untuk melindungi bagian kritis, bukan untuk menutupi desain data yang tidak konsisten. Pada sinkronisasi file kolaboratif, dua jenis lock yang sering dibutuhkan adalah per-file lock dan per-user lock.
Per-File Lock
Per-file lock cocok untuk operasi yang mengubah state file yang sama:
- menulis versi konten baru;
- rename, move, delete, restore;
- mengubah metadata sensitif seperti checksum, ukuran, parent folder, dan version counter;
- membangun preview atau indeks yang harus cocok dengan versi tertentu.
Kunci lock dapat berupa lock:file:{file_id}. Gunakan TTL agar lock tidak tertahan selamanya jika worker mati.
Per-User Lock
Per-user lock cocok untuk state yang spesifik pengguna:
- update kuota;
- status notifikasi terbaca;
- daftar recent files;
- preferensi sinkronisasi;
- fan-out notifikasi ke perangkat milik user yang sama.
Kunci lock dapat berupa lock:user:{user_id}:notifications atau lebih spesifik sesuai domain. Jangan menggunakan satu lock global untuk semua pengguna karena akan membunuh paralelisme.
Lease, TTL, dan Fencing Token
Distributed lock yang aman biasanya membutuhkan lease dengan TTL dan, untuk operasi kritis, fencing token. TTL mencegah stale lock permanen. Fencing token mencegah worker lama menulis setelah lock-nya kedaluwarsa dan sudah diambil worker lain.
function withFileLock(fileId, job):
token = lockStore.acquire('lock:file:' + fileId, ttl = 30s)
if token == null:
requeueWithBackoff(job)
return
try:
file = db.getFileForUpdate(fileId)
if token < file.last_lock_token:
raise StaleLockOwner
db.updateFile(fileId, {
last_lock_token: token,
metadata_version: file.metadata_version + 1
})
handleJob(job)
finally:
lockStore.releaseIfOwner('lock:file:' + fileId, token)Pseudo-code di atas menunjukkan dua lapisan proteksi: lock eksternal untuk mengurangi konflik antar worker, dan pengecekan token atau transaksi database untuk menjaga correctness. Pada sistem yang sangat sensitif, database row lock atau optimistic concurrency dengan version check sering tetap diperlukan.
Menangani Stale Lock
Stale lock terjadi ketika lock masih tercatat tetapi pemiliknya sudah mati, jaringan terputus, atau proses berhenti sebelum release. Cara menanganinya:
- Selalu beri TTL pada lock.
- Release lock hanya jika token pemilik cocok, bukan sekadar delete key.
- Gunakan heartbeat hanya jika job memang panjang; jangan memperpanjang lock tanpa batas.
- Catat owner, token, acquired_at, expires_at, dan correlation_id untuk debugging.
- Jika lock sering stale, pecah job besar menjadi beberapa langkah lebih kecil.
Kesalahan umum adalah membuat TTL terlalu pendek. Jika TTL kedaluwarsa saat worker masih menulis, worker lain dapat masuk dan menyebabkan konflik. Sebaliknya, TTL terlalu panjang memperlambat recovery ketika worker mati.
Ordering Event dan Cache Invalidation
Ordering harus didesain secara eksplisit. Mengandalkan waktu kedatangan job sering tidak cukup karena network delay, retry, dan worker paralel dapat mengubah urutan eksekusi.
Gunakan Version atau Sequence Number
Setiap file atau metadata entity sebaiknya memiliki version monotonik. Worker membandingkan version pada job dengan version terbaru di database.
- Jika
job.version < current.version, event lama dapat di-skip atau hanya dipakai untuk audit. - Jika
job.version == current.version, worker dapat memproses efek samping untuk versi tersebut. - Jika
job.version > current.version, berarti worker melihat event lebih cepat daripada database replica atau ada gap event; retry dengan backoff.
Untuk sistem dengan read replica, berhati-hatilah: worker yang membaca replica dapat melihat state lama. Gunakan primary database untuk proses yang membutuhkan konsistensi kuat, atau tunggu hingga replica mengejar dengan mekanisme yang terukur.
Cache Invalidation Setelah Commit
Cache harus diinvalidate setelah transaksi utama commit. Menghapus cache sebelum commit dapat membuat request lain mengisi ulang cache dengan data lama.
Pola yang aman:
- Commit perubahan metadata.
- Tulis event outbox.
- Worker membaca event dan menghapus atau memperbarui key cache terkait.
- Request berikutnya membaca dari database dan mengisi cache dengan version terbaru.
Untuk daftar direktori, cache key sebaiknya memasukkan version folder atau tenant, misalnya folder:{folder_id}:children:v{version}. Dengan pendekatan versioned cache, invalidation dapat dilakukan dengan menaikkan version, bukan mencari dan menghapus banyak key.
Retry dan Dead Letter Queue
Retry diperlukan karena banyak kegagalan bersifat sementara: database overload, storage lambat, rate limit layanan notifikasi, atau konflik lock. Namun retry tanpa batas dapat memperparah beban dan menyembunyikan bug.
Klasifikasi Error
- Transient: timeout, lock busy, koneksi sementara gagal. Gunakan retry dengan exponential backoff dan jitter.
- Conflict sementara: event datang terlalu cepat dibanding commit atau replica lag. Retry terbatas.
- Permanent: file tidak ada karena sudah dihapus permanen, payload tidak valid, izin tidak konsisten. Jangan retry tanpa perubahan data.
- Bug: exception tak terduga. Kirim ke DLQ setelah batas retry agar bisa dianalisis.
Dead Letter Queue
Dead letter queue menyimpan job yang gagal setelah beberapa retry. DLQ harus menyimpan payload asli, error terakhir, jumlah percobaan, worker id, timestamps, dan correlation_id. Jangan hanya menyimpan stack trace tanpa konteks domain.
Prosedur operasional yang sehat:
- Alert jika jumlah DLQ melewati ambang yang disepakati tim.
- Sediakan alat untuk replay job setelah bug diperbaiki.
- Pastikan replay tetap melewati idempotency check.
- Bedakan DLQ karena data rusak dan DLQ karena dependency eksternal sedang down.
Contoh Alur: Upload Versi Baru dan Notifikasi
Berikut alur ringkas saat pengguna mengunggah versi baru file yang dibagikan ke beberapa kolaborator:
- API menerima upload dan menyimpan blob ke object storage atau storage file.
- API membuka transaksi metadata.
- API mengambil lock atau row lock untuk
file_id. - API memperbarui ukuran, checksum, content_version, updated_at, dan actor.
- API menulis event outbox
file.content.updateddengan version baru. - Publisher mengirim event ke queue dengan partition key
file_id. - Worker mengambil per-file lock untuk update indeks dan preview.
- Worker membuat job turunan notifikasi per user atau per share.
- Worker notifikasi memakai per-user lock agar status notifikasi dan badge count konsisten.
- Cache file dan folder diinvalidate setelah commit.
function handleFileContentUpdated(job):
return withFileLock(job.aggregate_id, job, () => {
file = db.getFile(job.aggregate_id)
if file.content_version > job.version:
return markSkipped(job, 'old event')
if file.content_version < job.version:
return retryLater(job, 'metadata not ready')
upsertSearchIndex(file.id, file.content_version)
invalidateCache('file:' + file.id)
invalidateCache('folder:' + file.parent_id + ':children')
collaborators = db.listCollaborators(file.id)
for user in collaborators:
enqueue({
job_type: 'notification.file.updated',
aggregate_id: file.id,
user_id: user.id,
version: file.content_version,
idempotency_key: 'notify:' + user.id + ':' + file.id + ':' + file.content_version
})
markCompleted(job.idempotency_key)
})Contoh ini sengaja mengambil state file terbaru dari database. Payload job hanya membawa identitas dan version. Ini membuat worker lebih tahan terhadap payload stale, tetapi membutuhkan database yang cukup kuat untuk beban read worker.
Metrik Operasional yang Perlu Dipantau
Masalah sinkronisasi sering terlihat sebagai keluhan UI, tetapi akar masalahnya ada di queue, lock, cache, atau database. Metrik berikut membantu mempersempit penyebab:
- Queue depth: jumlah job menunggu per queue dan per jenis job.
- Queue lag: selisih waktu antara event dibuat dan mulai diproses.
- Job duration: p50, p95, dan p99 waktu pemrosesan per job type.
- Retry rate: persentase job yang retry, dikelompokkan berdasarkan error class.
- DLQ count: jumlah job masuk dead letter queue per interval.
- Lock wait time: waktu menunggu lock per key pattern.
- Lock contention: berapa sering acquire lock gagal karena lock sedang dipakai.
- Stale lock count: lock yang kedaluwarsa atau dilepas oleh mekanisme recovery.
- Outbox age: umur event tertua yang belum dipublish.
- Cache hit ratio dan invalidation count: untuk melihat apakah cache terlalu agresif atau terlalu jarang diinvalidate.
- Event skipped as old: indikator event out-of-order atau duplicate yang berhasil ditangani.
- Notification duplicate prevented: jumlah pengiriman yang dibatalkan oleh idempotency.
Log juga harus membawa correlation_id, event_id, idempotency_key, file_id, user_id, retry attempt, dan lock token jika ada. Tanpa field ini, debugging distributed workflow akan memakan waktu lama.
Checklist Debugging
Saat Worker Lambat
- Periksa queue depth dan queue lag per job type. Jangan hanya melihat total queue.
- Cari satu jenis job yang mendominasi durasi p95 atau p99.
- Periksa apakah worker sering menunggu lock pada file, folder, atau user tertentu.
- Pastikan retry tidak membuat storm. Error transient yang masif dapat menggandakan beban.
- Cek dependency eksternal: database, object storage, search index, dan layanan notifikasi.
- Periksa ukuran payload. Payload besar memperlambat serialisasi, network, dan memory worker.
- Lihat apakah ada hot key, misalnya satu folder besar atau satu file populer yang memicu ribuan event.
- Naikkan jumlah worker hanya setelah memastikan bottleneck bukan lock contention atau database.
Saat Data Terlihat Tidak Konsisten
- Bandingkan version pada database, cache, search index, dan payload job terakhir.
- Periksa apakah cache diinvalidate sebelum commit atau dari event lama.
- Cari event dengan
job.version < current.versionyang masih memodifikasi state. - Pastikan worker membaca dari primary database untuk operasi yang butuh konsistensi kuat.
- Periksa idempotency table: apakah job ditandai completed padahal efek samping gagal?
- Cek DLQ untuk event yang memutus rantai, misalnya indeks tidak diperbarui karena satu job gagal permanen.
- Trace berdasarkan correlation_id dari request API ke outbox, queue, worker, cache invalidation, dan notifikasi.
- Pastikan release lock memakai token pemilik. Delete lock tanpa token dapat melepas lock milik worker lain.
Trade-off dan Kesalahan Umum
- Terlalu banyak lock: correctness meningkat, tetapi throughput turun. Gunakan lock pada entitas terkecil yang memang perlu dilindungi.
- Terlalu percaya pada ordering queue: retry dan multi-worker tetap bisa membuat efek samping tiba dalam urutan berbeda. Version check tetap diperlukan.
- Job tidak idempotent: cepat terlihat saat ada timeout, deploy ulang, atau broker mengirim ulang pesan.
- Payload membawa state besar: worker memproses data yang sudah stale. Lebih aman membawa ID dan version, lalu membaca state terbaru.
- Retry tanpa batas: menyebabkan beban berulang dan menunda job lain. Gunakan batas retry dan DLQ.
- Cache invalidation manual yang tersebar: mudah terlewat. Sentralisasi invalidation berdasarkan event domain.
- Lock TTL tidak realistis: TTL terlalu pendek menyebabkan overlap, TTL terlalu panjang memperlambat recovery.
Kesimpulan
Queue dan lock untuk sinkronisasi file kolaboratif bukan sekadar komponen infrastruktur, tetapi bagian dari desain konsistensi sistem. Queue membuat pekerjaan berat dan fan-out notifikasi dapat diproses asynchronous. Lock mencegah konflik pada file atau user yang sama. Namun keduanya harus dilengkapi dengan idempotency key, deduplication, version check, cache invalidation yang benar, retry terkontrol, dead letter queue, dan observability.
Jika membangun platform kolaborasi file dengan karakteristik seperti suite modern semacam Nextcloud Hub, fokuslah pada invariant yang harus benar: satu file memiliki urutan versi yang jelas, efek samping aman diproses ulang, notifikasi tidak ganda, cache tidak mendahului commit, dan stale lock bisa pulih tanpa intervensi manual. Dengan fondasi itu, sistem akan lebih mudah diskalakan dan lebih mudah di-debug saat terjadi inkonsistensi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!