un

guest
1 / ?
back to lessons

ThreadLocal: الجملة الصحيحة، العصر الخاطئ

Java EE Servlet containers، حوالي 1999: عقدة واحدة لكل طلب. العقدة تتعامل مع طلب واحد من البداية إلى النهاية ثم تتحول. ThreadLocal يحمل قيمة مفهرسة بناءً على العقدة الحالية. مع وجود عقدة واحدة لكل طلب، القيمة المخزنة في ThreadLocal تعود إلى طلب واحد فقط. الجملة: صحيحة.

خزانت المخازن تغير العقدة. العقدة تتعامل مع طلب أ، وتحمل أ في ThreadLocal، وتكتمل طلب أ، وتعود العقدة إلى المخزن. المخازن لا تستعادة حالة العقدة. ThreadLocal.remove() يقوم بتنظيف، لكن استدعاؤه يتطلب التدين المحدد. عند عدم التمكن من ذلك، يقرأ طلب ب على نفس العقدة ويقرأ هوية أ في ThreadLocal.

الخدش الخماسي:

1. يصل طلب أ. يخصص الخادم Thread-7.

2. Thread-7 يضع ThreadLocal.set(principal_A) عند بدء الطلب.

3. يكتمل طلب أ. Thread-7 تعود إلى المخزن. ThreadLocal.remove() لا يتم استدعاؤه.

4. يصل طلب ب. يخصص الخادم Thread-7 (استخدام المخزن).

5. Thread-7 يقرأ ThreadLocal.get(): يعود principal_A. يُجرى طلب ب تحت الهوية الخاطئة.

لماذا تفشل الاختبارات في اكتشافه

اختبارات الوحدة تُجرى في عزلة: لا توجد خزنة، لا توجد استعادة. اختبارات التكامل تستخدم عروق جديدة أو تعيد تعيين الحالة بين الاختبارات. اختبارات الشحن يُسخن بملفات المستخدمين الصحيحة وتباين التزامن المنخفض. يظهر العيب فقط عند استعادة خزنة العقدة مع استمرار الطلبات، وهو ظرف يظهر في الإنتاج تحت حركة المرور العادية وليس في أي تكوين اختبار ي провер ذلك.

النتيجة الأمنية

هوية مستخدم أ تتدفق إلى طلب مستخدم ب. ليس سقوطا. ليس استثناء. خرق سري للحدود الأمنية: يقوم مستخدم ب بتنفيذ عمليات تحت هويته الخاصة هي هوية أ، وقراءة بيانات مستخدم أ، أو تجاوز صلاحيات مستخدم ب. يعمل النظام بشكل صحيح. تظهر السجلات أن طلب ب تمت الموافقة عليه.

الخمس خطوات

تعد الخطوات الخمسة من خدش ThreadLocal مهمة بشكل دقيق: لا يحدث العيب في اللحظة التي يُجرى فيها الكود الخاطئ. يحدث في وقت سابق، في غياب خطوة التنظيف.

اتباع الخطوات الخمسة. في أي خطوة يحدث العيب، ولماذا ستفشل وحدة الاختبار في اكتشافه؟

القيم المتصل بالمنطقة

ThreadLocal يرتبط القيمة بالجدار. الجدار يمتد بعد الطلب. تناقض.

القيم المتصلة بالمنطقة ترتبط بالقيمة العاملة. عند نهاية الوحدة العاملة، تنتهي القيمة أيضًا. لا تنظيف تلقائي. لا remove() لإعادة النظر في القيمة.

Java 21: ScopedValue

// ThreadLocal (مستودع العيب)
static final ThreadLocal<Principal> PRINCIPAL = new ThreadLocal<>();
PRINCIPAL.set(principal);           // تعيين في بداية الطلب
// ... معالجة الطلب ...
PRINCIPAL.remove();                 // يجب استدعاؤه؛ غالبا ما ينسى

// ScopedValue (مستودع صحيح)
static final ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
ScopedValue.where(PRINCIPAL, principal).run(() -> {
    // ... معالجة الطلب ...
    // القيمة تختفي تلقائيًا عند عودة run() 
});

Go: context.Context

// context.Context يحمل القيم بشكل صريح؛ المدى = سلسلة استدعاء الوظيفة
ctx := context.WithValue(r.Context(), principalKey, principal)
handleRequest(ctx)  // ctx يتم تمريره صراحة؛ يختفي عند عودة الوظيفة

Python asyncio: contextvars.ContextVar

# ContextVar متصل بالوحدة العاملة لكل مهمة آسيو
PRINCIPAL: ContextVar[str] = ContextVar('principal')
token = PRINCIPAL.set(principal)    # تعيين لهذه المهمة فقط
# ... task handling ...
PRINCIPAL.reset(token)              # or: scope ends with task

The property these share: lifetime matches the unit of work. When the request ends (the run() returns, the function returns, the task completes), the value ends. No cleanup to forget. No pool to corrupt.

Identify & Replace

A Java EE application stores tenant ID in a ThreadLocal at request start. Under high load, tenant A's ID appears in requests from tenant B. Tenant B's queries return tenant A's data. No exception gets thrown. The defect only appears in production load testing.

What MOAD does this describe? What carrier made the defect possible? What replaces it, & what property of the replacement prevents the leak?