Sessions: حالة السباق في FilesystemStore

تم إنشاؤها على ١١ يونيو ٢٠١٣  ·  23تعليقات  ·  مصدر: gorilla/sessions

هناك حالة سباق في FilesystemStore أعتزم إصلاحها ولكني أود إدخالها قبل أن أمضي قدمًا وأقوم بذلك. تكمن المشكلة في الأساس في أنه إذا كانت لديك طلبات متزامنة من نفس المستخدم (نفس الجلسة) ، فإن ما يلي ممكن:

  1. يفتح الطلب 1 الجلسة لإجراء عملية شبه طويلة
  2. طلب 2 يفتح الجلسة
  3. طلب 2 يزيل بيانات الجلسة لأداء "تسجيل الخروج" أو ما شابه
  4. طلب 2 يحفظ
  5. يحفظ الطلب 1 ، مما يجعل الجلسة كما لو لم يتم تسجيل خروجها مطلقًا

لقد أضفت حالة اختبار لهذا الخلل في cless / Session @ f84abeda17de0b4fcd72d277412f3d3192f206f2

الطريقة الأكثر مباشرة لإصلاح ذلك هي إدخال أقفال على مستوى نظام الملفات. ومع ذلك ، ليس لدى golang طريقة عبر النظام الأساسي للقيام بقفل الملف. إنه يكشف عن flock syscall لكن هذا لا يعمل إلا إذا كان نظام التشغيل يدعمه. أعتقد أن سلوك القطيع قد يكون مختلفًا أيضًا في أنظمة يونكس مختلفة على الرغم من أنني لست متأكدًا من أن هذا هو الحال. مشكلة أخرى مع القطيع هي أنه قد لا يعمل على NFS.

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

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

قد تنسخ خلفيات التخزين الأخرى المستندة إلى FilesystemStore هذا الخلل (لقد لاحظت هذه المشكلة عند مراجعة كود Redistore لمشروع خاص بي boj / redistore # 2)

bug stale

ال 23 كومينتر

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

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

ومع ذلك ، حتى إذا كان FilesystemStore يستخدم المعاملات ، أعتقد أن واجهة برمجة التطبيقات يجب أن تكون جاهزة لخلفيات التخزين التي تستخدم القفل ، ولكن هذا يعني إما عدم السماح باستدعاء session.Save() أكثر من مرة _ أو إدخال session.Release() جديد

متابعة هذه المشكلة لأنها تؤثر على RediStore. شكرا على المعلومات cless

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

هل لديك أي أمثلة على أطر عمل أخرى للجلسات تعمل على حل هذه المشكلة؟ سأكون فضوليًا لمعرفة ما يفعلونه.

أعلم أن المعالج الافتراضي لجلسة php يستخدم قفلًا قائمًا على نظام الملفات (في ملف tarball php 5.4 ، تأكدت للتو من وجود هذا في الملف ext/session/mod_files.c . وهو يستخدم flock الذي يوفره ext/standard/flock_compat.c ).

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

سأكون فضوليًا لمعرفة كيف يتعامل Flask أو Pyramid أو Django مع هذه الأشياء. سألقي نظرة عليهم إذا كان لدي الوقت. سيكون ريلز مثيرًا للاهتمام أيضًا إذا كان أي شخص على دراية بقاعدة البيانات هذه.

يبدو أن كل من Pyramid و Flask يستخدمان ملف تعريف ارتباط لتخزين البيانات ، وأظن أنه لا يتعامل مع ظروف السباق. لقد قرأت الوثائق لفترة وجيزة فقط لذا فقد أكون مخطئًا للغاية.

يدعم Django بيانات جلسة جانب الخادم لذا سألقي نظرة على هذا الرمز بعد قليل.

الخلفية الافتراضية لجلسة Django هي قاعدة البيانات وهذا يستخدم طبقة تجريد قاعدة بيانات Django التي لست على دراية بها لذا يصعب علي تفسيرها. ومع ذلك ، وجدت هذا التعليق في الواجهة الخلفية لنظام الملفات:

        # Write the session file without interfering with other threads
        # or processes.  By writing to an atomically generated temporary
        # file and then using the atomic os.rename() to make the complete
        # file visible, we avoid having to lock the session file, while
        # still maintaining its integrity.
        #
        # Note: Locking the session file was explored, but rejected in part
        # because in order to be atomic and cross-platform, it required a
        # long-lived lock file for each session, doubling the number of
        # files in the session storage directory at any given time.  This
        # rename solution is cleaner and avoids any additional overhead
        # when reading the session data, which is the more common case
        # unless SESSION_SAVE_EVERY_REQUEST = True.
        #
        # See ticket #8616.

يبدو أن هذا يشير إلى أنهم يتأكدون من أن محتويات ملف الجلسة ليست مشوهة أبدًا ولكن لا يمكن أن تحدث أي ظروف سباق. إذا كان لدى أي شخص تجربة Django ، فسيكون من الجيد رؤية حالة اختبار مثل cless / Session @ f84abed
يبدو أن Stackoverflow يشير أيضًا إلى أن Django قد يكون لديه ظروف سباق في الجلسات: http://stackoverflow.com/search؟q=django+session+race+condition

حسنًا ، يحتوي FileSystemStore بالفعل على كائن متغير خشن لمنع تلف مخزن الجلسة. لست متأكدًا من مقدار الحماية الإضافية التي تستحق وضعها في المكتبة لأنها ستضيف الكثير من النفقات العامة لكل طلب ، فقط لجعل الأمور أكثر اتساقًا مع بعض الطلبات. من المؤكد أن السيناريو هنا ممكن ، لكنني أعتقد أنه من المحتمل أن يكون معقدًا للغاية بحيث لا يمكن التعامل معه بشكل عام. أستطيع أن أرى أن هناك العديد من الحالات التي يكون فيها ما يحدث الآن على ما يرام تمامًا ، بينما قد يكون مشكلة في بعض التطبيقات. أظن أنه في معظم الأوقات سيكون لديك طلب واحد فقط على متن الطائرة لجلسة معينة على أي حال.

إنها حقًا مشكلة موجودة في أي مورد ويب ، سيحدث نفس الشيء إذا كان لديك GET متبوعًا بـ PUT استنادًا إلى المعلومات ، فربما تغيرت الأشياء في هذه الأثناء.

على أي حال ، هذه هي أفكاري في الوقت الحالي ولكن يسعدني مناقشة المزيد.

يبدو أن هذا على المدى الطويل هو أكثر من مشكلة مجال التطبيق.

بالنظر إلى السيناريو الذي تم نشره ، إذا كان من المتوقع تشغيل الطلب 1 لفترة كافية بحيث يمكن أن يحدث الطلب 2 بالتوازي (على سبيل المثال ، تطبيق صفحة واحدة مكتوب بلغة Angular.js ، أو تطبيق Node.js غير متزامن يضرب بعض واجهة برمجة التطبيقات) ، فيبدو أن يجب فصل الطلب طويل المدى عن الجلسة واعتباره عملية عاملة مستقلة في تلك المرحلة.

لا يعالج أي من هذا الموضوع بشكل مباشر بالطبع ، ولكن كما ذكر كيسيلك ، فإن القيام بأي نوع من القفل يضيف الكثير من النفقات العامة لما يبدو أنه حالة هامشية.

فيما يلي بعض الأمثلة من العالم الحقيقي لتأثير ظروف سباق الدورة. من الصعب تصحيح الأخطاء ، وتظهر على ما يبدو بشكل عشوائي وهي مصدر إزعاج لكل من المطورين والمستخدمين. بالنظر إلى تطبيق كبير بما فيه الكفاية يستخدم بشكل مكثف لأياكس ، ستظهر ظروف السباق هذه عاجلاً أم آجلاً.
http://www.hiretheworld.com/blog/tech-blog/codeigniter-session-race-conditions
http://www.chipmunkninja.com/Troubles-with-Asynchronous-Ajax-Requests-g

لقد قرأت الموضوع بأكمله في EllisLab / CodeIgniter # 1746 الذي يتعامل مع مشكلة مماثلة ، لكن مشكلتهم معقدة بسبب حقيقة أن CodeIgniter يعيد إنشاء معرف الجلسة بالقوة كل فترة ، وتدور معظم المناقشة هناك حول هذه الحقيقة .

أتفهم إحجامك عن تغيير الجلسات بطريقة غير متوافقة أو إدخال اختلافات طفيفة في واجهة برمجة التطبيقات والتي قد تؤدي إلى تعطيل التطبيقات الحالية. ومع ذلك ، ما زلت أعتقد أنه يجب أن يكون هناك بعض القفل الاختياري. ماذا عن إضافة وظيفتين إلى Store API مثل هذا:

type Store interface {
    Get(r *http.Request, name string) (*Session, error)
    New(r *http.Request, name string) (*Session, error)
    Save(r *http.Request, w http.ResponseWriter, s *Session) error
    Lock(r *http.Request, name string) error
    Release(r *http.Request, name string) error
}

سيكون استخدام هذه الوظائف اختياريًا تمامًا (على الرغم من أنني سأشجع شخصيًا على الإغلاق في كل طلب يتم إرساله إلى الجلسة) ولن يكون له أي تأثير على التطبيقات الحالية.

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

آسف فاتني رد Boj عندما نشرت لي:
من المهم أن تضع في اعتبارك أن الطلبات طويلة المدى ليست شرطًا لبدء حالة السباق. النوم 500 مللي ثانية في حالة الاختبار الخاصة بي موجود فقط لضمان تشغيل حالة السباق. في العالم الحقيقي ، ستحدث ظروف السباق هذه للطلبات "القصيرة" أيضًا ، وبتكرار أقل ، وهذا بالضبط ما يجعل من الصعب تصحيحها.

عليك أيضًا أن تضع في اعتبارك أنه في ظل الحمل الكبير ، قد تستغرق الطلبات القصيرة بعض الوقت للتنفيذ.

cless نقاط جيدة.

تعجبني تغييرات الواجهة المقترحة.

أن انتهاء القفل الخاص بك سيكون من السهل نسبيا لتنفيذ لRediStore، رديس لديه EXPIRE القيادة الذي يسمح للTTL إلى أن تعيين للحصول على مفتاح.

ألن يظل هذا النوع من القفل الدقيق غير فعال إلى حد ما للعديد من أنواع متاجر الجلسات؟ وانتهاء الصلاحية هو أيضا قضية.

يبدو أن جزءًا من المشكلة هو أن Save يمكن أن ينجح حتى إذا كانت الجلسة أطول ، ألن تقطع شوطًا طويلاً نحو حل المشكلة إذا لم يكن الأمر كذلك؟

kisielk ، هل يمكنك إعطاء أمثلة على المتاجر التي قد تكون غير فعالة؟ تحتوي معظم قواعد بيانات القيمة الرئيسية على شكل من أشكال العمليات الذرية التي تسمح لك بإنشاء أقفال. عادةً ما تتمتع قواعد بيانات SQL بإمكانية الوصول إلى التأمين المستند إلى الصفوف (على الرغم من أنني يجب أن أقول إنني لست على دراية بالتفاصيل هنا).
تتطلب طريقة قفل مخازن القيمة الرئيسية مثل redis رحلات متعددة ذهابًا وإيابًا إلى قاعدة البيانات ، ربما كان هذا ما قصدته؟
هناك مشكلة في مخازن نظام الملفات لأن go لا يوفر لنا طريقة عبر الأنظمة الأساسية للوصول إلى أقفال الملفات. أفترض أن cgo سيسمح لك بإنشاء واحد يدعم المنصات الرئيسية ، ولكن ربما يكون هذا هو الحل الذي قد أتجنبه.

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

boj ، المشكلة هي أن المبرمج يحتاج إلى طريقة ما للتحقق من أن القفل لم تنته صلاحيته بعد قبل أن يحاول الحفظ. ما هو وقت انتهاء الصلاحية على القفل؟

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

func Lock(expiration time.Duration) time.Time {
    // Block here until the lock becomes your. Set the lock to expire after
    // dur+50ms. This extra 50 ms ensures that you never assume you own an
    // expired lock
    return time.Now().Add(expiration)
}

func main() {
    end := Lock(1000 * time.Millisecond)

    // Do your thing here ...
    // time.Sleep(1100 * time.Millisecond)

    if time.Now().After(end) {
        fmt.Println("Lock expired, throw an error")
    } else {
        fmt.Println("Good to go, save the session")
    }
}

cless إن تعليقك بالكامل يلخص ما يبدو أنه مخاوف kisielk . X يفعل هذا ، Y يفعل ذلك ، Z قد لا يكون ممكنًا ما لم يتم تضمين بعض المتطرفين. في حين أن ظروف السباق غير مرغوبة نظرًا للحالات النادرة التي قد تحدث ، إلا أن تصميم الغوريلا / الجلسات الحالي على الأقل بسيط للغاية ويتبع عنصر فلسفة المفاجأة على الأقل.

قد يكون من السهل جدًا تنفيذ جزء الواجهة من الغوريلا / الجلسات كما اقترحت ، ومع ذلك ، يمكنك إنشاء سيناريو حيث قد تكون قادرًا أو لا تتمكن بسهولة من تبديل الخلفيات الخلفية ما لم يحدث تنفيذ Lock / Release بطريقة سهلة. وبطريقة فعالة. يبدو أن هناك عددًا كبيرًا جدًا من الأشياء التي تتعلق بما تقترحه ، وأهمها "هل يمكن للواجهة الخلفية حتى تنفيذ ذلك دون اللجوء إلى اختراق البرمجة؟" ، متبوعًا بـ "لقد حاولت التبديل من FileSystemStore إلى FooBarStore ، لكنها لا تفعل ذلك يبدو أنه يطبق Lock / Release ولا يتصرف البرنامج كما هو متوقع. "

هناك مقال جيد حول الفروق الدقيقة في تأمين كل جلسة هنا:

http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

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

فيما يتعلق بتطبيق Lock / Release ، يمكننا جعل تنفيذ قفل كل جلسة اختياريًا من خلال وجود واجهة منفصلة. يمكن بعد ذلك تأكيد الواجهة الخلفية مقابل تلك الواجهة وإذا لم يتم تنفيذها يمكننا توفير تطبيق افتراضي باستخدام أقفال في الذاكرة.

هناك مشكلة أخرى بالطبع وهي أن مستخدمي المكتبة سيحتاجون إلى تحديث التعليمات البرمجية الخاصة بهم لاستخدام Lock and Release ، لكن أعتقد أنه بدون ذلك سيكون لديهم نفس السلوك كما يفعلون الآن.

"يمكننا توفير تطبيق افتراضي باستخدام أقفال في الذاكرة"

سيكون هذا بلا معنى في بيئة متعددة الخوادم حيث يتم موازنة الطلبات.

أحب فكرة توفير واجهة مختلفة تمامًا للقفل لأنها تتيح لك مزيدًا من الحرية في تنفيذها بشكل صحيح دون تغيير سلوك الكود الحالي الذي يستخدم الجلسات.

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

لا تزال أقفال الذاكرة منطقية في بعض المتاجر ، وأعتقد أن FilesystemStore هو مرشح جيد لهم. في الواقع ، لن تستخدم مخزن نظام الملفات في حالة تحميل متوازن (على الرغم من أنه ممكن نظريًا مع أنظمة ملفات الشبكة). يبدو أن أقفال نظام الملفات مفضلة ، ولكن يبدو أنه من الصعب تنفيذ الأنظمة الأساسية المشتركة ، علاوة على ذلك ، فهي ليست مضمونة للعمل مع NFS أيضًا.

boj : موافق ، ولكن كما يشير cless ، فإن هذا يعتمد على نوع المتجر الذي لديك. يعتمد FilesystemStore بالفعل على RWMutex لمنع التعديلات المتزامنة على مخزن الجلسة. أتوقع أن معظم الخلفيات الخلفية ستنفذ بالفعل واجهة القفل على الأقل في نهاية المطاف ، ولكن وجود افتراضي سيسمح للمطور بالإفراج عن بيئة خادم واحد مناسبة في هذه الأثناء. سيكون FilesystemStore واحدًا من هؤلاء حتى يتحسن وضع القفل عبر الأنظمة الأساسية في مكتبات Go.

أتفق تمامًا مع cless وأريد التأكيد على أهمية قفل الجلسات وتنفيذه المنصات الناضجة (مثل PHP) في متاجر kisielk للحصول على واجهة جديدة (وجود وظيفتي "Lock" و "Release"

  • spinLockWait: مللي ثانية للنوم بين محاولات الحصول على القفل (الافتراضي: 150)
  • lockMaxWait: ثوان لانتظار القفل (الافتراضي: 30)
  • lockMaxAge: ثوان قد يتم الاحتفاظ بالقفل قبل تحريره تلقائيًا (الافتراضي: lockMaxWait)

تم وضع علامة على هذه المشكلة تلقائيًا على أنها قديمة لأنها لم تشهد تحديثًا مؤخرًا. سيتم إغلاقه تلقائيًا في غضون أيام قليلة.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات