Arsitektur flat-file textboard seperti cl-bbs menyimpan seluruh thread dan metadata dalam file teks mentah tanpa relational database management system (RDBMS). Masalah utama muncul saat lonjakan traffic: terjadi race condition File I/O yang menyebabkan korupsi data, baris terpotong, atau hilangnya entri thread baru saat beberapa worker proses menulis ke file yang sama secara bersamaan.

Gejala Korupsi Data Thread pada Textboard Flat-File

Pada beban konkurensi tinggi, backend forum flat-file menunjukkan dua anomali utama:

  • Interleaved Lines: Dua postingan dari dua user berbeda bercampur dalam satu baris file teks karena buffer write dieksekusi bersamaan.
  • Lost Updates: Counter reply atau file metadata index tertimpa (overwritten) oleh thread worker yang membaca nilai lama sebelum worker lain selesai menulis pembaruan (read-modify-write hazard).

Penyebab langsungnya adalah eksekusi append non-atomik dan ketiadaan sinkronisasi level OS antar worker proses yang terpisah (misalnya backend yang berjalan di balik FastCGI, uWSGI, atau multi-process server).

Root Cause: Ketiadaan Advisory Locking dan Batasan O_APPEND

Pengembang sering mengasumsikan flag O_APPEND pada POSIX selalu menjamin penulisan atomik. Anggapan ini keliru pada sistem tertentu:

  • Operasi write() hanya dijamin atomik oleh standar POSIX jika payload berada di bawah ukuran PIPE_BUF dan filesystem mendukung semantik atomik murni. Jika postingan thread berisi teks panjang (melebihi buffer page 4KB), kernel dapat memecahnya menjadi beberapa batch I/O, membuka celah interleaved write.
  • Metadata file (seperti threads.json atau index.txt) yang dibaca seluruhnya ke memori, dimodifikasi, lalu ditulis ulang langsung via open(..., O_WRONLY|O_TRUNC) pasti mengalami lost updates jika tidak dikunci secara eksklusif.

Analisis Overlapping Syscalls via strace dan lsof

Untuk membuktikan race condition terjadi di tingkat kernel, gunakan strace untuk memantau proses worker yang bersaing menulis ke file target.

# 1. Identifikasi proses worker backend yang membuka file thread
lsof /var/data/bbs/threads/12345.txt

# 2. Trace interaksi system call penulisan file secara konkuren
# Monitor syscall openat, write, read, dan flock
strace -f -tt -T -e trace=openat,write,read,flock,close -p <PID_WORKER_1> -p <PID_WORKER_2>

Output strace tipikal saat terjadi race condition tanpa lock:

[PID 2104] 14:02:01.100120 write(4, "Nama: Anon\nIsi: Halo dunia\n", 29) = 29 <0.000045>
[PID 2105] 14:02:01.100135 write(4, "Nama: User2\nIsi: Test...", 25) = 25 <0.000042>
[PID 2104] 14:02:01.100180 write(4, "---\n", 4) = 4 <0.000015>

Syscall write PID 2105 tersisip di antara payload data dan delimiter record PID 2104. File data menjadi korup karena pembatas record terpisah.

Implementasi Perbaikan: Advisory Locking dan Atomic Rename

Solusi teknis membutuhkan dua strategi berbeda tergantung pada operasi data:

  1. Append Postingan: Gunakan flock(2) dengan flag LOCK_EX sebelum menulis dan pastikan buffer di-flush ke disk menggunakan fsync() sebelum melepas lock.
  2. Update Metadata Index: Tulis data ke file temporer pada filesystem/mount point yang sama, panggil fsync(), lalu lakukan atomic replace menggunakan rename(2).

1. Sinkronisasi Append via flock

int append_post(const char *thread_path, const char *payload, size_t len) {
    int fd = open(thread_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd < 0) return -1;

    // Akuisisi exclusive advisory lock (blocking)
    if (flock(fd, LOCK_EX) < 0) {
        close(fd);
        return -1;
    }

    ssize_t written = write(fd, payload, len);
    
    // Flush buffer kernel ke media penyimpanan
    fdatasync(fd);

    // Lepas lock dan tutup file descriptor
    flock(fd, LOCK_UN);
    close(fd);

    return (written == (ssize_t)len) ? 0 : -1;
}

2. Atomic Replace untuk File Metadata

Jangan pernah membuka file index langsung dengan mode truncate (O_TRUNC). Tulis perubahan ke file staging terlebih dahulu:

# pseudocode alur atomic metadata replace
temp_file = "/var/data/bbs/index.json.tmp." + pid
write_all_data(temp_file, new_json_bytes)
fsync(temp_file)

# rename(2) adalah operasi atomik dalam filesystem yang sama
rename(temp_file, "/var/data/bbs/index.json")

Uji Konkurensi: Verifikasi Bebas Race Condition

Jalankan skrip Bash ini untuk menguji apakah implementasi penulisan postingan aman dari kondisi balapan:

#!/usr/bin/env bash
set -euo pipefail

TARGET_FILE="/tmp/test_thread.txt"
NUM_WORKERS=20
WRITES_PER_WORKER=50
rm -f "$TARGET_FILE"

write_record() {
    local worker_id="$1"
    local seq="$2"
    # Payload valid satu baris lengkap
    local payload="worker_${worker_id}_seq_${seq}_padding_xxxxxxxxxxxxxxxxxxxxxx"
    
    # Simulasi eksekusi backend yang menggunakan flock
    (
        flock -x 200
        echo "$payload" >> "$TARGET_FILE"
    ) 200>"/tmp/test_thread.lock"
}

echo "Menjalankan simulasi konkurensi..."
for w in $(seq 1 $NUM_WORKERS); do
    (
        for i in $(seq 1 $WRITES_PER_WORKER); do
            write_record "$w" "$i"
        done
    ) &
done

wait
echo "Semua worker selesai."

TOTAL_EXPECTED=$(( NUM_WORKERS * WRITES_PER_WORKER ))
TOTAL_ACTUAL=$(wc -l < "$TARGET_FILE")

if [ "$TOTAL_ACTUAL" -eq "$TOTAL_EXPECTED" ]; then
    echo "PASS: Integritas data valid ($TOTAL_ACTUAL/$TOTAL_EXPECTED baris)."
else
    echo "FAIL: Korupsi terdeteksi! Tercatat: $TOTAL_ACTUAL, Seharusnya: $TOTAL_EXPECTED"
    exit 1
fi

Trade-off dan Limitasi

Penggunaan advisory locking (flock) menyelesaikan masalah integritas data, tetapi memperkenalkan trade-off throughput. File I/O lock bersifat sequential; saat antrean write memuncak, request latency meningkat karena lock contention. Jika volume penulisan melebihi kapabilitas I/O lokal disk, gantikan flat-file backend dengan SQLite (WAL mode) atau storage engine berbasis LSM-tree.