Saat mengembangkan fitur seperti iOS Share Extension atau Android Background Worker pada aplikasi React Native, komponen tersebut berjalan di proses sistem operasi (OS) yang terpisah dari Main App. Ketika kedua proses mengakses penyimpanan bersama (seperti App Group Directory atau shared external storage) secara simultan, inkonsistensi state dan korupsi data tidak terhindarkan jika sinkronisasi hanya mengandalkan mekanisme in-memory.

Akar Masalah: Mengapa In-Memory Mutex Gagal

Pustaka konkurensi JavaScript umum seperti async-mutex atau objek sinkronisasi C++ standar (seperti std::mutex) bekerja pada level virtual memory space dari satu proses. Ketika Main App dan App Extension berjalan:

  • Isolasi Memori: OS mengalokasikan ruang alamat memori terisolasi untuk tiap proses. Mutex di Main App tidak memiliki visibilitas terhadap thread di Extension.
  • MMKV Multi-process Pitfall: Meskipun MMKV mendukung mode multi-proses, sinkronisasinya berbasis sinyal internal yang tidak menjamin atomisitas transaksi multi-step read-modify-write.
  • SQLite Lock Contention: Tanpa konfigurasi yang tepat, SQLite melempar error SQLITE_BUSY atau database is locked ketika satu proses menahan write lock sementara proses lain mencoba menulis.

Konfigurasi Fondasi: SQLite WAL Mode

Sebelum menerapkan cross-process lock manual di layer aplikasi, SQLite wajib dikonfigurasi ke Write-Ahead Logging (WAL). Mode default (Rollback Journal) memblokir pembaca ketika ada penulisan. Mode WAL memungkinkan multiple concurrent readers dan satu concurrent writer.

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;

Pengaturan busy_timeout memberi toleransi pada SQLite engine untuk menunggu hingga 5 detik sebelum melempar SQLITE_BUSY jika write-lock sedang dipegang oleh proses lain. Namun, untuk operasi bertingkat (misalnya: baca data, kalkulasi bisnis di JS/Native, lalu tulis balik), transaksi SQLite saja tidak cukup mencegah race condition di tingkat aplikasi.

Solusi: File-Based Advisory Lock Menggunakan POSIX fcntl

Pendekatan lintas platform (iOS dan Android) yang paling stabil adalah memanfaatkan file-based advisory locking melalui system call POSIX fcntl. Berbeda dengan in-memory mutex, advisory lock terdaftar pada vnode/file-table di kernel OS, sehingga berlaku di semua proses yang membuka file deskriptor yang sama.

Implementasi Native Lock Helper (C++)

Kode C++ berikut dapat dipanggil langsung melalui React Native JSI atau Native Module:

#include <fcntl.h>
#include <unistd.h>
#include <chrono>
#include <thread>
#include <string>

class CrossProcessLock {
private:
    int file_desc = -1;
    std::string lock_path;

public:
    CrossProcessLock(const std::string& path) : lock_path(path) {}

    bool acquire(int timeout_ms = 3000) {
        file_desc = open(lock_path.c_str(), O_RDWR | O_CREAT, 0666);
        if (file_desc == -1) return false;

        struct flock fl;
        fl.l_type = F_WRLCK;
        fl.l_whence = SEEK_SET;
        fl.l_start = 0;
        fl.l_len = 0;

        auto start = std::chrono::steady_clock::now();
        while (fcntl(file_desc, F_SETLK, &fl) == -1) {
            auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
                std::chrono::steady_clock::now() - start
            ).count();

            if (elapsed >= timeout_ms) {
                close(file_desc);
                file_desc = -1;
                return false;
            }
            // Polling backoff setiap 20ms
            std::this_thread::sleep_for(std::chrono::milliseconds(20));
        }
        return true;
    }

    void release() {
        if (file_desc != -1) {
            struct flock fl;
            fl.l_type = F_UNLCK;
            fl.l_whence = SEEK_SET;
            fl.l_start = 0;
            fl.l_len = 0;
            fcntl(file_desc, F_SETLK, &fl);
            close(file_desc);
            file_desc = -1;
        }
    }

    ~CrossProcessLock() {
        release();
    }
};

Catatan iOS: Jika menggunakan container file sandboxed di iOS App Groups, NSFileCoordinator dapat digunakan sebagai alternatif Cocoa-native. Namun, fcntl advisory lock tetap valid selama file lock berada di dalam direktori Shared App Group Container yang dapat diakses oleh Main App dan Extension.

Integrasi pada React Native

Bungkus implementasi native di atas ke dalam Native Module/TurboModule agar dapat dipanggil secara aman dalam eksekusi kritis JavaScript:

import { NativeModules } from 'react-native';

const { CrossProcessLockModule } = NativeModules;
const SHARED_LOCK_FILE = `${SharedStorage.getAppGroupPath()}/storage.lock`;

export async function runCriticalSection<T>(task: () => Promise<T>): Promise<T> {
  const lockAcquired = await CrossProcessLockModule.acquire(SHARED_LOCK_FILE, 4000);
  if (!lockAcquired) {
    throw new Error('Lock contention timeout: Gagal mengamankan akses ke shared storage');
  }

  try {
    return await task();
  } finally {
    await CrossProcessLockModule.release(SHARED_LOCK_FILE);
  }
}

Menangani Deadlock dan Edge Cases

  • Process Crashes: Keunggulan POSIX fcntl adalah kernel OS otomatis melepaskan lock jika proses holding mengalami crash mendadak (karena file descriptor otomatis ditutup oleh kernel).
  • Timeout Contention: Selalu terapkan batas timeout (misal 3-5 detik). Hindari pemanggilan blocking tak terbatas (F_SETLKW) yang berisiko memicu Application Not Responding (ANR) di Android atau Watchdog termination di iOS.
  • Lock Granularity: Gunakan lock hanya untuk blok eksekusi yang melibatkan mutasi state I/O, jangan sertakan operasi jaringan/HTTP di dalam critical section.

Strategi Pengujian Konkurensi Multi-Proses

Validasi mekanisme lock tidak bisa diuji hanya dengan Jest standard unit test. Pengujian harus melibatkan multi-proses riil:

  1. Simulasi Skrip Shell / Dual Runner: Buat dua proses native terpisah yang membaca, meng-increment, lalu menulis kembali nilai counter di shared database sebanyak 500 iterasi secara paralel.
  2. Stress Testing tanpa Lock: Nilai akhir counter akan bernilai kurang dari 1000 akibat overwriting (lost updates).
  3. Stress Testing dengan fcntl Lock: Verifikasi nilai akhir counter tepat bernilai 1000 tanpa adanya SQLITE_BUSY uncaught exceptions.