ThreadLocal: सही मुहावरा, गलत युग
Java EE Servlet कंटेनर, लगभग 1999: एक थ्रेड प्रति अनुरोध। एक थ्रेड ठीक एक अनुरोध को शुरू से अंत तक हैंडल करता है, फिर समाप्त हो जाता है। ThreadLocal वर्तमान थ्रेड के लिए कुंजीबद्ध मान संग्रहीत करता है। एक-थ्रेड-प्रति-अनुरोध के साथ, ThreadLocal में संग्रहीत मान ठीक एक अनुरोध का होता है। मुहावरा: सही।
थ्रेड पूल ने अनुबंध बदल दिया। एक थ्रेड अनुरोध A को हैंडल करता है, ThreadLocal में प्रिंसिपल A संग्रहीत करता है, अनुरोध A समाप्त करता है, और पूल में लौटता है। थ्रेड पूल थ्रेड स्थिति रीसेट नहीं करते। ThreadLocal.remove() सफाई करता है, लेकिन इसे कॉल करने के लिए स्पष्ट अनुशासन की आवश्यकता होती है। जब अनुशासन विफल होता है, अनुरोध B उसी थ्रेड पर चलता है और ThreadLocal में प्रिंसिपल A पढ़ता है।
5-चरणीय लीक:
1. अनुरोध A आता है। सर्वर Thread-7 असाइन करता है।
2. Thread-7 अनुरोध शुरू होने पर ThreadLocal.set(principal_A) सेट करता है।
3. Request A पूरा होता है। Thread-7 पूल में लौटता है। ThreadLocal.remove() कॉल नहीं किया गया।
4. Request B आता है। सर्वर Thread-7 को असाइन करता है (पूल पुनः उपयोग)।
5. Thread-7 ThreadLocal.get() पढ़ता है: principal_A लौटाता है। Request B गलत पहचान के साथ चलता है।
परीक्षण इसे क्यों मिस करते हैं
यूनिट परीक्षण अलगाव में चलते हैं: कोई थ्रेड पूल नहीं, कोई पुनः उपयोग नहीं। इंटीग्रेशन परीक्षण नए थ्रेड्स का उपयोग करते हैं या परीक्षणों के बीच स्थिति रीसेट करते हैं। लोड परीक्षण सही उपयोगकर्ताओं और कम समवर्तीता के साथ वार्म-अप करते हैं। दोष केवल थ्रेड पूल पुनः उपयोग और ओवरलैपिंग अनुरोधों के तहत प्रकट होता है, एक ऐसी स्थिति जो सामान्य ट्रैफ़िक के तहत प्रोडक्शन में दिखाई देती है, न कि किसी भी परीक्षण कॉन्फ़िगरेशन में जो इसे चेक करता है।
सुरक्षा परिणाम
User A का principal User B के अनुरोध में लीक हो जाता है। कोई क्रैश नहीं। कोई अपवाद नहीं। एक मौन सुरक्षा सीमा उल्लंघन: User B User A के रूप में कार्रवाई करता है, User A का डेटा पढ़ता है, या User B की अनुमतियों को बायपास करता है। सिस्टम कोई त्रुटि उत्पन्न नहीं करता। लॉग्स दिखाते हैं कि Request B अधिकृत था। सब कुछ सही दिखता है।
The Five Steps
ThreadLocal leak के पाँच चरण ठीक उसी तरह मायने रखते हैं: दोष उस समय नहीं होता जब गलत कोड चलता है। यह पहले होता है, cleanup चरण की अनुपस्थिति में।
Scope-Attached Values
ThreadLocal एक मान को थ्रेड से जोड़ता है। एक थ्रेड अनुरोध से अधिक समय तक जीवित रहता है। बेमेल।
Scope-attached मान एक कार्य इकाई से जुड़ते हैं। जब कार्य इकाई समाप्त होती है, तो मान भी उसके साथ समाप्त हो जाता है। कोई स्पष्ट सफाई नहीं। remove() भूलने के लिए नहीं।
Java 21: ScopedValue
// ThreadLocal (DEFECT carrier)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal); // set at request start
// ... request handling ...
PRINCIPAL.remove(); // अवश्य कॉल करें; अक्सर भुला दिया जाता है
// ScopedValue (सही कैरियर)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
// ... अनुरोध हैंडलिंग ...
// run() लौटने पर मान स्वचालित रूप से हट जाता है
});
Go: context.Context
// context.Context मानों को स्पष्ट रूप से ले जाता है; scope = फंक्शन कॉल चेन
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx) // ctx स्पष्ट रूप से पास किया गया; फंक्शन रिटर्न होने पर समाप्त
Python asyncio: contextvars.ContextVar
# प्रत्येक async task के लिए scoped ContextVar
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal) # केवल इस task के लिए सेट करें
# ... task handling ...
PRINCIPAL.reset(token) # या: task के साथ scope समाप्त हो जाता है
इनमें जो गुण समान है: lifetime, work की इकाई से मेल खाता है। जब request समाप्त होती है (run() लौटता है, function लौटता है, task पूरा होता है), तो value भी समाप्त हो जाती है। कोई cleanup भूलने की चिंता नहीं। कोई pool corrupt होने का खतरा नहीं।
Identify & Replace
एक Java EE एप्लिकेशन अनुरोध शुरू होने पर ThreadLocal में tenant ID संग्रहीत करता है। उच्च लोड के तहत, tenant A का ID tenant B के अनुरोधों में दिखाई देता है। Tenant B की क्वेरीज़ tenant A का डेटा लौटाती हैं। कोई अपवाद नहीं फेंका जाता। यह दोष केवल प्रोडक्शन लोड टेस्टिंग में दिखाई देता है।