Pada sistem code review dan CI/CD otomatis, lonjakan commit (commit burst)—seperti saat developer melakukan interactive rebase atau mendorong puluhan commit sekaligus via git push --force—sering memicu antrean worker yang membengkak secara instan.
Setiap webhook commit memicu job untuk mengomputasi diff. Akibatnya, worker mengeksekusi komputasi berat untuk commit-commit perantara yang sebenarnya sudah usang. Hal ini menyebabkan dua kendala operasional utama: cache thrashing pada object storage/Redis dan lock contention saat beberapa worker mengakses bare repository Git yang sama di filesystem lokal.
Anatomi Masalah: Mengapa Commit Hash Memicu Thrashing
Secara umum, sistem code review menyimpan cache diff menggunakan pasangan Commit SHA:
cache_key = f"diff:{base_commit_sha}:{head_commit_sha}"Strategi ini memiliki kelemahan mendasar. Saat developer mengubah pesan commit, melakukan amend, atau rebase ke master tanpa mengubah substansi file, Commit SHA akan selalu berubah secara kriptografis. Worker dipaksa menjalankan kalkulasi git diff ulang meskipun isi pohon direktori sama persis.
Selain itu, eksekusi paralel worker pada bare repo lokal yang sama memicu perebutan file lock (misalnya pada file packed-refs atau lock index) dan membebani disk I/O dengan komputasi diff dari commit yang tidak pernah dilihat oleh reviewer.
Strategi 1: Content-Addressable Caching via Git Tree SHA
Penyelesaian komputasi redundant tidak memerlukan commit hash, melainkan Tree SHA. Objek tree merepresentasikan snapshot riil dari hierarki direktori pada commit tersebut.
Periksa Tree SHA dari commit target menggunakan Git CLI:
git rev-parse commit_id^{tree}Jika developer melakukan amend pesan commit atau rebase tanpa konflik, Tree SHA tidak berubah. Cache diff berbasis Tree SHA menjamin cache hit tetap 100%:
cache_key = f"diff:tree:{base_tree_sha}:{head_tree_sha}"Strategi 2: Queue Debouncing untuk Mengabaikan Job Usang
Saat terjadi 10 commit berturut-turut pada satu pull request dalam hitungan detik, hanya diff dari commit terakhir (HEAD) yang relevan untuk reviewer.
Daripada memproses 10 job secara berurutan:
- Simpan hash HEAD terbaru dari branch/PR ke dalam metadata store (misalnya Redis) saat webhook masuk.
- Ketika worker mengambil job dari queue, validasi apakah commit yang ditugaskan masih sesuai dengan HEAD terkini pada branch tersebut.
- Jika commit job lebih tua dari HEAD terbaru yang tercatat, segera gugurkan job (no-op).
Implementasi: Worker Flow dengan Distributed Lock dan Tree SHA
Berikut adalah implementasi alur pemrosesan worker menggunakan Python dan Redis. Pola ini memadukan pembatalan job usang, pengecekan tree cache, dan serialisasi eksekusi per-repo menggunakan distributed lock.
import subprocess
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def get_tree_sha(repo_path: str, commit_sha: str) -> str:
cmd = ["git", "-C", repo_path, "rev-parse", f"{commit_sha}^{{tree}}"]
return subprocess.check_output(cmd, text=True).strip()
def compute_git_diff(repo_path: str, base_tree: str, head_tree: str) -> str:
cmd = ["git", "-C", repo_path, "diff", "--no-color", "--patch", base_tree, head_tree]
return subprocess.check_output(cmd, text=True)
def process_diff_job(repo_id: str, repo_path: str, pr_id: str, base_sha: str, head_sha: str):
# 1. Debouncing Check: Pastikan commit ini masih HEAD terbaru dari PR
latest_head = r.get(f"pr:{pr_id}:latest_head")
if latest_head and latest_head.decode('utf-8') != head_sha:
# Commit sudah usang, batalkan komputasi
return None
# 2. Resolusi Tree SHA
base_tree = get_tree_sha(repo_path, base_sha)
head_tree = get_tree_sha(repo_path, head_sha)
# 3. Content-Addressable Cache Lookup
cache_key = f"diff:tree:{base_tree}:{head_tree}"
cached_diff = r.get(cache_key)
if cached_diff:
return cached_diff.decode('utf-8')
# 4. Distributed Lock per Repo ID untuk mencegah contention I/O
# ponytail: ttl 10 detik cukup; naikkan ke Redlock jika cluster multi-master
lock_key = f"lock:repo:{repo_id}"
with r.lock(lock_key, timeout=10, blocking_timeout=5):
# Double check cache setelah lock didapat (mencegah stampede)
cached_diff = r.get(cache_key)
if cached_diff:
return cached_diff.decode('utf-8')
diff_output = compute_git_diff(repo_path, base_tree, head_tree)
r.setex(cache_key, 86400 * 7, diff_output) # Cache selama 7 hari
return diff_output
Trade-offs dan Catatan Operasional
- Granularitas Lock: Mengunci seluruh
repo_idmencegah contention pada bare repo disk, namun dapat menurunkan throughput jika ada pull request berbeda pada repo yang sama. Jika repo memiliki I/O memadai, persempit lock ke tingkat PR (lock:repo:{repo_id}:pr:{pr_id}). - Lock Timeout vs Git Fetch: Jika worker perlu melakukan
git fetchsebelum diff, operasi network bisa melebihi TTL lock. Pisahkan proses fetching referensi ke fase job terpisah sebelum lock komputasi diff diambil. - Batas Ukuran Diff: Hindari menyimpan output diff berukuran ratusan megabyte langsung ke Redis string. Tetapkan threshold (misal 5MB). Untuk diff raksasa, simpan artefak ke object storage (S3/GCS) dan jadikan cache Redis hanya sebagai pointer URI.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!