summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarten Kenbeek <marten.knbk@gmail.com>2016-07-14 20:41:52 +0200
committerTim Graham <timograham@gmail.com>2016-07-14 14:41:52 -0400
commit389a5318a06e7e4d8f8aba14af88c4cc4ea0db47 (patch)
treef9ffdc02fc1571b04d8a395f38fe96eb6fa55b5b
parent3e71f6544feca490211e88db4f449dfdb7acce39 (diff)
Fixed #26888 -- Fixed concurrency issue in URL resolver.
Fixed a regression in 625b8e9295d79650208bfb3fca8bf9e6aaf578e4: improper short-circuiting could lead to a KeyError when threads concurrently call RegexURLResolver._populate().
-rw-r--r--django/urls/resolvers.py15
-rw-r--r--tests/urlpatterns_reverse/tests.py13
2 files changed, 23 insertions, 5 deletions
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index f650346b02..6af847caa0 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 0201e28bf8..67a4370e78 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):