Saat tim memindahkan beban dari layanan Git terkelola ke platform self-hosted, tanggung jawab operasional ikut berpindah: webhook yang harus terkirim, indexing yang tidak boleh tertinggal, mirror sync yang konsisten, email notifikasi yang tidak dobel, dan cleanup repository yang aman. Di titik ini, queue internal untuk self-hosted Git bukan sekadar komponen performa, tetapi mekanisme kontrol konsistensi sistem.
Masalah yang paling sering muncul bukan "bagaimana menjalankan background job", melainkan bagaimana mencegah job yang sama dieksekusi dua kali, bagaimana recovery saat worker crash di tengah proses, bagaimana membatasi retry agar tidak berubah menjadi retry storm, dan bagaimana menjaga data tetap masuk akal saat event datang tidak berurutan. Artikel ini membahas desain praktis untuk problem tersebut, termasuk kapan memakai Redis, database, atau broker terpisah.
Mengapa self-hosted Git butuh queue yang dirancang serius
Dalam platform Git self-hosted atau alternatif GitHub/Codeberg, banyak operasi tidak aman dijalankan secara sinkron di request utama:
- Webhook ke CI, issue tracker, chat, atau deployment system.
- Indexing repository untuk pencarian kode, statistik, atau blame cache.
- Mirror sync ke remote lain, backup, atau geo-replication.
- Email notifikasi untuk push, merge request, review, atau audit.
- Cleanup repo seperti penghapusan artefak sementara, reflog cleanup, GC, atau pruning state lama.
Semua pekerjaan ini punya karakteristik berbeda. Webhook sensitif pada retry dan timeout jaringan. Indexing berat di CPU/disk. Mirror sync sensitif pada urutan commit dan kredensial. Cleanup mudah merusak state jika tidak diberi guard yang tepat. Karena itu, satu queue generik sering tidak cukup; minimal Anda butuh pemisahan berdasarkan tipe workload, prioritas, dan mode eksekusi.
Tren migrasi ke self-hosting membuat tim tidak hanya mengelola Git server, tetapi juga failure mode di belakangnya. Yang sebelumnya ditangani platform SaaS kini menjadi tanggung jawab tim sendiri: antrean, observability, kapasitas, dan recovery.
Prinsip desain queue internal untuk self-hosted Git
1. Anggap delivery minimal at-least-once
Dalam sistem nyata, job bisa diproses lebih dari sekali karena worker crash setelah side effect terjadi tetapi sebelum status job tersimpan. Karena itu, desain yang aman biasanya berangkat dari asumsi at-least-once delivery, lalu dipadukan dengan idempotensi.
Jangan mengandalkan "exactly once" sebagai properti broker. Dalam praktik backend, exactly once umumnya membutuhkan koordinasi lintas storage dan side effect yang mahal, kompleks, atau tidak realistis.
2. Pisahkan enqueue dari side effect utama
Saat menerima event push atau perubahan refs, simpan dulu event ke database transaksi utama atau outbox, lalu enqueue job secara terkontrol. Ini mencegah kondisi di mana request sukses tetapi job tidak pernah masuk queue, atau sebaliknya job masuk tetapi transaksi utama gagal.
3. Gunakan payload yang cukup, bukan berlebihan
Payload job sebaiknya berisi identitas stabil seperti repo_id, event_id, delivery_key, target_ref, atau attempt. Hindari memasukkan snapshot besar repository atau objek yang cepat usang. Worker sebaiknya memuat ulang state penting dari sumber kebenaran saat eksekusi.
4. Bedakan concurrency global dan concurrency per resource
Masalah umum di Git self-hosted adalah dua job menyentuh repository yang sama secara bersamaan: misalnya indexing dan cleanup, atau mirror sync dan repo GC. Sistem queue perlu membatasi parallelism total sekaligus memberikan serialisasi per repository untuk operasi yang tidak aman dijalankan paralel.
Alur backend yang konkret
Berikut alur yang umum dan cukup aman untuk event push:
- Request push diterima.
- Server Git memperbarui refs dan menulis event domain ke tabel outbox dalam transaksi yang sama atau fase commit yang dapat dipertanggungjawabkan.
- Publisher outbox membaca event yang belum terbit dan mengirim job ke queue: webhook, indexing, mirror sync, email.
- Worker mengambil job dengan visibility timeout.
- Worker memperoleh lock per repository atau per ref jika diperlukan.
- Worker memuat state terbaru dari database atau storage Git.
- Worker memeriksa idempotency key atau sequence/version untuk memastikan event masih relevan.
- Worker menjalankan side effect.
- Worker menyimpan hasil, metrik, dan ack job.
- Jika gagal berulang kali, job dipindahkan ke dead letter queue.
Model ini memisahkan tiga hal penting: durabilitas event, penjadwalan kerja, dan eksekusi side effect. Itu membuat debugging dan recovery lebih mudah dibanding langsung melakukan semuanya dari handler request.
Duplicate job, worker crash, dan idempotensi
Kenapa duplicate job terjadi
Duplicate job bukan bug langka. Beberapa penyebab paling umum:
- Publisher mengirim ulang karena timeout saat submit ke broker.
- Worker selesai menjalankan side effect, tetapi crash sebelum ack.
- Lease atau visibility timeout habis, lalu job diambil worker lain.
- Operator menjalankan replay dari DLQ tanpa deduplikasi.
Strategi idempotensi yang realistis
Untuk setiap jenis job, tentukan unit idempotensi yang jelas:
- Webhook: kombinasi
event_id + endpoint_id. - Email: kombinasi
notification_type + object_id + recipient_id. - Indexing: versi index terakhir untuk
repo_id + commit/ref. - Mirror sync: target remote dan ref update tertentu.
- Cleanup: window waktu atau generation tertentu, bukan hanya
repo_id.
Simpan kunci idempotensi di storage yang tahan restart, biasanya database. Redis bisa dipakai untuk dedup jangka pendek, tetapi jangan dijadikan satu-satunya sumber kebenaran jika side effect bernilai tinggi.
-- contoh tabel sederhana untuk idempotensi delivery webhook
CREATE TABLE webhook_delivery (
event_id BIGINT NOT NULL,
endpoint_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL,
last_error TEXT,
delivered_at TIMESTAMP NULL,
PRIMARY KEY (event_id, endpoint_id)
);
Pola eksekusinya:
- Worker mencoba membuat atau mengunci baris untuk
event_id + endpoint_id. - Jika status sudah
delivered, worker keluar tanpa side effect. - Jika belum, worker kirim webhook.
- Jika sukses, update status ke
delivered.
Dengan pola ini, worker yang crash setelah kirim tetapi sebelum update status masih mungkin memicu duplikasi. Untuk mengurangi efeknya, kirim juga delivery ID ke penerima agar sisi penerima dapat dedup. Idempotensi yang baik sering membutuhkan kerja sama producer dan consumer.
Distributed lock: kapan perlu, kapan berbahaya
Kapan lock dibutuhkan
Lock terdistribusi berguna saat dua worker tidak boleh memodifikasi resource yang sama bersamaan. Contoh:
- Hanya satu proses repo cleanup per repository.
- Hanya satu mirror sync aktif per remote target.
- Indexing per repository dijalankan serial untuk menghindari thrash disk dan index corruption.
Jangan jadikan lock sebagai pengganti desain job
Lock sering disalahgunakan untuk menutupi model data yang belum rapi. Jika semua jenis job harus saling menunggu lock global, throughput turun dan deadlock operasional naik. Lebih baik gunakan lock yang sempit:
- Per repository untuk operasi destructive.
- Per ref atau per remote untuk operasi yang lebih lokal.
- Beri TTL agar lock tidak menggantung saat worker mati.
Redis lock vs database lock
Redis lock cocok untuk koordinasi cepat dan short-lived lease. Keuntungannya ringan dan cepat. Kekurangannya: Anda harus hati-hati terhadap expiry, clock skew, network partition, dan kasus worker masih berjalan setelah lease habis.
Database lock cocok jika sumber kebenaran Anda memang ada di database dan throughput tidak terlalu tinggi. Keuntungannya lebih sederhana secara konsistensi dan observability. Kekurangannya bisa menambah kontensi pada database utama.
Praktik aman:
- Gunakan lock sebagai advisory coordination, bukan jaminan mutlak.
- Selalu validasi state aktual sebelum menulis hasil.
- Jangan asumsikan lock yang expired berarti eksekusi sebelumnya tidak jadi melakukan side effect.
// pseudo-code worker dengan lease dan validasi state
job = queue.reserve(visibility_timeout=60s)
lock = lockManager.acquire("repo:" + job.repoId, ttl=30s)
if !lock:
queue.requeue(job, delay=10s)
return
try:
current = repoStore.loadState(job.repoId)
if current.version > job.expectedVersion:
queue.ack(job) // job usang
return
process(job)
queue.ack(job)
finally:
lock.releaseIfOwned()
Retry, backoff, dan mencegah retry storm
Retry perlu dibedakan berdasarkan jenis kegagalan
Tidak semua error pantas di-retry. Klasifikasi minimal:
- Transient: timeout jaringan, koneksi broker putus, service downstream 503. Layak retry dengan backoff.
- Persistent: endpoint salah, kredensial invalid, repo target tidak ada. Jangan retry agresif; cepat pindah ke DLQ atau tandai perlu intervensi.
- Resource contention: lock gagal diperoleh, rate limit lokal, disk sibuk. Retry dengan jeda pendek dan jitter.
Gunakan exponential backoff + jitter
Retry storm sering terjadi saat service downstream bermasalah, lalu semua worker serentak mencoba lagi. Gunakan backoff eksponensial dengan jitter acak agar permintaan tersebar.
Hindari retry instan tanpa batas. Itu hanya memindahkan kegagalan ke broker, Redis, database, dan CPU worker.
// pseudo-code strategi retry
if error.isTransient():
delay = min(baseDelay * 2^attempt, maxDelay)
delay = addJitter(delay)
queue.requeue(job, delay)
elif error.isPermanent():
queue.moveToDLQ(job, reason=error.code)
else:
queue.requeue(job, delay=shortJitter)
Visibility timeout harus lebih panjang dari kerja normal, tetapi tidak terlalu lama
Visibility timeout adalah waktu ketika job yang sudah diambil worker disembunyikan dari worker lain. Jika worker tidak ack sebelum waktu habis, job dianggap tersedia lagi.
Atur timeout berdasarkan durasi kerja wajar ditambah margin. Jika terlalu pendek, job aktif akan diproses ganda. Jika terlalu panjang, recovery saat worker mati menjadi lambat. Untuk job lama seperti indexing besar atau mirror sync, pertimbangkan heartbeat atau perpanjangan lease selama worker masih hidup.
Out-of-order event dan konsistensi data
Masalah nyata: event datang tidak berurutan
Dalam sistem Git, urutan event bisa terganggu karena antrian terpisah, retry, network delay, atau replay. Contoh:
- Event
push Bdiproses lebih dulu daripadapush A. - Webhook untuk branch delete tiba sebelum indexing branch update selesai.
- Cleanup dijalankan sebelum mirror sync untuk commit yang sama selesai.
Jika worker memproses payload secara buta, state jadi mundur atau saling menimpa.
Pakai versi, sequence, atau validasi terhadap state terbaru
Untuk mengatasi event tidak berurutan, job harus bisa menentukan apakah dirinya masih relevan. Pendekatan umum:
- Sequence number per repository atau per ref.
- Updated-at/version dari objek domain.
- Current tip validation: sebelum indexing ref, cek apakah SHA target masih menjadi tip saat ini.
Contoh untuk indexing branch:
- Job berisi
repo_id,ref_name,target_sha. - Worker membaca tip ref terbaru dari repo metadata.
- Jika tip terbaru berbeda dan job lama tidak lagi relevan, tandai job sebagai stale lalu ack tanpa proses berat.
Ini jauh lebih aman daripada memaksa FIFO global, yang biasanya mahal dan tidak perlu.
Cache invalidation harus mengikuti sumber kebenaran
Cache invalidation sering gagal saat event datang tak berurutan. Dua aturan praktis:
- Jangan update cache hanya berdasarkan asumsi urutan event; selalu bandingkan dengan versi/state terbaru.
- Untuk cache turunan seperti daftar branch, statistik repo, atau permission snapshot, lebih aman menyimpan version stamp dan mengganti hanya jika versi baru lebih tinggi.
Jika cache rusak, lebih baik bisa dibuang dan dibangun ulang daripada menjadi sumber kebenaran tersembunyi.
Kapan memakai Redis, database, atau broker terpisah
Redis
Pilih Redis bila Anda butuh antrean cepat, operasi sederhana, dan latensi rendah untuk job internal seperti webhook ringan, email, atau fan-out event yang volumenya sedang.
Kelebihan:
- Sederhana dioperasikan jika stack sudah memakai Redis.
- Cepat untuk enqueue/dequeue dan lock ringan.
- Cocok untuk workload bursty yang tidak terlalu kompleks.
Kekurangan:
- Fitur durable queue dan observability sering lebih terbatas dibanding broker khusus.
- Perlu desain tambahan untuk lease, DLQ, replay, dan dedup yang aman.
- Mudah dipakai terlalu banyak peran sekaligus: cache, lock, queue, rate limit.
Database
Pilih database bila volume job masih moderat, Anda membutuhkan konsistensi kuat dengan data aplikasi, dan ingin menyederhanakan operasional.
Kelebihan:
- Outbox, idempotency record, dan status eksekusi bisa hidup dekat dengan data domain.
- Lebih mudah diaudit dan di-debug dengan SQL.
- Mengurangi jumlah komponen yang harus dijaga.
Kekurangan:
- Beban queue dapat mengganggu database utama jika throughput tinggi.
- Polling yang buruk bisa mahal.
- Tidak ideal untuk fan-out besar atau kebutuhan throughput sangat tinggi.
Broker terpisah
Pilih broker terpisah bila job volume tinggi, banyak tipe consumer, butuh routing, replay, retention, atau isolasi failure yang lebih baik.
Kelebihan:
- Lebih cocok untuk pipeline event yang berkembang.
- Biasanya lebih baik untuk DLQ, ack model, consumer group, dan observability.
- Memisahkan tekanan operasional dari database aplikasi.
Kekurangan:
- Kompleksitas operasional naik: deployment, monitoring, backup, upgrade.
- Masih tetap perlu idempotensi dan konsistensi di level aplikasi.
Aturan praktis yang masuk akal:
- Mulai dari database + outbox jika sistem masih sederhana dan kebutuhan audit kuat.
- Gunakan Redis jika Anda butuh antrean cepat dan tim sudah nyaman mengelolanya.
- Naik ke broker terpisah jika antrean menjadi sistem inti dengan banyak consumer, volume besar, dan kebutuhan replay yang jelas.
Pemisahan queue berdasarkan workload
Jangan campur semua job ke satu antrean dengan prioritas seadanya. Untuk platform Git self-hosted, pemisahan berikut biasanya lebih sehat:
- high-priority: webhook kecil, email notifikasi penting.
- default: indexing metadata ringan, invalidasi cache.
- heavy: full indexing, mirror sync besar, GC/cleanup.
- scheduled: retry tertunda, maintenance, kompaksasi.
Dengan pemisahan ini, indexing besar tidak memblokir email atau webhook. Selain itu, Anda bisa memberi limit worker, CPU, dan timeout berbeda untuk tiap kelas job.
Observability yang wajib ada
Queue tanpa observability akan terasa "baik-baik saja" sampai insiden datang. Metrik minimal yang perlu tersedia:
- Queue depth per antrean.
- Job age / lag: usia job tertua yang belum diproses.
- Success/failure rate per tipe job.
- Retry rate dan distribusi attempt.
- DLQ size dan alasan masuk DLQ.
- Processing time per job type.
- Lock contention rate.
- Stale/discarded job count untuk event usang.
Logging juga perlu terstruktur. Minimal sertakan:
job_idjob_typerepo_idevent_idattemptworker_idlock_keybila adaresultdanerror_class
Dengan begitu, Anda bisa menjawab pertanyaan operasional penting: apakah webhook lambat karena endpoint luar gagal? apakah indexing tertahan karena lock repo? apakah queue depth naik karena worker mati atau karena workload memang melonjak?
Runbook dasar saat insiden
1. Queue menumpuk
- Cek queue depth dan job age per antrean.
- Lihat apakah bottleneck ada di worker CPU, disk, network, atau lock contention.
- Pastikan job heavy tidak memonopoli worker untuk antrean penting.
- Skalakan worker secara selektif, bukan serentak tanpa batas.
2. Retry storm
- Cek error dominan: timeout, auth error, DNS, atau endpoint 5xx.
- Aktifkan rate limit atau circuit breaker untuk downstream yang sedang gagal.
- Perlebar backoff, tambah jitter, dan hentikan retry untuk error permanen.
- Jangan replay DLQ sebelum akar masalah jelas.
3. Duplicate delivery
- Periksa visibility timeout dan durasi job.
- Periksa apakah worker sering crash setelah side effect tetapi sebelum ack.
- Audit implementasi idempotency record.
- Untuk webhook, verifikasi consumer mendukung dedup berbasis delivery ID.
4. Data terlihat mundur atau inkonsisten
- Cek apakah event diproses out of order.
- Verifikasi guard sequence/version sebelum update state turunan.
- Buang cache yang meragukan dan bangun ulang dari sumber kebenaran.
- Pastikan cleanup tidak berjalan paralel dengan operasi repo yang sensitif.
Kesalahan desain yang sering terjadi
- Menganggap queue menjamin sekali proses, lalu tidak menyiapkan idempotensi.
- Menggunakan satu Redis untuk cache, queue, lock, dan session tanpa batas resource yang jelas.
- Menetapkan visibility timeout terlalu pendek untuk job panjang.
- Retry semua error dengan pola yang sama.
- Memakai lock global saat yang dibutuhkan hanya serialisasi per repository.
- Mengandalkan FIFO total untuk menyelesaikan masalah konsistensi yang seharusnya ditangani dengan version check.
- Tidak punya DLQ atau punya DLQ tetapi tanpa prosedur replay yang aman.
Checklist implementasi
- Tentukan tipe job: webhook, indexing, mirror sync, email, cleanup.
- Pisahkan antrean berdasarkan prioritas dan berat workload.
- Gunakan outbox atau mekanisme durable untuk publishing event.
- Tentukan idempotency key untuk setiap tipe job.
- Terapkan visibility timeout dan heartbeat untuk job panjang.
- Gunakan lock per resource hanya untuk operasi yang benar-benar perlu serialisasi.
- Tambahkan version/sequence check agar event usang bisa dibuang aman.
- Definisikan aturan retry: transient, persistent, contention.
- Sediakan DLQ beserta prosedur inspect dan replay.
- Bangun metrik, tracing, dan structured logging sejak awal.
- Dokumentasikan runbook insiden dan uji dengan simulasi worker crash.
- Uji kasus duplikasi, out-of-order, dan recovery sebelum production.
Penutup
Queue internal untuk self-hosted Git yang andal tidak ditentukan oleh pilihan tool saja, melainkan oleh kombinasi desain idempotensi, lock yang tepat sasaran, retry yang terkendali, dan validasi konsistensi terhadap state terbaru. Untuk webhook, indexing, mirror sync, email, dan cleanup repo, masalah terberat biasanya muncul saat sistem gagal sebagian, bukan saat semuanya normal.
Jika Anda membangun platform Git self-hosted sendiri, mulailah dari model yang bisa diaudit: event durable, job yang idempotent, lock seperlunya, dan observability yang cukup untuk menjelaskan kegagalan. Sistem queue yang sederhana tetapi jujur terhadap failure mode nyata hampir selalu lebih aman daripada arsitektur kompleks yang tampak rapi di diagram, tetapi tidak siap menghadapi duplikasi, retry storm, dan event yang datang tidak berurutan.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!