Skip to content

Commit 2b6620b

Browse files
committed
Fix memory leak related to call_later() and call_at()
The crux of the problem is that TimerHandle did not clean up a strong reference from the event loop to `self`. This typically isn't a problem unless there's another strong reference to the loop from the callback or from its arguments (such as a Future). A few new unit tests should ensure this kind of bugs won't happen again. Fixes: #239.
1 parent 73529c5 commit 2b6620b

File tree

3 files changed

+75
-19
lines changed

3 files changed

+75
-19
lines changed

tests/test_base.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_handle_weakref(self):
3636
h = self.loop.call_soon(lambda: None)
3737
wd['h'] = h # Would fail without __weakref__ slot.
3838

39-
def test_call_soon(self):
39+
def test_call_soon_1(self):
4040
calls = []
4141

4242
def cb(inc):
@@ -56,6 +56,22 @@ def cb(inc):
5656

5757
self.assertEqual(calls, [10, 1])
5858

59+
def test_call_soon_2(self):
60+
waiter = self.loop.create_future()
61+
waiter_r = weakref.ref(waiter)
62+
self.loop.call_soon(lambda f: f.set_result(None), waiter)
63+
self.loop.run_until_complete(waiter)
64+
del waiter
65+
self.assertIsNone(waiter_r())
66+
67+
def test_call_soon_3(self):
68+
waiter = self.loop.create_future()
69+
waiter_r = weakref.ref(waiter)
70+
self.loop.call_soon(lambda f=waiter: f.set_result(None))
71+
self.loop.run_until_complete(waiter)
72+
del waiter
73+
self.assertIsNone(waiter_r())
74+
5975
def test_call_soon_base_exc(self):
6076
def cb():
6177
raise KeyboardInterrupt()
@@ -160,6 +176,24 @@ async def main():
160176
delta = time.monotonic() - started
161177
self.assertGreater(delta, 0.019)
162178

179+
def test_call_later_3(self):
180+
# a memory leak regression test
181+
waiter = self.loop.create_future()
182+
waiter_r = weakref.ref(waiter)
183+
self.loop.call_later(0.01, lambda f: f.set_result(None), waiter)
184+
self.loop.run_until_complete(waiter)
185+
del waiter
186+
self.assertIsNone(waiter_r())
187+
188+
def test_call_later_4(self):
189+
# a memory leak regression test
190+
waiter = self.loop.create_future()
191+
waiter_r = weakref.ref(waiter)
192+
self.loop.call_later(0.01, lambda f=waiter: f.set_result(None))
193+
self.loop.run_until_complete(waiter)
194+
del waiter
195+
self.assertIsNone(waiter_r())
196+
163197
def test_call_later_negative(self):
164198
calls = []
165199

uvloop/cbhandles.pxd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ cdef class Handle:
2424

2525
cdef class TimerHandle:
2626
cdef:
27-
object callback, args
27+
object callback
28+
tuple args
2829
bint _cancelled
2930
UVTimer timer
3031
Loop loop
3132
object context
33+
tuple _debug_info
3234
object __weakref__
3335

34-
readonly _source_traceback
35-
3636
cdef _run(self)
3737
cdef _cancel(self)
3838
cdef inline _clear(self)

uvloop/cbhandles.pyx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,12 @@ cdef class TimerHandle:
194194
self.context = None
195195

196196
if loop._debug:
197-
self._source_traceback = extract_stack()
197+
self._debug_info = (
198+
format_callback_name(callback),
199+
extract_stack()
200+
)
201+
else:
202+
self._debug_info = None
198203

199204
self.timer = UVTimer.new(
200205
loop, <method_t>self._run, self, delay)
@@ -204,6 +209,11 @@ cdef class TimerHandle:
204209
# Only add to loop._timers when `self.timer` is successfully created
205210
loop._timers.add(self)
206211

212+
property _source_traceback:
213+
def __get__(self):
214+
if self._debug_info is not None:
215+
return self._debug_info[1]
216+
207217
def __dealloc__(self):
208218
if UVLOOP_DEBUG:
209219
self.loop._debug_cb_timer_handles_count -= 1
@@ -227,7 +237,7 @@ cdef class TimerHandle:
227237
self.loop._timers.remove(self)
228238
finally:
229239
self.timer._close()
230-
self.timer = None # let it die asap
240+
self.timer = None # let the UVTimer handle GC
231241

232242
cdef _run(self):
233243
if self._cancelled == 1:
@@ -260,8 +270,8 @@ cdef class TimerHandle:
260270
'handle': self,
261271
}
262272

263-
if self._source_traceback is not None:
264-
context['source_traceback'] = self._source_traceback
273+
if self._debug_info is not None:
274+
context['source_traceback'] = self._debug_info[1]
265275

266276
self.loop.call_exception_handler(context)
267277
else:
@@ -276,6 +286,7 @@ cdef class TimerHandle:
276286
Py_DECREF(self)
277287
if PY37:
278288
Context_Exit(context)
289+
self._clear()
279290

280291
# Public API
281292

@@ -285,19 +296,20 @@ cdef class TimerHandle:
285296
if self._cancelled:
286297
info.append('cancelled')
287298

288-
if self.callback is not None:
289-
func = self.callback
290-
if hasattr(func, '__qualname__'):
291-
cb_name = getattr(func, '__qualname__')
292-
elif hasattr(func, '__name__'):
293-
cb_name = getattr(func, '__name__')
294-
else:
295-
cb_name = repr(func)
299+
if self._debug_info is not None:
300+
callback_name = self._debug_info[0]
301+
source_traceback = self._debug_info[1]
302+
else:
303+
callback_name = None
304+
source_traceback = None
296305

297-
info.append(cb_name)
306+
if callback_name is not None:
307+
info.append(callback_name)
308+
elif self.callback is not None:
309+
info.append(format_callback_name(self.callback))
298310

299-
if self._source_traceback is not None:
300-
frame = self._source_traceback[-1]
311+
if source_traceback is not None:
312+
frame = source_traceback[-1]
301313
info.append('created at {}:{}'.format(frame[0], frame[1]))
302314

303315
return '<' + ' '.join(info) + '>'
@@ -309,6 +321,16 @@ cdef class TimerHandle:
309321
self._cancel()
310322

311323

324+
cdef format_callback_name(func):
325+
if hasattr(func, '__qualname__'):
326+
cb_name = getattr(func, '__qualname__')
327+
elif hasattr(func, '__name__'):
328+
cb_name = getattr(func, '__name__')
329+
else:
330+
cb_name = repr(func)
331+
return cb_name
332+
333+
312334
cdef new_Handle(Loop loop, object callback, object args, object context):
313335
cdef Handle handle
314336
handle = Handle.__new__(Handle)

0 commit comments

Comments
 (0)