Opened 8 months ago
Last modified 8 months 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 , 8 months ago
| Attachment: | cycle.2.png added |
|---|
comment:1 by , 8 months ago
| Description: | modified (diff) |
|---|
comment:2 by , 8 months ago
Does someone working on this issue, or it's good to go for starting out for this??
comment:3 by , 8 months 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 , 8 months ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:5 by , 8 months 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 334 335 336 337 338 339 340 341 342 343 344 345 346 347 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 360 361 362 363 364 365 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 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. 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 66 67 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 , 8 months 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 , 8 months 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 , 8 months 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 , 8 months ago
| Has patch: | set |
|---|---|
| Needs tests: | set |
| Patch needs improvement: | set |

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