Pada arsitektur self-learning AI agents, worker harvester bertugas mengekstraksi instruksi, skema alat, atau kapabilitas baru ke dalam file katalog terpusat, umumnya bernama AGENTS.md. Masalah muncul ketika beberapa worker berjalan paralel dan memutasi file Markdown secara langsung. Pembacaan file oleh worker lain menghasilkan crash mendadak: Unexpected EOF atau ScannerError: mapping values are not allowed here saat mem-parsing YAML frontmatter.
Masalah ini disebabkan oleh read-write race condition. Artikel ini membedah langkah investigasi I/O di tingkat kernel Linux dan menyajikan solusi definitif menggunakan advisory locking (flock) serta POSIX atomic rename.
Gejala dan Log Kerusakan
Saat beban harvester meningkat, worker yang bertugas memuat dependensi skill dari AGENTS.md melempar exception saat parsing metadata:
yaml.scanner.ScannerError: while scanning a block scalar
in "AGENTS.md", line 42, column 5
expected <block end>, but found '<stream end>'
yaml.parser.ParserError: Unexpected EOF while parsing frontmatterPemeriksaan manual pada file menunjukkan struktur Markdown valid setelah crash terjadi. Ini mengindikasikan file sempat berada dalam status tidak lengkap (partially written) tepat pada saat proses consumer melakukan eksekusi read().
Investigasi: Tracing I/O dengan strace dan lsof
Langkah awal diagnosis adalah memverifikasi apakah ada multiple file descriptor aktif yang menulis ke file target secara bersamaan.
1. Verifikasi File Descriptor dengan lsof
Jalankan lsof terhadap path AGENTS.md selama siklus kerja harvester berlangsung:
$ lsof /var/app/data/AGENTS.md
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
harvester-1 4102 deploy 3w REG 8,1 14205 1845201 /var/app/data/AGENTS.md
harvester-2 4108 deploy 3w REG 8,1 14205 1845201 /var/app/data/AGENTS.md
agent-core 4115 deploy 4r REG 8,1 8192 1845201 /var/app/data/AGENTS.mdOutput menunjukkan PID 4102 dan 4108 membuka file dalam mode write (3w), sementara PID 4115 sedang membaca (4r) dengan offset pembacaan parsial (8192 byte dari total 14205 byte).
2. Trace Syscall dengan strace
Gunakan strace untuk melihat urutan system call pada proses yang crash:
$ strace -f -p 4115 -e trace=openat,read,write,close
[pid 4115] openat(AT_FDCWD, "/var/app/data/AGENTS.md", O_RDONLY) = 4
[pid 4102] openat(AT_FDCWD, "/var/app/data/AGENTS.md", O_WRONLY|O_CREAT|O_TRUNC) = 3
[pid 4102] write(3, "---\nname: code_search\n", 23) = 23
[pid 4115] read(4, "---\nname: code_search\n", 8192) = 23
[pid 4115] read(4, "", 8169) = 0
[pid 4115] close(4) = 0
[pid 4102] write(3, "description: semantic search\n---\n...", 4096) = 4096Urutan kejadian di atas mengungkap akar masalah:
- Worker pembaca (PID
4115) membuka file untuk diproses. - Worker penulis (PID
4102) mengeksekusiopenatdengan flagO_TRUNC, memotong panjang file menjadi 0 byte secara seketika. - PID
4115membaca buffer yang baru terisi sebagian (23 byte). Pemanggilanread()kedua mengembalikan0(EOF). - Parser YAML/Markdown menerima stream terpotong dan memicu crash
Unexpected EOF.
Akar Masalah: Tidak Ada Isolasi Mutasi
Kode legacy menggunakan mode penulisan langsung (open(path, 'w') atau open(path, 'a')). Masalah teknis pada pendekatan ini:
- Non-atomic Truncation: Flag
O_TRUNCmengosongkan file sebelum konten baru selesai ditulis ke storage disk. - Interleaved Writes: Ketika dua worker menulis secara bersamaan via mode append, blok I/O yang melebihi buffer page OS dapat terfragmentasi dan saling menimpa (lost updates).
- Unprotected Read Phase: Reader tidak mengetahui apakah proses flush I/O worker lain telah tuntas atau belum.
Solusi: Atomic Write via Tempfile Rename dan Advisory Lock
Perbaikan membutuhkan dua lapis proteksi:
- POSIX
rename(2): Tulis data lengkap ke file temporer di mount point yang sama, lalu timpa file target menggunakan operasi atomic rename. Pembaca akan selalu melihat versi lama yang utuh atau versi baru yang utuh, tanpa kondisi file kosong atau parsial. - Advisory Locking (
flock(2)): Sinkronisasi antar-harvester untuk memastikan operasi read-modify-write tidak saling menimpa riwayat penambahan skill (serialized execution).
Implementasi Backend Python
import os
import fcntl
import tempfile
from contextlib import contextmanager
LOCK_FILE_PATH = "/var/app/data/.agents_md.lock"
TARGET_FILE_PATH = "/var/app/data/AGENTS.md"
@contextmanager
def file_lock(lock_path: str):
"""Advisory lock menggunakan fcntl.flock untuk mengisolasi mutasi file."""
lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
yield
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
os.close(lock_fd)
def safe_atomic_update_agents(target_path: str, new_content: str):
target_dir = os.path.dirname(os.path.abspath(target_path))
# 1. Isolasi proses konkuren dengan advisory lock
with file_lock(LOCK_FILE_PATH):
# 2. Tulis ke file temporer di partisi/direktori yang sama
with tempfile.NamedTemporaryFile(
mode="w",
dir=target_dir,
delete=False,
encoding="utf-8"
) as temp_file:
temp_path = temp_file.name
temp_file.write(new_content)
temp_file.flush()
os.fsync(temp_file.fileno()) # Pastikan blok data tertulis ke disk
# 3. Ganti file target secara atomik (POSIX rename)
# ponytail: os.replace mengasumsikan file ada di filesystem/mount yang sama
os.replace(temp_path, target_path)
# Verifikasi self-check
if __name__ == "__main__":
sample_data = "---\nversion: 1.0\nskills:\n - code_analyzer\n---\n# Agents Config"
safe_atomic_update_agents(TARGET_FILE_PATH, sample_data)
with open(TARGET_FILE_PATH, "r", encoding="utf-8") as f:
assert "code_analyzer" in f.read()Mengapa Pendekatan Ini Bekerja
os.fsync(): Memaksa kernel melakukan flush dirty pages dari OS page cache ke media penyimpanan fisik sebelum modifikasi metadata direktori dilakukan.os.replace(): Pada sistem POSIX, pemanggilan syscallrename(2)adalah operasi atomik. Jika pointer file target diganti saat reader sedang mengakses inode lama, reader tetap membaca salinan data lama sampai file descriptor ditutup. Tidak akan pernah terjadi status file terpotong (0 byte).- Named Lock File: Mengunci file terpisah (
.agents_md.lock) alih-alih mengunciAGENTS.mdlangsung. Ini menghindari masalah inode dangling, di mana penggantian inode olehos.replacemembuat proses lain memegang lock pada inode lama yang sudah terlepas (unlinked).
Batasan dan Trade-Off
- Cross-Device Links (
EXDEV):os.replace()gagal jika file temporer dan target berada di mount point berbeda (misal,/tmpberada di tmpfs RAM dan data di SSD). Selalu definisikandir=target_dirpadaNamedTemporaryFile. - Distributed File Systems:
flock(2)tidak sepenuhnya dapat diandalkan pada network filesystem lama (NFSv3). Untuk infrastruktur multi-node, alihkan mekanisme locking ke distributed lock manager seperti Redis (Redlock) atau etcd.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!