Proses benchmark backend dan evaluasi model otomatis sering kali mengalami hang tanpa melempar exit code error. Gejala umumnya spesifik: job berjalan normal pada awal eksekusi, lalu berhenti total pada iterasi tertentu—misalnya pada batch N=22—hingga batas global timeout CI tercapai. Kondisi ini menguras kuota menit komputasi, menahan runner pool, dan memblokir antrean merge request lain.

Penyebab utama benchmark macet di CI umumnya berkisar pada kebocoran file descriptor (FD), deadlock pada thread pool komputasi, atau socket connection pool yang kehabisan slot tanpa konfigurasi timeout granular. Panduan ini membahas instrumentasi worker, penerapan watchdog heartbeat, serta safe rollback runner.

Observabilitas Worker: CPU, File Descriptor, dan Timeout Leak

Saat proses benchmark berhenti merespons, pipeline default CI hanya menampilkan log kosong. Diperlukan pencatatan telemetri level proses untuk mengetahui apakah worker berada dalam status busy-wait (CPU 100%) atau deadlock/I-O wait (CPU 0%).

1. Diagnostik Resource Secara Periodik

Jalankan background daemon sederhana di dalam step CI untuk memantau alokasi resource proses benchmark:

#!/usr/bin/env bash
# monitor.sh: Monitor resource benchmark
PID=$1
while kill -0 "$PID" 2>/dev/null; do
  FD_COUNT=$(ls -1 /proc/"$PID"/fd 2>/dev/null | wc -l)
  CPU_MEM=$(ps -p "$PID" -o %cpu,%mem,stat --no-headers)
  echo "[MONITOR] $(date +%T) | PID: $PID | FDs: $FD_COUNT | CPU/MEM/STAT: $CPU_MEM"
  sleep 5
done

Jika metrik FD_COUNT naik terus menerus mendekati batas sistem (ulimit -n) hingga iterasi macet di N=22, masalahnya adalah resource leak pada socket connection pool atau unclosed file stream. Jika STAT berubah menjadi D (uninterruptible sleep) atau S (interruptible sleep) dengan CPU 0%, thread mengalami deadlock pada lock primitive atau koneksi jaringan tanpa read_timeout.

Watchdog Heartbeat per-Batch N

Timeout bawaan runner CI (seperti timeout-minutes: 60) tidak efektif mendeteksi macet per-iterasi karena membiarkan runner terkunci terlalu lama. Solusi deterministik adalah memasang watchdog berbasis timer yang di-reset setiap kali batch N selesai diproses.

Implementasi Watchdog Python

import os
import sys
import time
import threading

class BenchmarkWatchdog:
    def __init__(self, timeout_seconds=30):
        self.timeout = timeout_seconds
        self.last_heartbeat = time.time()
        self._stop_event = threading.Event()
        self._thread = threading.Thread(target=self._monitor, daemon=True)

    def start(self):
        self._thread.start()

    def ping(self, batch_idx):
        self.last_heartbeat = time.time()
        sys.stderr.write(f"[HEARTBEAT] Batch {batch_idx} processed\n")
        sys.stderr.flush()

    def stop(self):
        self._stop_event.set()

    def _monitor(self):
        while not self._stop_event.is_set():
            time.sleep(2)
            elapsed = time.time() - self.last_heartbeat
            if elapsed > self.timeout:
                sys.stderr.write(f"[FATAL] Watchdog timeout: Batch stuck for {elapsed:.1f}s\n")
                sys.stderr.flush()
                # Kirim SIGABRT ke diri sendiri untuk menghasilkan stack dump
                os.abort()

# Simulasi penggunaan
watchdog = BenchmarkWatchdog(timeout_seconds=15)
watchdog.start()

try:
    for n in range(1, 100):
        # Eksekusi unit benchmark
        if n == 22:
            time.sleep(60)  # Simulasi hang
        else:
            time.sleep(0.5)
        watchdog.ping(batch_idx=n)
finally:
    watchdog.stop()

Eksekusi os.abort() menghentikan proses secara langsung saat batch tertentu melebihi toleransi waktu (15 detik), menghasilkan core dump atau stack trace seketika tanpa menunggu pipeline timeout global.

Strategi Otomatis Cancel dan Safe Rollback Runner

Saat job benchmark dibatalkan paksa (baik oleh watchdog atau CI cancellation), subproses sering kali tertinggal dan menjadi zombie process atau orphaned process. Pada self-hosted runner, hal ini mengakibatkan shared memory (/dev/shm), port, atau GPU memory (VRAM) tetap terkunci, merusak eksekusi job berikutnya.

1. Isolasi Environment Menggunakan Process Group

Bungkus proses benchmark dalam process group terpisah agar signal SIGTERM dan SIGKILL dapat membersihkan seluruh child process secara tuntas.

# Jalankan proses benchmark dalam session/process group baru
set -m
python run_benchmark.py &
BENCH_PID=$!

cleanup() {
  echo "[CLEANUP] Terminating process group..."
  kill -TERM -"$BENCH_PID" 2>/dev/null || true
  sleep 3
  kill -KILL -"$BENCH_PID" 2>/dev/null || true
}

trap cleanup EXIT INT TERM
wait "$BENCH_PID"

2. Safe Rollback di Level Workflow CI

Gunakan step teardown yang diproteksi kondisi always() untuk mengembalikan runner pool ke kondisi bersih:

jobs:
  benchmark:
    runs-on: [self-hosted, benchmark-worker]
    steps:
      - uses: actions/checkout@v4

      - name: Run Benchmark with Wrapper
        run: |
          chmod +x ./scripts/run_bench.sh
          ./scripts/run_bench.sh

      - name: Runner Sanitization and Rollback
        if: always()
        run: |
          # Bersihkan residual zombie processes
          pkill -u runner -f run_benchmark.py || true
          
          # Reset resource GPU/Shared Memory jika digunakan
          ipcs -m | awk '$3 == "runner" {print $2}' | xargs -r ipcrm -m || true
          
          # Verifikasi ketersediaan disk & FD sebelum job selesai
          df -h /tmp
          ulimit -a

Postmortem Ringan dan Mitigasi Regresi

Insiden benchmark macet di CI harus diakhiri dengan evaluasi penyebab mendasar (root-cause analysis):

  1. Audit I/O Timeouts: Pastikan setiap network client (Database driver, HTTP client, GRPC channel) memiliki limit connect_timeout dan read_timeout eksplisit. Nilai default tanpa batas (infinite) adalah sumber utama deadlock di batch N.
  2. Batas Concurrency Runner: Jika batch N melibatkan beban CPU/GPU paralel, batasi concurrency worker CI (misal pytest-xdist -n 2 atau worker pool size) agar tidak memicu thread contention starvation atau OOM reaping senyap dari kernel OS.
  3. Snapshot Artifact: Saat watchdog memicu SIGABRT, konfigurasi runner CI untuk mengunggah log metrik FD, status thread dump, dan 100 baris log terakhir sebagai artifact pengujian agar investigasi dapat dilakukan secara asinkron tanpa mengulang eksekusi manual.