-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Memory leak when asyncio.open_connection raise #88863
Comments
I write some short example. import resource
import asyncio
class B:
def __init__(self, loop):
self.loop = loop
self.some_big_data = bytearray(1024 * 1024) # 1Mb for memory bloating
async def doStuff(self):
if not await self.connect():
return
print('Stuff done')
async def connect(self) -> bool:
try:
_, writer = await asyncio.open_connection('127.0.0.1', 12345, loop=self.loop)
writer.close()
return True
except OSError as e:
pass
return False
class A:
def __init__(self, loop):
self.loop = loop
async def doBStuff(self):
b = B(self.loop)
await b.doStuff()
async def work(self):
print('Working...')
for _ in range(1000):
await self.loop.create_task(self.doBStuff())
print('Done.')
print(
'Memory usage {}kb'.format(
resource.getrusage(
resource.RUSAGE_SELF).ru_maxrss))
async def amain(loop):
a = A(loop)
await a.work()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(amain(loop)) 100 cycles 1000 cycles 10000 cycles And so on... Does anyone know workaround? |
Confirmed. I have the same problem. I suspect this is related to https://bugs.python.org/issue41699. |
Checked on 3.9.6 - still leaking. Strange stuff, but if I write except OSError as e:
del self instead of except OSError as e:
pass leak is disappearing. |
Thank you Arteem, that should help indicate where the memory leak is present. |
This seems more like a gc issue as adding import resource
import asyncio
import gc
class B:
def __init__(self, loop):
self.loop = loop
self.some_big_data = bytearray(1024 * 1024) # 1Mb for memory bloating
async def doStuff(self):
if not await self.connect():
return
print('Stuff done')
async def connect(self) -> bool:
try:
_, writer = await asyncio.open_connection('127.0.0.1', 12345)
writer.close()
return True
except OSError as e:
pass
return False
class A:
def __init__(self, loop):
self.loop = loop
async def doBStuff(self):
b = B(self.loop)
await b.doStuff()
async def work(self):
print('Working...')
for i in range(1000):
await self.loop.create_task(self.doBStuff())
gc.collect()
if i % 100 == 0:
print('Memory usage {}kb {}'.format(resource.getrusage(
resource.RUSAGE_SELF).ru_maxrss, i))
print('Memory usage {}kb '.format(resource.getrusage(
resource.RUSAGE_SELF).ru_maxrss, ))
print('Done.')
async def amain(loop):
a = A(loop)
await a.work()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(amain(loop)) cc @pablogsal |
That doesn't mean it is a GC issue. That means the GC is doing its work and the problem is that there are reference cycles to clean that until the GC runs they won't go away on their own. It may be something to help here, but the GC is working as expected from this description. |
This might be related to #81001 |
After some investigation, the issue is that the raised exception's traceback forms ref cycles are eventually cleared by gc but causes increased memory usage. The following example does not form ref cycles and hence memory usage is constant. import resource
import asyncio
class B:
def __init__(self, loop):
self.loop = loop
self.some_big_data = bytearray(1024 * 1024) # 1Mb for memory bloating
async def doStuff(self):
if not await self.connect():
return
print('Stuff done')
async def connect(self) -> bool:
try:
_, writer = await asyncio.open_connection('127.0.0.1', 12345)
writer.close()
return True
except OSError as e:
e.__traceback__ = None # Clear ref cycle manually
return False
class A:
def __init__(self, loop):
self.loop = loop
async def doBStuff(self):
b = B(self.loop)
await b.doStuff()
async def work(self):
print('Working...')
for i in range(1000):
await self.loop.create_task(self.doBStuff())
if i % 100 == 0:
print('Memory usage {}kb {}'.format(resource.getrusage(
resource.RUSAGE_SELF).ru_maxrss, i))
print('Memory usage {}kb '.format(resource.getrusage(
resource.RUSAGE_SELF).ru_maxrss, ))
print('Done.')
async def amain(loop):
a = A(loop)
await a.work()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(amain(loop)) |
As kumaraditya303 said, it seems like there are some local variables (exception and future instances) which makes reference cycles in asyncio library. After the fix numbers from get_traced_memory does not grow linearly with the loop count anymore.
Checked in Windows 10/Mac Monterey/Centos 7.9 |
…on raises (#95739) Break reference cycles to resolve memory leak, by removing local exception and future instances from the frame
…open_connection raises (pythonGH-95739) Break reference cycles to resolve memory leak, by removing local exception and future instances from the frame. (cherry picked from commit 995f617) Co-authored-by: Dong Uk, Kang <nailbrainz@gmail.com>
…open_connection raises (pythonGH-95739) Break reference cycles to resolve memory leak, by removing local exception and future instances from the frame. (cherry picked from commit 995f617) Co-authored-by: Dong Uk, Kang <nailbrainz@gmail.com>
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs
The text was updated successfully, but these errors were encountered: