Opened 4 weeks ago
Last modified 3 weeks ago
#36662 assigned Cleanup/optimization
django.contrib.messages Storage creates circular references with Request objects
| Reported by: | Raphael Gaschignard | Owned by: | Augusto Pontes |
|---|---|---|---|
| Component: | contrib.messages | Version: | dev |
| Severity: | Normal | Keywords: | gc, garbage, weakref |
| Cc: | Raphael Gaschignard | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | yes | Patch needs improvement: | yes |
| Easy pickings: | no | UI/UX: | no |
Description (last modified by )
To be honest I don't know how much people care about this kind of issue, but I am having some (very silly) operational issues downstream of Requests getting garbage collected at awkward times.
Request objects hold onto message storages via Request._messages. The storage classes hold references back to Request. This creates a reference cycle preventing Requests from being collected as fast as they could be (and can create some awkwardness in CPython, with del calls happening on associated objects in seemingly unrelated spots)
The "simple" fix here would be to hold onto request in the storage through a weakref, and add a property to get the request object on the storage class.
Attachments (1)
Change History (12)
by , 4 weeks ago
| Attachment: | cycle.2.png added |
|---|
comment:1 by , 4 weeks ago
| Description: | modified (diff) |
|---|
comment:2 by , 3 weeks ago
Does someone working on this issue, or it's good to go for starting out for this??
comment:3 by , 3 weeks ago
Kinda complex issue, but thanks to send the image, it clarified a lot, and definitely this circular reference can cause not only garbage collector issues, but in my point of view: performance, i will analyse this module on django core, and see if i can give any suggestions, also, we need to see if writing any changes on this module, can affect not only other modules, but the unit tests
comment:4 by , 3 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:5 by , 3 weeks ago
| Keywords: | gc garbage weakref added |
|---|---|
| Triage Stage: | Unreviewed → Accepted |
Thanks, I reproduced by riffing on the test from #34865:
(quick and dirty, not suggesting to edit this test case)
-
tests/messages_tests/base.py
diff --git a/tests/messages_tests/base.py b/tests/messages_tests/base.py index ce4b2acac8..b512788ec0 100644
a b class BaseTests: 330 330 self.assertEqual(len(storage), 6) 331 331 332 332 def test_high_level(self): 333 import gc 334 335 # Schedule the restore of the garbage collection settings. 336 self.addCleanup(gc.set_debug, 0) 337 self.addCleanup(gc.enable) 338 339 # Disable automatic garbage collection to control when it's triggered, 340 # then run a full collection cycle to ensure `gc.garbage` is empty. 341 gc.disable() 342 gc.collect() 343 344 # The garbage list isn't automatically populated to avoid CPU overhead, 345 # so debugging needs to be enabled to track all unreachable items and 346 # have them stored in `gc.garbage`. 347 gc.set_debug(gc.DEBUG_SAVEALL) 348 333 349 request = self.get_request() 334 350 storage = self.storage_class(request) 335 351 request._messages = storage … … class BaseTests: 340 356 add_level_messages(storage) 341 357 self.assertEqual(len(storage), 2) 342 358 359 # Dereference. 360 request = None 361 storage = None 362 363 # Enforce garbage collection to populate `gc.garbage` for inspection. 364 gc.collect() 365 self.assertEqual(gc.garbage, []) 366 367 343 368 @override_settings(MESSAGE_LEVEL=29) 344 369 def test_settings_level(self): 345 370 request = self.get_request()
AssertionError: Lists differ: [<HttpRequest>, <QueryDict: {}>, <QueryDic[237 chars], []] != [] First list contains 11 additional elements. First extra element 0: <HttpRequest> + [] - [<HttpRequest>, - <QueryDict: {}>, - <QueryDict: {}>, - {}, - {}, - <MultiValueDict: {}>, - <SessionStorage: request=<HttpRequest>>, - [Message(level=30, message='A warning'), - Message(level=40, message='An error')], - Message(level=30, message='A warning'), - Message(level=40, message='An error'), - []]
The "simple" fix here would be to hold onto request in the storage through a weakref, and add a property to get the request object on the storage class.
I tried that:
-
django/contrib/messages/storage/base.py
diff --git a/django/contrib/messages/storage/base.py b/django/contrib/messages/storage/base.py index 5d89acfe69..9bca9e04c2 100644
a b 1 import weakref 1 2 from django.conf import settings 2 3 from django.contrib.messages import constants, utils 3 4 from django.utils.functional import SimpleLazyObject … … class BaseStorage: 55 56 """ 56 57 57 58 def __init__(self, request, *args, **kwargs): 58 self. request = request59 self._request = weakref.proxy(request) 59 60 self._queued_messages = [] 60 61 self.used = False 61 62 self.added_new = False 62 63 super().__init__(*args, **kwargs) 63 64 65 @property 66 def request(self): 67 return self._request 68 64 69 def __len__(self): 65 70 return len(self._loaded_messages) + len(self._queued_messages)
But I just got:
ERROR: test_add_lazy_translation (messages_tests.test_session.SessionTests.test_add_lazy_translation) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/jwalls/django/tests/messages_tests/base.py", line 101, in test_add_lazy_translation storage.update(response) ~~~~~~~~~~~~~~^^^^^^^^^^ File "/Users/jwalls/django/django/contrib/messages/storage/base.py", line 145, in update return self._store(messages, response) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^ File "/Users/jwalls/django/django/contrib/messages/storage/session.py", line 40, in _store self.request.session[self.session_key] = self.serialize_messages(messages) ^^^^^^^^^^^^^^^^^^^^ ReferenceError: weakly-referenced object no longer exists ---------------------------------------------------------------------- Ran 4 tests in 0.008s
follow-up: 7 comment:6 by , 3 weeks ago
Tentatively accepting on the premise that there is a non-awkward solution for this given the way this app uses middleware.
comment:7 by , 3 weeks ago
Replying to Jacob Walls:
Tentatively accepting on the premise that there is a non-awkward solution for this given the way this app uses middleware.
But its still possible to fix it?
comment:9 by , 3 weeks ago
I created a thread on the django discord server, here is the link: https://discord.gg/4VtuEf2J, the thread has the same name of the ticket here
comment:11 by , 3 weeks ago
| Has patch: | set |
|---|---|
| Needs tests: | set |
| Patch needs improvement: | set |

An example of the sort of cycle that appears (objgraph output)