pythonTask: fix refcount leak of non-panda Future::done

A missing Py_DECREF on the future's "done" method
caused both the bound method and the underlying
self instance of the future to be leaked when
awaiting non-panda futures (such as _asyncio.Future).

This change includes a simple new test addition
to catch this in the future.
This commit is contained in:
John C. Allwein 2024-08-06 10:51:52 -06:00
parent fc1d95ae1d
commit 21b39da65d
2 changed files with 28 additions and 3 deletions

View File

@ -795,6 +795,7 @@ do_python_task() {
<< *this << " is now polling " << PyUnicode_AsUTF8(str) << ".done()\n"; << *this << " is now polling " << PyUnicode_AsUTF8(str) << ".done()\n";
Py_DECREF(str); Py_DECREF(str);
} }
Py_DECREF(fut_done);
_fut_waiter = result; _fut_waiter = result;
return DS_cont; return DS_cont;
} }

View File

@ -1,9 +1,9 @@
from panda3d import core from panda3d import core
from asyncio.exceptions import TimeoutError, CancelledError
import pytest import pytest
import time import time
import sys import sys
from asyncio.exceptions import TimeoutError, CancelledError import weakref
class MockFuture: class MockFuture:
_asyncio_future_blocking = False _asyncio_future_blocking = False
@ -31,7 +31,6 @@ class MockFuture:
return self._result return self._result
def test_future_cancelled(): def test_future_cancelled():
fut = core.AsyncFuture() fut = core.AsyncFuture()
@ -681,3 +680,28 @@ def test_event_future_cancel2():
assert not fut2.done() assert not fut2.done()
assert not fut2.cancelled() assert not fut2.cancelled()
def test_task_manager_cleanup_non_panda_future():
future = MockFuture()
# Create a weakref so we can verify the future was cleaned up.
future_ref = weakref.ref(future)
async def coro_main():
return await future
task = core.PythonTask(coro_main(), 'coro_main')
task_mgr = core.AsyncTaskManager.get_global_ptr()
task_mgr.add(task)
# Poll the task_mgr so the PythonTask starts polling on future.done()
task_mgr.poll()
future._result = 'Done123'
future._state = 'DONE'
# Drop our reference to the future, `task` should hold the only reference
del future
# Recognize the future has completed
task_mgr.poll()
# Verify the task was completed.
assert task.result() == 'Done123'
del coro_main # this should break the last strong reference to the mock future
assert future_ref() is None, "MockFuture was not cleaned up!"