English· Español· Deutsch· Nederlands· Français· 日本語· ქართული· 繁體中文· 简体中文· Português· Русский· العربية· हिन्दी· Italiano· 한국어· Polski· Svenska· Türkçe· Українська· Tiếng Việt· Bahasa Indonesia

un

tamu
1 / ?
kembali ke pelajaran

ThreadLocal: Idiom yang Benar, Era yang Salah

Kontainer Servlet Java EE, sekitar 1999: satu thread per request. Sebuah thread menangani tepat satu request dari awal hingga selesai, lalu berakhir. ThreadLocal menyimpan nilai yang diikat ke thread saat ini. Dengan model satu-thread-per-request, nilai yang disimpan di ThreadLocal hanya milik satu request. Idiom ini: benar.

Thread pool mengubah kontrak tersebut. Sebuah thread menangani request A, menyimpan principal A di ThreadLocal, menyelesaikan request A, & kembali ke pool. Thread pool tidak mereset status thread. ThreadLocal.remove() membersihkan, tetapi memanggilnya memerlukan disiplin eksplisit. Ketika disiplin gagal, request B berjalan di thread yang sama & membaca principal A di ThreadLocal.

Kebocoran 5-langkah:

1. Request A tiba. Server menetapkan Thread-7.

2. Thread-7 memanggil ThreadLocal.set(principal_A) saat awal request.

3. Request A selesai. Thread-7 dikembalikan ke pool. ThreadLocal.remove() tidak dipanggil.

4. Request B tiba. Server menugaskan Thread-7 (reuse dari pool).

5. Thread-7 membaca ThreadLocal.get(): mengembalikan principal_A. Request B berjalan dengan identitas yang salah.

Mengapa Tes Tidak Menemukannya

Unit test berjalan secara terisolasi: tidak ada thread pool, tidak ada reuse. Integration test menggunakan thread baru atau mereset state antar tes. Load test melakukan pemanasan dengan user yang benar dan konkurensi rendah. Cacat ini hanya muncul saat thread pool reuse dengan request yang tumpang tindih, kondisi yang terjadi di produksi pada lalu lintas normal, bukan pada konfigurasi tes mana pun yang memeriksanya.

Konsekuensi Keamanan

Principal milik User A bocor ke request User B. Bukan crash. Bukan exception. Pelanggaran batas keamanan yang diam-diam: User B melakukan aksi sebagai User A, membaca data User A, atau melewati izin User B. Sistem tidak menghasilkan error. Log menunjukkan request B telah diotorisasi. Semuanya tampak benar.

The Five Steps

Lima langkah kebocoran ThreadLocal penting karena: cacat tidak terjadi saat kode yang salah dijalankan. Cacat terjadi lebih awal, yaitu saat tidak ada langkah pembersihan.

Jalankan kelima langkah tersebut. Pada langkah mana cacat terjadi, dan mengapa rangkaian pengujian bisa melewatkannya?

Nilai yang Melekat pada Scope

ThreadLocal menempelkan nilai ke sebuah thread. Sebuah thread hidup lebih lama daripada sebuah request. Ketidakcocokan.

Nilai yang dilekatkan pada scope menempelkan nilai ke sebuah unit kerja. Ketika unit kerja berakhir, nilai tersebut ikut berakhir bersamanya. Tanpa pembersihan eksplisit. Tanpa remove() untuk melupakan.

Java 21: ScopedValue

// ThreadLocal (pembawa DEFECT)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal);           // di-set saat request dimulai
// ... penanganan request ...
PRINCIPAL.remove();                 // HARUS dipanggil; sering terlupakan

// ScopedValue (pembawa yang BENAR)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
// ... penanganan request ...
// nilai otomatis hilang saat run() selesai
});

Go: context.Context

// context.Context membawa nilai secara eksplisit; scope = rantai pemanggilan fungsi
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx)  // ctx dilewatkan secara eksplisit; hilang saat fungsi selesai

Python asyncio: contextvars.ContextVar

# ContextVar yang dicakup untuk setiap tugas async
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal)    # hanya diatur untuk tugas ini
# ... penanganan tugas ...
PRINCIPAL.reset(token)              # atau: cakupan berakhir bersama tugas

Properti yang mereka miliki bersama: masa hidup sesuai dengan unit kerja. Ketika permintaan berakhir (run() mengembalikan nilai, fungsi mengembalikan nilai, tugas selesai), nilainya pun berakhir. Tidak ada pembersihan yang terlupakan. Tidak ada pool yang bisa rusak.

Identifikasi & Ganti

Aplikasi Java EE menyimpan ID tenant dalam ThreadLocal saat awal request. Di bawah beban tinggi, ID tenant A muncul di request dari tenant B. Query tenant B mengembalikan data tenant A. Tidak ada exception yang dilempar. Cacat ini hanya muncul saat pengujian beban produksi.

MOAD apa yang digambarkan ini? Carrier apa yang memungkinkan cacat ini? Apa yang menggantikannya, dan properti apa dari pengganti tersebut yang mencegah kebocoran?