Pytest-django: تنظيف الاتصال الوحشي في تفكيك django_db_setup

تم إنشاؤها على ٢١ نوفمبر ٢٠١٦  ·  11تعليقات  ·  مصدر: pytest-dev/pytest-django

لدينا عدد قليل من المشاريع التي تعاني من تعليق الاتصالات بقاعدة البيانات ، ويمكن أن تكون من عمليات أو خيوط خارجية تحمل اتصالاً بقاعدة البيانات ، وتتسبب في فشل الاختبارات من وقت لآخر.

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

هل نحن وحدنا نواجه هذه المشاكل؟ لا يعد وجود هذا الامتداد المحدد لـ django_db_setup مشكلة كبيرة ، ولكن إذا كان هناك الكثير من الأشخاص يقومون بعمليات اختراق مماثلة ، فيجب أخذ ذلك في الاعتبار :)

@pytest.yield_fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker):
    """
    Fixture that will clean up remaining connections, that might be hanging
    from threads or external processes. Extending pytest_django.django_db_setup
    """

    yield

    with django_db_blocker.unblock():
        from django.db import connections

        conn = connections['default']
        cursor = conn.cursor()
        cursor.execute("""SELECT * FROM pg_stat_activity;""")
        print('current connections')
        for r in cursor.fetchall():
            print(r)

        terminate_sql = """
            SELECT pg_terminate_backend(pg_stat_activity.pid)
            FROM pg_stat_activity
            WHERE pg_stat_activity.datname = '%s'
                AND pid <> pg_backend_pid();
        """ % conn.settings_dict['NAME']
        print('Terminate SQL: ', terminate_sql)
        cursor.execute(terminate_sql)

التعليق الأكثر فائدة

للحصول على معلومات:

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

عندما يتم تنفيذ المباراة pytest_django db للمرة الأولى بعد الانتهاء من المباراة django_db_setup (بمعنى آخر ، استعدادًا للاختبار الأول) ، و test_case._pre_setup() تم تنفيذ طريقة

# django.test.testcases.TestCase

    <strong i="12">@classmethod</strong>
    def _enter_atomics(cls):
        """Open atomic blocks for multiple databases."""
        atomics = {}
        for db_name in cls._databases_names():
            atomics[db_name] = transaction.atomic(using=db_name)
            atomics[db_name].__enter__()
        return atomics

عندما يتم استدعاء transaction.Atomic.__enter__ فإنه يحصل على الاتصال بقاعدة البيانات ويتحقق من حالة المعاملة الحالية:

    def __enter__(self):
        connection = get_connection(self.using)

        if not connection.in_atomic_block:
            # Reset state when entering an outermost atomic block.
            connection.commit_on_exit = True
            connection.needs_rollback = False
            if not connection.get_autocommit():

هنا تسوء الأمور. أثناء اجتياز الاختبار بنجاح ، يكون الاتصال في تلك المرحلة في كتلة ذرية ، ولذا فإننا لا ندخل كتلة الكود هذه. عندما يفشل الاختبار ، لا يكون الاتصال في كتلة ذرية ، وينتهي بنا الأمر باستدعاء connection.get_autocommit() . أحد آثار ذلك هو استدعاء connection.ensure_connection() ، والذي له تأثير في هذه الحالة على إنشاء اتصال جديد تمامًا بقاعدة البيانات.

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

عند اكتمال تنفيذ الاختبار ، يغلق Django اتصال قاعدة البيانات ، ولكن نظرًا لوجود اتصالين مفتوحين ، يتم ترك الآخر مفتوحًا. عندما يتم استدعاء DROP DATABASE من _nodb_connection ، فإن هذه الجلسة المهجورة تمنع العملية من النجاح ، ونحصل على OperationalError المشار إليه هنا.

لست واضحًا ما إذا كانت هذه مشكلة يمكن حلها بشكل نظيف. يمكنني استخدام القليل من المساعدة في محاولة معرفة ذلك.

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

ال 11 كومينتر

إنها المرة الأولى التي أسمع فيها عن هذا المطلب.

إنه يعمل بشكل جيد كما لدينا الآن. أعرف المزيد من المشاريع التي لديها نفس المشكلة ، وسوف تجد بعض المراجع لها. على سبيل المثال عمال الكرفس الذين لديهم اتصال بالديسيبل. تضمين التغريدة

هذا مثال على الاستثناءات التي نحصل عليها إذا لم تقم بالقوة الغاشمة بإنهاء الاتصالات التي قد تكون نشطة ...

==================================== ERRORS ====================================
_______ ERROR at teardown of test_subscriber_subdomain_valid_prefix[a-b] _______
[gw0] linux2 -- Python 2.7.9 /home/travis/build/dolphinkiss/tailor-experience/.tox/pytest-travis-non-functional/bin/python2.7
def teardown_database():
        with _django_cursor_wrapper:
>           teardown_databases(db_cfg)
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/pytest_django/fixtures.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/django/test/runner.py:509: in teardown_databases
    connection.creation.destroy_test_db(old_name, self.verbosity, self.keepdb)
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/django/db/backends/base/creation.py:264: in destroy_test_db
    self._destroy_test_db(test_database_name, verbosity)
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/django/db/backends/base/creation.py:283: in _destroy_test_db
    % self.connection.ops.quote_name(test_database_name))
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/django/db/backends/utils.py:64: in execute
    return self.cursor.execute(sql, params)
.tox/pytest-travis-non-functional/lib/python2.7/site-packages/django/db/utils.py:95: in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <django.db.backends.utils.CursorWrapper object at 0x7f2e451cf090>
sql = 'DROP DATABASE "test_project_gw0"', params = None
    def execute(self, sql, params=None):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
>               return self.cursor.execute(sql)
E               OperationalError: database "test_project_gw0" is being accessed by other users
E               DETAIL:  There is 1 other session using the database.

@ peterlauri ، شكرًا لترك هذا المرجع هنا. لقد كنت أواجه هذه المشكلة وأحاول تشخيصها في قاعدة بيانات عملي خلال اليومين الماضيين.

القضية متقطعة. فشل مع نفس الخطأ التشغيلي الذي رأيته أعلاه في حوالي 40-60٪ من عمليات التشغيل التجريبية. في أوقات أخرى يعمل بشكل جيد. من الحفر وطباعة الإدخالات في pg_stat_activity لقاعدة بيانات الاختبار الخاصة بي ، أستطيع أن أرى أن استعلام sql المخالف يبدو أنه

SELECT "django_migrations"."app", "django_migrations"."name" FROM "django_migrations"

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

هل أحرزت أي تقدم مع هذا؟

(ملاحظة: أنا أستخدم Django 2.0.3 و Python 3)

للحصول على معلومات:

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

عندما يتم تنفيذ المباراة pytest_django db للمرة الأولى بعد الانتهاء من المباراة django_db_setup (بمعنى آخر ، استعدادًا للاختبار الأول) ، و test_case._pre_setup() تم تنفيذ طريقة

# django.test.testcases.TestCase

    <strong i="12">@classmethod</strong>
    def _enter_atomics(cls):
        """Open atomic blocks for multiple databases."""
        atomics = {}
        for db_name in cls._databases_names():
            atomics[db_name] = transaction.atomic(using=db_name)
            atomics[db_name].__enter__()
        return atomics

عندما يتم استدعاء transaction.Atomic.__enter__ فإنه يحصل على الاتصال بقاعدة البيانات ويتحقق من حالة المعاملة الحالية:

    def __enter__(self):
        connection = get_connection(self.using)

        if not connection.in_atomic_block:
            # Reset state when entering an outermost atomic block.
            connection.commit_on_exit = True
            connection.needs_rollback = False
            if not connection.get_autocommit():

هنا تسوء الأمور. أثناء اجتياز الاختبار بنجاح ، يكون الاتصال في تلك المرحلة في كتلة ذرية ، ولذا فإننا لا ندخل كتلة الكود هذه. عندما يفشل الاختبار ، لا يكون الاتصال في كتلة ذرية ، وينتهي بنا الأمر باستدعاء connection.get_autocommit() . أحد آثار ذلك هو استدعاء connection.ensure_connection() ، والذي له تأثير في هذه الحالة على إنشاء اتصال جديد تمامًا بقاعدة البيانات.

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

عند اكتمال تنفيذ الاختبار ، يغلق Django اتصال قاعدة البيانات ، ولكن نظرًا لوجود اتصالين مفتوحين ، يتم ترك الآخر مفتوحًا. عندما يتم استدعاء DROP DATABASE من _nodb_connection ، فإن هذه الجلسة المهجورة تمنع العملية من النجاح ، ونحصل على OperationalError المشار إليه هنا.

لست واضحًا ما إذا كانت هذه مشكلة يمكن حلها بشكل نظيف. يمكنني استخدام القليل من المساعدة في محاولة معرفة ذلك.

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

blueyed ، pelme ، وضع علامة باسمكما هنا للفت الانتباه إلى هذه المشكلة. آمل في الحصول على بعض التعليقات إذا كان لديك أي أفكار حول الحلول الذكية.

للتوضيح فقط: هل هذا مع 3.1.2؟ هل جربت سيد؟ (سيصدر قريبا باسم 3.2.0)

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

blueyed : يمكنني التحقق من حدوث نفس المشكلات على المستوى الرئيسي كما في 3.1.2. العمل على السبب الجذري.

تضمين التغريدة

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

عندما تستدعي الأداة django_db_setup setup_databases لتشغيل قاعدة بيانات الاختبار ، فإنها تستدعي تطبيق Django الخاص بـ create_test_db ، والذي يستخدم django.test.utils.get_unique_databases_and_mirrors لمعرفة قواعد البيانات التي يجب إنشاؤها . إذا كانت هناك إدخالات قاعدة بيانات أساسية / متماثلة في DATABASES ، فقد يكون الاتصال المستخدم في البداية لإنشاء قاعدة البيانات وتشغيل عمليات الترحيل هو الاتصال الأساسي أو قد يكون هو النسخة المتماثلة.

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

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

شكرا على الصبر.

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

    def teardown_database():
        with django_db_blocker.unblock():
            # close unused connections
            for connection, old_name, destroy in db_cfg:
                if not destroy:
                    connection.close()
            # delete test databases
            for connection, old_name, destroy in db_cfg:
                if not destroy:
                    continue
                connection.creation.destroy_test_db(
                    old_name, request.config.option.verbose, False
                )
                connection.close()

كانت المشكلة أن الاتصالات لم تكن مغلقة بشكل صحيح في الوظيفة الرئيسية.

السياق الكامل:

@pytest.fixture(scope="session")
def django_db_setup(request, django_test_environment, django_db_blocker):
    """Top level fixture to ensure test databases are available"""
    from pytest_django.compat import setup_databases

    with django_db_blocker.unblock():
        db_cfg = setup_databases(
            verbosity=request.config.option.verbose,
            interactive=False,
        )

    def teardown_database():
        with django_db_blocker.unblock():
            # close unused connections
            for connection, old_name, destroy in db_cfg:
                if not destroy:
                    connection.close()
            # delete test databases
            for connection, old_name, destroy in db_cfg:
                if not destroy:
                    continue
                connection.creation.destroy_test_db(
                    old_name, request.config.option.verbose, False
                )
                connection.close()

    request.addfinalizer(teardown_database)
هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات