Optimize get_resolver to prevent duplicate URLResolver instances

Normalize urlconf argument before caching in get_resolver to ensure only one URLResolver is created per URLconf. This reduces redundant _populate calls and improves performance for large route sets. Fixes unnecessary resource usage when get_resolver is called before and after request handling.
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 23:00:20 +00:00
parent 55b68de643
commit b01a7e13fd
3 changed files with 154 additions and 3 deletions

View file

@ -7,7 +7,7 @@ from django.utils.functional import lazy
from django.utils.translation import override
from .exceptions import NoReverseMatch, Resolver404
from .resolvers import get_ns_resolver, get_resolver
from .resolvers import _get_resolver_cached, get_ns_resolver, get_resolver
from .utils import get_callable
# SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for
@ -92,7 +92,7 @@ reverse_lazy = lazy(reverse, str)
def clear_url_caches():
get_callable.cache_clear()
get_resolver.cache_clear()
_get_resolver_cached.cache_clear()
get_ns_resolver.cache_clear()

View file

@ -63,10 +63,33 @@ class ResolverMatch:
)
@functools.lru_cache(maxsize=None)
def get_resolver(urlconf=None):
"""
Return a URLResolver for the given URLconf.
If urlconf is None, use settings.ROOT_URLCONF. This normalization happens
before the cached function call to ensure that get_resolver(None) and
get_resolver(settings.ROOT_URLCONF) return the same cached instance.
This optimization prevents multiple URLResolver instances from being
created when get_resolver is called both before (e.g., at import time with
None) and after (with explicit settings.ROOT_URLCONF) request handling.
Since URLResolver._populate() can be expensive for applications with many
routes, avoiding duplicate instances saves significant resources.
"""
if urlconf is None:
urlconf = settings.ROOT_URLCONF
return _get_resolver_cached(urlconf)
@functools.lru_cache(maxsize=None)
def _get_resolver_cached(urlconf):
"""
Internal cached function that creates URLResolver instances.
This is separated from get_resolver() to allow urlconf normalization
(None -> settings.ROOT_URLCONF) to happen before caching.
"""
return URLResolver(RegexPattern(r'^/'), urlconf)

View file

@ -0,0 +1,128 @@
"""
Test for URL resolver optimization to prevent multiple URLResolver instances.
This test verifies the fix for the issue where multiple URLResolver instances
were being created when get_resolver was called with None (before request handling)
and then with settings.ROOT_URLCONF (after request handling).
"""
from django.conf import settings
from django.test import SimpleTestCase, override_settings
from django.urls import clear_url_caches, get_resolver
@override_settings(ROOT_URLCONF='urlpatterns_reverse.named_urls')
class GetResolverOptimizationTests(SimpleTestCase):
"""
Tests to ensure get_resolver doesn't create duplicate URLResolver instances.
"""
def setUp(self):
"""Clear URL caches before each test."""
clear_url_caches()
def tearDown(self):
"""Clear URL caches after each test."""
clear_url_caches()
def test_get_resolver_with_none_uses_settings_root_urlconf(self):
"""
Test that calling get_resolver(None) returns the same instance as
get_resolver(settings.ROOT_URLCONF).
"""
# Get resolver with None (simulating early call before request handling)
resolver_none = get_resolver(None)
# Get resolver with explicit ROOT_URLCONF (simulating call after request handling)
resolver_explicit = get_resolver(settings.ROOT_URLCONF)
# They should be the same instance (optimization working)
self.assertIs(
resolver_none,
resolver_explicit,
"get_resolver(None) and get_resolver(settings.ROOT_URLCONF) should "
"return the same cached instance"
)
def test_get_resolver_caching_consistency(self):
"""
Test that multiple calls with None and explicit ROOT_URLCONF all return
the same cached instance.
"""
# Multiple calls with None
resolver1 = get_resolver(None)
resolver2 = get_resolver(None)
# Multiple calls with explicit ROOT_URLCONF
resolver3 = get_resolver(settings.ROOT_URLCONF)
resolver4 = get_resolver(settings.ROOT_URLCONF)
# Mixed calls
resolver5 = get_resolver(None)
resolver6 = get_resolver(settings.ROOT_URLCONF)
# All should be the same instance
self.assertIs(resolver1, resolver2)
self.assertIs(resolver1, resolver3)
self.assertIs(resolver1, resolver4)
self.assertIs(resolver1, resolver5)
self.assertIs(resolver1, resolver6)
def test_get_resolver_populate_called_once(self):
"""
Test that _populate is only called once even when get_resolver is called
with both None and explicit ROOT_URLCONF.
"""
# Clear cache to start fresh
clear_url_caches()
# Get resolver with None
resolver_none = get_resolver(None)
# Access reverse_dict to trigger _populate if not already populated
_ = resolver_none.reverse_dict
# Verify it's populated
self.assertTrue(resolver_none._populated)
# Get resolver with explicit ROOT_URLCONF
resolver_explicit = get_resolver(settings.ROOT_URLCONF)
# Should be the same instance, so _populate should not be called again
self.assertIs(resolver_none, resolver_explicit)
# The _populated flag should still be True
self.assertTrue(resolver_explicit._populated)
def test_get_resolver_with_different_urlconf(self):
"""
Test that get_resolver returns different instances for different urlconfs.
"""
resolver1 = get_resolver('urlpatterns_reverse.named_urls')
resolver2 = get_resolver('urlpatterns_reverse.namespace_urls')
# Different urlconfs should return different instances
self.assertIsNot(
resolver1,
resolver2,
"Different urlconfs should return different resolver instances"
)
def test_clear_url_caches_clears_internal_cache(self):
"""
Test that clear_url_caches properly clears the internal resolver cache.
"""
# Get a resolver instance
resolver1 = get_resolver(None)
# Clear caches
clear_url_caches()
# Get resolver again
resolver2 = get_resolver(None)
# Should be a different instance after cache clear
self.assertIsNot(
resolver1,
resolver2,
"After clearing caches, get_resolver should return a new instance"
)