Memindahkan arsitektur multi-agent dari cloud workspace terkelola ke lingkungan worker lokal sering memicu kegagalan konkurensi. Di lingkungan cloud prototype, eksekusi agent sering kali berjalan dalam satu thread, dieksekusi secara sekuensial oleh orchestrator bawaan, atau bergantung pada memori internal runtime. Begitu sistem dideploy ke beberapa proses worker lokal independen, shared memory terdistribusi menjadi rentan terhadap race condition pemanggilan tool dan benturan write-back state.

Akar Masalah: Perubahan Pola Konkurensi Pasca-Export

Saat agent dijalankan pada worker terdistribusi, agent berbagi resource eksternal yang sama seperti database, tool pihak ketiga, dan shared cache. Dua anomali utama yang sering muncul adalah:

  • Tool Call Race Condition: Dua agent memanggil tool mutasi yang sama secara paralel (misalnya, memodifikasi alokasi saldo atau reservasi resource) dengan asumsi state awal yang sama.
  • Cache Overwrite (Lost Updates): Agent A dan Agent B membaca snapshot state dari Redis pada waktu T0. Agent A selesai pada T1 dan menyimpan modifikasi. Agent B selesai pada T2 dan menimpa perubahan Agent A dengan data yang berbasis snapshot usang.

Distributed Locking: Redis Mutex Implementation

Solusi standar industri untuk mencegah race condition pada multi-worker lokal adalah mengisolasi eksekusi resource kritis menggunakan distributed lock. Pola paling aman menggunakan Redis adalah perintah SET resource_key token NX PX milliseconds yang memastikan akuisisi terjadi secara atomik.

Pelepasan lock wajib menggunakan Lua script guna memastikan token acak (fencing token) cocok sebelum kunci dihapus, mencegah worker lambat menghapus lock milik worker lain yang sudah habis masa berlakunya (TTL expiry).

Implementasi minimal Redis mutex menggunakan Python:

import time
import uuid
import redis

client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

RELEASE_LUA = """
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
"""

class DistributedLock:
    def __init__(self, key: str, ttl_ms: int = 5000):
        self.key = f"lock:{key}"
        self.ttl_ms = ttl_ms
        self.token = str(uuid.uuid4())

    def acquire(self, blocking: bool = True, timeout: float = 10.0) -> bool:
        start_time = time.time()
        while True:
            # Atomic acquisition: NX (Not Exists), PX (Expiry in milliseconds)
            if client.set(self.key, self.token, nx=True, px=self.ttl_ms):
                return True
            if not blocking or (time.time() - start_time) > timeout:
                return False
            time.sleep(0.05)

    def release(self) -> bool:
        # Atomic release with token validation
        result = client.eval(RELEASE_LUA, 1, self.key, self.token)
        return result == 1

# Penggunaan pada tool agent
def execute_agent_step(agent_id: str, resource_id: str):
    lock = DistributedLock(resource_id, ttl_ms=3000)
    if not lock.acquire(blocking=True, timeout=5.0):
        raise TimeoutError(f"Agent {agent_id} gagal memperoleh lock pada {resource_id}")
    try:
        # Jalankan eksekusi kritis tool/state update
        pass
    finally:
        lock.release()

Strategi Cache Invalidation: Write-Through vs Invalidation Fallback

Saat memperbarui shared memory di Redis, pola write-back asinkron membawa risiko data stale yang tinggi pada sistem multi-agent. Terapkan pendekatan Cache Invalidation with Versioning:

  1. Optimistic Concurrency Control (OCC): Simpan nomor versi (version counter) pada payload state di cache maupun database persistent.
  2. Explicit Invalidation: Daripada memperbarui isi cache secara langsung saat write-back, hapus kunci cache (DEL state:{agent_id}). Biarkan agent berikutnya memuat ulang data autoritatif dari database saat terjadi cache miss.
  3. TTL Safeguard: Tetapkan TTL pendek pada seluruh cache state runtime (misalnya 30–60 detik) untuk mencegah data basi menetap jika proses pub/sub invalidasi gagal.

Verifikasi Idempotensi Eksekusi Step Agent

Distributed locking mencegah eksekusi paralel serentak, tetapi tidak menghentikan duplikasi eksekusi step jika worker crash atau terjadi retry jaringan. Idempotensi step mutlak diperlukan.

Setiap mutasi step agent harus menyertakan idempotency_key unik (gabungan run_id, agent_id, dan step_index). Simpan hasil eksekusi step di Redis sebelum mengeksekusi aksi lanjutan.

def execute_idempotent_step(run_id: str, step_index: int, action_fn):
    idempotency_key = f"step_result:{run_id}:{step_index}"
    cached_result = client.get(idempotency_key)
    
    if cached_result is not None:
        return cached_result  # Kembalikan hasil sebelumnya tanpa eksekusi ulang

    result = action_fn()
    # Simpan hasil dengan TTL 24 jam untuk deduplikasi retry
    client.set(idempotency_key, str(result), ex=86400)
    return result

Trade-offs dan Kesalahan Umum

  • Deadlock karena Long-Running LLM Calls: Memegang lock Redis saat menunggu respons LLM API eksternal adalah anti-pattern. Jika endpoint timeout, worker lain akan terblokir. Solusi: Dapatkan lock hanya saat membaca state lokal dan menulis kembali hasilnya, bukan selama proses inferensi LLM.
  • Redlock Over-engineering: Algoritma Redlock (multi-node Redis) hanya dibutuhkan jika kluster Redis berdiri di atas node independen tanpa replikasi single-leader. Untuk sebagian besar setup worker lokal, single instance Redis atau Redis Sentinel sudah cukup.
  • Lock Stolen: Jika TTL terlalu pendek dan eksekusi lokal memakan waktu lebih lama, lock akan habis masa berlakunya secara otomatis dan diambil oleh worker lain. Solusinya adalah menjalankan background renewal thread (heartbeat) jika proses berjalan lebih lama dari durasi awal.