| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
There's no tests for case which solved by #94504, so I'm somewhat unsure if this is the right solution However, refleaks in test_concurrency and PyUnpicklerTests (which related to issue with refleaks in test_pickle on Windows) are gone. |
Sorry, something went wrong.
|
I think we are almost here! sys.modules["_frozen_importlib"]._blocking_on�is the root of the leak. |
Sorry, something went wrong.
|
As for the fix, we usually clean up things like these within the test case and leave the cache as is. |
Sorry, something went wrong.
|
🤖 New build scheduled with the buildbot fleet by @sunmy2019 for commit 5328e32 🤖 If you want to schedule another build, you need to add the 🔨 test-with-refleak-buildbots label again. |
Sorry, something went wrong.
Yeah, you're right. The refleaks is happen in _blocking_on or somewhere nearby, I tried playing around with this, and seems that is the most correct solution. |
Sorry, something went wrong.
|
Confirmed fixed on Windows |
Sorry, something went wrong.
|
How about this? diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py
index fd46819fd90..c5f301c2d18 100644
--- a/Lib/test/libregrtest/utils.py
+++ b/Lib/test/libregrtest/utils.py
@@ -125,6 +125,15 @@ def clear_caches():
if stream is not None:
stream.flush()
+ try:
+ _frozen_importlib = sys.modules['_frozen_importlib']
+ except KeyError:
+ pass
+ else:
+ _frozen_importlib._blocking_on = {
+ k: v for k, v in _frozen_importlib._blocking_on.items() if v
+ }
+
try:
re = sys.modules['re']
except KeyError: |
Sorry, something went wrong.
I would prefer to not leak so much implementation details in libregrtest. If importlib cannot clean that automatically and it must be cleared, maybe add a private function for that. But i don't see why all tests must not clear that dict if only a test is affected. For me it would make sense to clear that dict in test_import and test_importlib, in a tearDownModule() function or something like that. |
Sorry, something went wrong.
This leak also present in test_pickle see #104702 for more details. Should we add a tearDownModule in test_pickle? |
Sorry, something went wrong.
I don't understand how a pickle tests leaks a deep internal importlib "blocking" object? Why is this "blocking" object created? Why is not deleted automatically? The Refleaks test checks that each test iteration either creates no new objects or deletes all objects that it created. |
Sorry, something went wrong.
Let me try to explain. Previously, structure of _blocking_on looks like that: thread_id: module_lock_instance. After changes, it took this form: thread_id: list_of_module_lock_instances. Why test_pickle leaks? In test_pickle we have similiar test to test_concurrency in test_import - pickletester/test_unpickle_module_race. _blocking_on not cleared between iteration tests. It's just growing up. (if we don't do things like in this PR) Why is so? Well... I don't know. Probably, _blocking_on is cleared when interpreter finalizes. (Does interpreter really finalizes in each iteration of refleak tests?) At the current moment of research, I think solution which presented in this PR is the only true. |
Sorry, something went wrong.
I guess that by "dying", you just mean that the thread exits cleanly. Can this "per-thread" dictionary becomes a thread local storage, like threading.local(), so it's automatically cleared when the thread exits? You may use _thread._local(). Or can you register a function with threading._register_atexit() and clear _blocks_on[thread_id] at a thread exit? A practical problem is that the threading.py module is not imported yet when _bootstrap is run :-( For me, this memory leak is a real issue. If an application spawns many threads, each thread creates an item in _blocks_on and the thread identifier is not recycled, this dictionary will only grow and will never be cleared: so the memory usage only increases, whereas the threads are no longer running (exited). Prototype using thread local: import threading
import _thread
import tracemalloc
NTHREAD = 10
def big_alloc():
return bytearray(1024 * 50)
if 1:
def mylock():
thread_locals = _thread._local()
thread_locals.blocks_on = big_alloc()
else:
_blocks_on = {}
def mylock():
_blocks_on[_thread.get_ident()] = big_alloc()
def test():
threads = [threading.Thread(target=mylock, args=()) for _ in range(NTHREAD)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
threads = None
# warmup to have a more accurate memory usage measurement
test()
tracemalloc.start()
before = tracemalloc.get_traced_memory()[0]
test()
after = tracemalloc.get_traced_memory()[0]
usage = after - before
print(f"usage: {usage} bytes")It shows less than 1 kB of memory usage: no leak. If you replace if 1: with if 0:, it leaks 50 kB: 10 kB per thread. |
Sorry, something went wrong.
|
I don't have a specific opinion on this, but do note you do have to make sure _thread can be used by importlib at start-up and it will work appropriately for platforms w/o fully-functioning threading (e.g. WebAssembly).
It should mean the thread exited while in the middle of an import, else there's a logic error causing keys to get left behind in the dict even when all imports resolve. |
Sorry, something went wrong.
_BlockingOnManager.__exit__() removes the lock from _blocking_on[thread_id], but it doesn't remove _blocking_on[thread_id] item in the dictionary. Maybe it should remove it, if the list becomes empty after the removal? |
Sorry, something went wrong.
That SGTM! |
Sorry, something went wrong.
It's just what this PR has done. |
Sorry, something went wrong.
| """Remove self.lock from this thread's _blocking_on list.""" | ||
| self.blocked_on.remove(self.lock) | ||
| if not self.blocked_on: | ||
| del _blocking_on[self.thread_id] |
There was a problem hiding this comment.
Just have one question, is this always true?
assert self.blocked_on is _blocking_on[self.thread_id]
Sorry, something went wrong.
There was a problem hiding this comment.
I think, if it were sometimes is not true, self.blocked_on.remove(...) will fail, no?
Though, currently test suite didn't failed with new assert check.
Sorry, something went wrong.
There was a problem hiding this comment.
It may not be True under certain circumstances.
Sorry, something went wrong.
|
Actually, I think weakref delivers exactly what we need here. But is weakref available here? |
Sorry, something went wrong.
Sorry, something went wrong.
|
ISTM, that this problem has been tried before in a similar way, but then got reverted: |
Sorry, something went wrong.
|
Oh. That problem which looks simple seems quite complicated to be fixed. |
Sorry, something went wrong.
Unfortunately, seems no. |
Sorry, something went wrong.
|
As Guido says, __del__ (in _BlockingOnManager, just del _blocking_on[self.thread_id]) actually solve the refleak issue. But is it really solves the problem which #94504 is trying to solve? |
Sorry, something went wrong.
No. Either deleting the list inside __exit__ or __del__ can be interrupted by GC (triggered by interpreter core). imports can happen during GC, causing the issue that #94504 is trying to solve. |
Sorry, something went wrong.
The key point is that _weakref provides an atomic removal function which won't be interrupted by GC. def remove(wr, selfref=ref(self), _atomic_removal=_remove_dead_weakref):
self = selfref()
if self is not None:
if self._iterating:
self._pending_removals.append(wr.key)
else:
# Atomic removal is necessary since this function
# can be called asynchronously by the GC
_atomic_removal(self.data, wr.key)static PyObject *
_weakref__remove_dead_weakref_impl(PyObject *module, PyObject *dct,
PyObject *key)
/*[clinic end generated code: output=d9ff53061fcb875c input=19fc91f257f96a1d]*/
{
if (_PyDict_DelItemIf(dct, key, is_dead_weakref) < 0) {
if (PyErr_ExceptionMatches(PyExc_KeyError))
/* This function is meant to allow safe weak-value dicts
with GC in another thread (see issue #28427), so it's
ok if the key doesn't exist anymore.
*/
PyErr_Clear();
else
return NULL;
}
Py_RETURN_NONE;
}We can provide similar things inside some C module. |
Sorry, something went wrong.
importlib._bootstrap can use _weakref._remove_dead_weakref(). |
Sorry, something went wrong.
You have to be sure to do it thread-safely and re-entrant-safely or the original bug is re-introduced. |
Sorry, something went wrong.
I was able to implement a WeakValueDictionary in _bootstrap.py. It's seems weird, but it works. Is there a less painful way, without an implementation of an entire (in fact, a half) WeakValueDictionary? |
Sorry, something went wrong.
If you're asking if you can use it at all, then the answer is "yes, you can use it" since _weakref is a built-in module and since the stdlib itself can use private APIs. |
Sorry, something went wrong.
|
Another option is if people can come up with a different solution to the import deadlock/race condition problem. It's obviously rather tricky (thanks, threads), but maybe someone else has another way to approach the problem? |
Sorry, something went wrong.
|
We can also revert the fix if this continues to block 3.12.0rc1 and rethink how to tackle the threaded import issue. |
Sorry, something went wrong.
The bug fixed caused real Python programs to crash in multiple environments in mysterious ways due to unpredictable interactions between the import system and third-party modules. The problem introduced by the fix seems to be a leak of one dictionary entry per thread that performs an import and then exits. Some Python programs might create hundreds of thousands or millions of unique threads over their lifetime and might see noticable memory usage as a result - but at least they won't crash when one of those threads gets unlucky and enters the importlib machinery at just the wrong time. |
Sorry, something went wrong.
I'm aware it fixed actual issues (I did the code review and merge of your PR), but the issue isn't something a large portion of people run into. And considering it wasn't (potentially) fixed until Python 3.12, I would argue it wasn't crippling either.
And the release manager for Python 3.12 is blocking the release due to this memory leak, which I consider the more critical issue. As I said, I would love if someone can come up with a fix for the refleak and keep the fix, but if it's a choice between keeping the fix or stopping the refleak, the RM has chosen the latter as the more critical thing at the moment. |
Sorry, something went wrong.
This is news to me. Who is the release manager and where can I read about their concerns? |
Sorry, something went wrong.
It's in the issue attached to this PR (you actually commented on the issue about how you didn't have time, so maybe you unsubscribed?). |
Sorry, something went wrong.
|
Is it possible to temporarily disable gc when deleting this empty list? |
Sorry, something went wrong.
How does that help with the memory staying around? The key issue is making sure appropriately cleanup occurs regardless of what eventually happens to the thread as there end up being dangling objects. |
Sorry, something went wrong.
|
Eeh, I would like to clarify that I have not taken a stance on whether the original bug or the leak is more important. What does matter is that test_import currently leaks references, and that is a blocker. The leak in test_import can be fixed in different ways. I also don't think accumulating a new, effectively immortal object per thread is a good thing to do, but that in itself isn't blocking 3.12rc1. |
Sorry, something went wrong.
|
#108497 has been merged, so let's close this. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.