Tantangan Skalabilitas Arsitektur Penyimpanan VCS

Pada sistem Version Control System (VCS) monorepo atau sistem berskala ribuan pengembang aktif, operasi push konkuren menjadi bottleneck utama. Ketika ratusan worker atau developer mengirim changeset secara simultan, backend VCS harus menjaga konsistensi Directed Acyclic Graph (DAG) commit. Tanpa orkestrasi yang tepat, sistem rentan terhadap lock contention ekstrem, data corrupt pada storage index (revlog), dan race condition pada atomic branch reference update.

Arsitektur wire protocol dan revlog Mercurial mengandalkan pembacaan index yang sangat cepat dan penulisan append-only. Kendala muncul ketika writer threads bersaing memvalidasi root commit dan base revlog index secara bersamaan. Pendekatan konvensional dengan lock level berkas lokal (fcntl atau flock) gagal saat sistem didistribusikan ke cluster multi-node. Artikel ini mengupas solusi distributed lock leasing, isolasi cache revlog, dan idempotensi push pipeline.

Distributed Locking untuk Atomic Reference Update

Mempertahankan konsistensi pointer branch (misal: refs/heads/main atau Mercurial branch heads) membutuhkan linearizability. Setiap push harus memverifikasi bahwa parent hash changeset sama persis dengan hash branch head terkini (Compare-And-Swap).

Distributed lock dapat diimplementasikan menggunakan etcd atau Redis dengan pola redlock/single-lease. Kuncinya adalah short-lived distributed lock yang hanya memproteksi fase validasi ancestry dan update pointer, bukan keseluruhan proses transfer payload objek.

Implementasi Lock Leasing dan Verifikasi State

Contoh berikut mendemonstrasikan lock acquisition dengan lease TTL, auto-renewal thread-safe, dan validasi DAG hash sebelum data ditulis:

import time
import uuid
import etcd3

class VCSReferenceManager:
    def __init__(self, etcd_client, repo_id: str):
        self.client = etcd_client
        self.repo_id = repo_id
        self.ref_prefix = f"vcs:repo:{repo_id}:refs:"
        self.lock_prefix = f"vcs:repo:{repo_id}:lock:"

    def push_changeset(self, branch: str, expected_parent_node: str, new_node: str, timeout_sec: int = 5) -> bool:
        lock_key = f"{self.lock_prefix}{branch}"
        ref_key = f"{self.ref_prefix}{branch}"
        client_token = str(uuid.uuid4())

        # Buat lease TTL 10 detik agar tidak deadlock jika worker crash
        lease = self.client.lease(10)
        
        # Coba akuisisi distributed lock via transaction
        success, _ = self.client.transaction(
            compare=[self.client.transactions.create_revision(lock_key) == 0],
            success=[self.client.transactions.put(lock_key, client_token, lease=lease)],
            failure=[]
        )

        if not success:
            return False

        try:
            # 1. Verifikasi linearitas state DAG (Atomic check)
            current_head, _ = self.client.get(ref_key)
            current_head = current_head.decode("utf-8") if current_head else "0" * 40

            if current_head != expected_parent_node:
                # Terjadi non-fast-forward push conflict
                return False

            # 2. Update head pointer secara atomik
            self.client.put(ref_key, new_node)
            return True

        finally:
            # Hapus lock menggunakan token release via transkrip aman
            self.client.transaction(
                compare=[self.client.transactions.value(lock_key) == client_token.encode("utf-8")],
                success=[self.client.transactions.delete(lock_key)],
                failure=[]
            )
            lease.revoke()

Mitigasi Cache Stampede pada Revlog Index Metadata

Revlog (revision log) memisahkan pointer offset payload ke dalam index file (.i) dan data delta ke berkas raw (.d). Membaca index revlog ribuan kali per detik memicu saturasi I/O disk. Penggunaan in-memory cache mutlak diperlukan, namun invalidasi cache saat append push memicu thundering herd atau cache stampede.

Single-Flight Mutex dan Probabilistic Early Expiration

Untuk mengeliminasi lonjakan beban database atau disk backend saat cache invalidation, gunakan pendekatan single-flight lock pada level in-memory worker. Jika key cache metadata revlog habis, hanya satu worker thread yang diizinkan melakukan load dari disk; worker lain menunggu hasil dari caller pertama.

  • Deterministic Single-Flight: Kelompokkan concurrent reader hash yang sama via Mutex map.
  • Append-Aware Metadata Cache: Karena revlog bersifat append-only, index lama bersifat immutable. Alih-alih meng-evict seluruh cache index, cukup extend index dengan slice changeset baru setelah lock release.
  • Read-Copy-Update (RCU): Reader tidak diblokir selama revlog reader memetakan file descriptor secara mmap, selama penulisan header node menggunakan atomic pointer write di storage level.

Idempotensi Worker Pipeline Changeset

Pada pipeline asynchronous push berbasis queuing (RabbitMQ, Kafka, atau internal RPC pool), network failure dapat menyebabkan re-transmission data payload (bundle). Pipeline harus bersifat idempotent dari ingest layer hingga commit hooks.

  1. Changeset Hash Deduping: Gunakan hash SHA/BLAKE dari payload bundle sebagai Idempotency Key pada distributed key-value store.
  2. Staging Isolated Temporary Revlog: Jangan tulis commit langsung ke shared repo branch. Tulis bundle ke isolasi staging (quarantine area).
  3. Atomic Linking: Pindahkan data dari quarantine ke production revlog hanya ketika reference lease berhasil diamankan. Jika push dibatalkan atau terdeteksi redundant, discard folder staging tanpa mengotori production store.

Penanganan Contention: Truncated Exponential Backoff dengan Jitter

Jika ratusan CI/CD bot mencoba melakukan push simultan ke branch rilis yang sama, polling agresif akan menjatuhkan distributed consensus coordinator (etcd/Zookeeper). Terapkan algoritma backoff berbasis jitter secara ketat pada push agent.

import random
import time

def acquire_with_backoff(push_fn, max_retries: int = 6, base_delay: float = 0.1, max_delay: float = 2.0):
    retries = 0
    while retries < max_retries:
        if push_fn():
            return True
        
        retries += 1
        if retries == max_retries:
            break
            
        # Full Jitter: Sleep acak antara 0 dan min(max_delay, base * 2^retries)
        calculated_backoff = min(max_delay, base_delay * (2 ** retries))
        sleep_time = random.uniform(0, calculated_backoff)
        time.sleep(sleep_time)

    raise TimeoutError("Push gagal: Lock contention tinggi pada branch target.")

Debugging dan Trade-Off Arsitektur

Mengintegrasikan distributed lock overhead pada backend VCS membawa konsekuensi latensi jaringan tambahan. Pertimbangkan faktor berikut sebelum deployment:

  • Clock Drift Risk: Redlock/TTL lease sangat sensitif terhadap NTP clock drift. Pastikan etcd time drift < 50ms di seluruh node cluster.
  • Split-Brain Storage: Pastikan storage backend (NFS, Ceph, EBS) tidak menjalankan distributed write cache writeback tanpa strict consistency guarantee.
  • Lock Granularity: Jangan mengunci seluruh repositori. Batasi distributed lock per spesifik ref branch head agar developer pada branch fitur berbeda tidak terkena lock contention yang tidak relevan.