🌐 AI搜索 & 代理 主页
Skip to content

Conversation

@Samriddha9619
Copy link
Contributor

Trac ticket number

ticket-36701

Branch description

This PR addresses ticket #36701 regarding memory leaks caused by reference cycles in ModelState

I implemented a __del__ method on ModelState to explicitly clear self.fields_cache.

I added a regression test (tests/model_state/test_memory_leak.py) that:

  1. Creates a OneToOneField cycle.
  2. Deletes strong references.
  3. Forces a gc.collect().
  4. Asserts that the objects are successfully reclaimed (using weakref).

I also verified this locally with a reproduction script generating 50,000 cyclic objects. Without the fix, memory usage balloons. With the fix, gc.collect() successfully reclaims the objects.

Checklist

  • This PR targets the main branch.
  • The commit message is written in past tense, mentions the ticket number, and ends with a period.
  • I have checked the "Has patch" ticket flag in the Trac system.
  • I have added or updated relevant tests.
  • I have added or updated relevant docs, including release notes if applicable.
  • I have attached screenshots in both light and dark modes for any UI changes.

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 👍

Comment on lines +500 to +501
except AttributeError:
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the except?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining -- in that case these lines are reachable, so we need a test to cover them.

self.addCleanup(gc.set_debug, gc.get_debug())
gc.set_debug(gc.DEBUG_SAVEALL)

gc.collect()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the garbage_collect() helper from django.test.utils.

gc.set_debug(gc.DEBUG_SAVEALL)

gc.collect()
del gc.garbage[:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this?

Comment on lines 14 to 17
"""
Ensure OneToOneField intermediate state does not create reference
cycles during model initialization.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""
Ensure OneToOneField intermediate state does not create reference
cycles during model initialization.
"""

Thanks for the method name change. Now we can lose the docstring (we avoid "Ensure ..." preambles anyway.)


leaked = [obj for obj in gc.garbage if id(obj) == p_id]

self.assertEqual(leaked, [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this test I get this warning:

Exception ignored in GC shutdown:
ResourceWarning: gc: 7995 uncollectable objects at shutdown; use gc.set_debug(gc.DEBUG_UNCOLLECTABLE) to list them

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, thanks very much for explaining. Now we need to register this as a cleanup earlier (right after garbage_collect, with self.addCleanup) because if the assertion fails for some reason I get the warning again.

@Samriddha9619 Samriddha9619 force-pushed the ticket-36701-modelstate-leak branch from 4c184a7 to 126f444 Compare December 9, 2025 10:36
@Samriddha9619
Copy link
Contributor Author

Samriddha9619 commented Dec 9, 2025

@jacobtylerwalls I tried to write tests using garbage_collect() from django.tests.utils but I was not getting desired results so I switched to a direct test (test_model_state_del_clears_cache). This new test proves that ModelState.__del__ correctly removes the _fields_cache, which breaks the reference cycle.
(If the test is not satisfactory I can try rewriting it again with garbage_collect() as you suggested )

Also removed the try / except block and verified the tests locally

@Samriddha9619 Samriddha9619 force-pushed the ticket-36701-modelstate-leak branch from 126f444 to 78c2504 Compare December 10, 2025 20:40
@Samriddha9619
Copy link
Contributor Author

@jacobtylerwalls
For the try/except block in django/db/models/base.py
I looked into this because you asked when _fields_cache would be missing.
It turns out _fields_cache is lazy-loaded. It doesn't actually exist on a model instance until you access a related field (like a ForeignKey).
I tested this locally if I create a simple Model instance without touching any relationships, _fields_cache isn't there. If I remove this try/except block , __del__ crashes with an AttributeError for those cases. I kept it to handle that default state safely

For the del gc.garbage line in tests/model_regress/test_state.py Since I'm using gc.DEBUG_SAVEALL to catch the cycle, the garbage collector intentionally keeps the leaked objects in gc.garbage instead of deleting them.
If I don't clear that list manually here at the end, the Django test runner detects those 'saved' objects during shutdown and throws a ResourceWarning about uncollectable objects. This line just ensures the test cleans up after itself so the suite stays clean.

@github-actions
Copy link

📊 Coverage Report for Changed Files

-------------
Diff Coverage
Diff: origin/main...HEAD, staged and unstaged changes
-------------
django/db/models/base.py (60.0%): Missing lines 500-501
-------------
Total:   5 lines
Missing: 2 lines
Coverage: 60%
-------------


Note: Missing lines are warnings only. Some lines may not be covered by SQLite tests as they are database-specific.

For more information about code coverage on pull requests, see the contributing documentation.

Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates 👍

Comment on lines +500 to +501
except AttributeError:
pass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining -- in that case these lines are reachable, so we need a test to cover them.


leaked = [obj for obj in gc.garbage if id(obj) == p_id]

self.assertEqual(leaked, [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, thanks very much for explaining. Now we need to register this as a cleanup earlier (right after garbage_collect, with self.addCleanup) because if the assertion fails for some reason I get the warning again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants