ThreadLocal: სწორი იდიომა, არასწორი ეპოქა
Java EE Servlet კონტეინერები, დაახლოებით 1999 წელი: ერთი Thread ერთ მოთხოვნაზე. Thread ამუშავებს ზუსტად ერთ მოთხოვნას დასაწყისიდან ბოლომდე, შემდეგ წყდება. ThreadLocal ინახავს მნიშვნელობას, რომელიც დაკავშირებულია მიმდინარე Thread-თან. ერთი-Thread-ერთ-მოთხოვნა მოდელში ThreadLocal-ში შენახული მნიშვნელობა ეკუთვნის ზუსტად ერთ მოთხოვნას. იდიომა: სწორი.
Thread პულებმა შეცვალეს კონტრაქტი. Thread ამუშავებს მოთხოვნა A-ს, ინახავს principal A-ს ThreadLocal-ში, ამთავრებს მოთხოვნა A-ს და უბრუნდება პულს. Thread პულები არ აღადგენენ Thread-ის მდგომარეობას. ThreadLocal.remove() ასუფთავებს, მაგრამ მისი გამოძახება მოითხოვს აშკარა დისციპლინას. როდესაც დისციპლინა ვერ ხერხდება, მოთხოვნა B მუშაობს იმავე Thread-ზე და კითხულობს principal A-ს ThreadLocal-ში.
გაჟონვის 5-საფეხურიანი პროცესი:
1. მოთხოვნა A მოდის. სერვერი ანიჭებს Thread-7-ს.
2. Thread-7 იწყებს მოთხოვნას და აყენებს ThreadLocal.set(principal_A).
3. მოთხოვნა A სრულდება. Thread-7 ბრუნდება პულში. ThreadLocal.remove() არ არის გამოძახებული.
4. მოთხოვნა B მოდის. სერვერი Thread-7-ს ანიჭებს (პულის ხელახალი გამოყენება).
5. Thread-7 კითხულობს ThreadLocal.get(): აბრუნებს principal_A. მოთხოვნა B მუშაობს არასწორი იდენტობით.
რატომ არ ამჩნევენ ტესტები
ერთეული ტესტები მუშაობენ იზოლირებულად: არ არის თრედების პული, არ არის ხელახალი გამოყენება. ინტეგრაციული ტესტები იყენებენ ახალ თრედებს ან აღადგენენ მდგომარეობას ტესტებს შორის. დატვირთვის ტესტები იწყება სწორი მომხმარებლებით და დაბალი კონკურენტობით. დეფექტი ვლინდება მხოლოდ თრედების პულის ხელახალი გამოყენებისას გადაფარული მოთხოვნებით — პირობა, რომელიც წარმოიქმნება წარმოებაში ნორმალური ტრაფიკის დროს და არა არცერთ ტესტურ კონფიგურაციაში, რომელიც ამას ამოწმებს.
უსაფრთხოების შედეგი
მომხმარებლის A პრინციპალი გადადის მომხმარებლის B მოთხოვნაში. არ არის კრახი. არ არის გამონაკლისი. უჩუმარი უსაფრთხოების საზღვრის დარღვევა: მომხმარებელი B ასრულებს მოქმედებებს როგორც მომხმარებელი A, კითხულობს მომხმარებლის A მონაცემებს ან გვერდს უვლის მომხმარებლის B უფლებებს. სისტემა არ აწარმოებს შეცდომას. ლოგებში ჩანს, რომ მოთხოვნა B ავტორიზებული იყო. ყველაფერი სწორად გამოიყურება.
ხუთი ნაბიჯი
ThreadLocal-ის გაჟონვის ხუთი ნაბიჯი მნიშვნელოვანია ზუსტად იმიტომ, რომ დეფექტი არ ჩნდება იმ მომენტში, როცა არასწორი კოდი სრულდება. ის წარმოიქმნება ადრე, დასუფთავების ნაბიჯის არარსებობის გამო.
სკოუპთან მიმაგრებული მნიშვნელობები
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
# ContextVar თითოეული ასინქრონული ამოცანისთვის
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal) # დაყენება მხოლოდ ამ ამოცანისთვის
# ... ამოცანის დამუშავება ...
PRINCIPAL.reset(token) # ან: სკოუპი სრულდება ამოცანასთან ერთად
თვისება, რომელიც მათ აერთიანებს: სიცოცხლის ხანგრძლივობა ემთხვევა სამუშაო ერთეულს. როდესაც მოთხოვნა სრულდება (run() ბრუნდება, ფუნქცია ბრუნდება, ამოცანა სრულდება), მნიშვნელობაც სრულდება. არაფერია დასამახსოვრებელი გასასუფთავებლად. არაფერია დასაზიანებელი პულში.
Identify & Replace
Java EE აპლიკაცია ინახავს tenant ID-ს ThreadLocal-ში მოთხოვნის დასაწყისში. მაღალი დატვირთვის დროს tenant A-ს ID ჩნდება tenant B-ს მოთხოვნებში. Tenant B-ს მოთხოვნები აბრუნებს tenant A-ს მონაცემებს. გამონაკლისი არ ჩნდება. დეფექტი მხოლოდ წარმოების დატვირთვის ტესტირებისას ვლინდება.