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

un

ضيف
1 / ?

ThreadLocal: أسلوب صحيح، عصر خاطئ

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

غيّرت تجمعات الخيوط (Thread pools) العقد. يتعامل الخيط مع الطلب A، يخزن Principal A في ThreadLocal، ينهي الطلب A، ويعود إلى الـ pool. لا تعيد تجمعات الخيوط تعيين حالة الخيط. ينظف ThreadLocal.remove()، لكن استدعاءه يتطلب انضباطاً صريحاً. عند فشل الانضباط، يعمل الطلب B على نفس الخيط ويقرأ 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 تربط قيمة بـ thread. يعيش الـ thread أطول من الطلب. عدم تطابق.

القيم المرتبطة بالنطاق (Scope-attached) تربط قيمة بوحدة عمل. عندما تنتهي وحدة العمل، تنتهي القيمة معها. لا تنظيف صريح. لا حاجة لـ 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)    # تعيين لهذه المهمة فقط
# ... معالجة المهمة ...
PRINCIPAL.reset(token)              # أو: ينتهي النطاق مع انتهاء المهمة

الخاصية المشتركة بينها: مدة الحياة تطابق وحدة العمل. عندما ينتهي الطلب (تعود run()، أو تعود الدالة، أو تكتمل المهمة)، تنتهي القيمة. لا تنظيف يُنسى. ولا تجمع يُفسد.

تحديد واستبدال

يقوم تطبيق Java EE بتخزين معرّف المستأجر (tenant ID) في ThreadLocal عند بدء الطلب. تحت الحمل العالي، يظهر معرّف المستأجر A في طلبات المستأجر B. تعيد استعلامات المستأجر B بيانات المستأجر A. لا يتم رمي أي استثناء. يظهر العيب فقط أثناء اختبار الحمل في الإنتاج.

ما هو MOAD الذي يصفه هذا؟ ما هو الحامل (carrier) الذي أتاح حدوث العيب؟ ما الذي يحل محله، وما هي خاصية البديل التي تمنع التسرب؟