diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index f650346b02c..6af847caa09 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals import functools import re +import threading from importlib import import_module from django.conf import settings @@ -164,7 +165,7 @@ class RegexURLResolver(LocaleRegexProvider): # urlpatterns self._callback_strs = set() self._populated = False - self._populating = False + self._local = threading.local() def __repr__(self): if isinstance(self.urlconf_name, list) and len(self.urlconf_name): @@ -178,9 +179,13 @@ class RegexURLResolver(LocaleRegexProvider): ) def _populate(self): - if self._populating: + # Short-circuit if called recursively in this thread to prevent + # infinite recursion. Concurrent threads may call this at the same + # time and will need to continue, so set 'populating' on a + # thread-local variable. + if getattr(self._local, 'populating', False): return - self._populating = True + self._local.populating = True lookups = MultiValueDict() namespaces = {} apps = {} @@ -213,7 +218,7 @@ class RegexURLResolver(LocaleRegexProvider): namespaces[namespace] = (p_pattern + prefix, sub_pattern) for app_name, namespace_list in pattern.app_dict.items(): apps.setdefault(app_name, []).extend(namespace_list) - if not pattern._populating: + if not getattr(pattern._local, 'populating', False): pattern._populate() self._callback_strs.update(pattern._callback_strs) else: @@ -225,7 +230,7 @@ class RegexURLResolver(LocaleRegexProvider): self._namespace_dict[language_code] = namespaces self._app_dict[language_code] = apps self._populated = True - self._populating = False + self._local.populating = False @property def reverse_dict(self): diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 0201e28bf89..67a4370e78e 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -5,6 +5,7 @@ Unit tests for reverse URL lookups. from __future__ import unicode_literals import sys +import threading from admin_scripts.tests import AdminScriptTestCase @@ -429,6 +430,18 @@ class ResolverTests(SimpleTestCase): self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.View3')) self.assertFalse(resolver._is_callback('urlpatterns_reverse.nested_urls.blub')) + def test_populate_concurrency(self): + """ + RegexURLResolver._populate() can be called concurrently, but not more + than once per thread (#26888). + """ + resolver = RegexURLResolver(r'^/', 'urlpatterns_reverse.urls') + resolver._local.populating = True + thread = threading.Thread(target=resolver._populate) + thread.start() + thread.join() + self.assertNotEqual(resolver._reverse_dict, {}) + @override_settings(ROOT_URLCONF='urlpatterns_reverse.reverse_lazy_urls') class ReverseLazyTest(TestCase):