Skip to content

Commit 1a5dbc2

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 3070ec8 commit 1a5dbc2

File tree

3 files changed

+74
-19
lines changed

3 files changed

+74
-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: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ cdef class TimerHandle:
192192
self.context = None
193193

194194
if loop._debug:
195-
self._source_traceback = extract_stack()
195+
self._debug_info = (
196+
format_callback_name(callback),
197+
extract_stack()
198+
)
199+
else:
200+
self._debug_info = None
196201

197202
self.timer = UVTimer.new(
198203
loop, <method_t>self._run, self, delay)
@@ -202,6 +207,11 @@ cdef class TimerHandle:
202207
# Only add to loop._timers when `self.timer` is successfully created
203208
loop._timers.add(self)
204209

210+
property _source_traceback:
211+
def __get__(self):
212+
if self._debug_info is not None:
213+
return self._debug_info[1]
214+
205215
def __dealloc__(self):
206216
if UVLOOP_DEBUG:
207217
self.loop._debug_cb_timer_handles_count -= 1
@@ -225,7 +235,7 @@ cdef class TimerHandle:
225235
self.loop._timers.remove(self)
226236
finally:
227237
self.timer._close()
228-
self.timer = None # let it die asap
238+
self.timer = None # let the UVTimer handle GC
229239

230240
cdef _run(self):
231241
if self._cancelled == 1:
@@ -256,8 +266,8 @@ cdef class TimerHandle:
256266
'handle': self,
257267
}
258268

259-
if self._source_traceback is not None:
260-
context['source_traceback'] = self._source_traceback
269+
if self._debug_info is not None:
270+
context['source_traceback'] = self._debug_info[1]
261271

262272
self.loop.call_exception_handler(context)
263273
else:
@@ -272,6 +282,7 @@ cdef class TimerHandle:
272282
Py_DECREF(self)
273283
if PY37:
274284
Context_Exit(context)
285+
self._clear()
275286

276287
# Public API
277288

@@ -281,19 +292,20 @@ cdef class TimerHandle:
281292
if self._cancelled:
282293
info.append('cancelled')
283294

284-
if self.callback is not None:
285-
func = self.callback
286-
if hasattr(func, '__qualname__'):
287-
cb_name = getattr(func, '__qualname__')
288-
elif hasattr(func, '__name__'):
289-
cb_name = getattr(func, '__name__')
290-
else:
291-
cb_name = repr(func)
295+
if self._debug_info is not None:
296+
callback_name = self._debug_info[0]
297+
source_traceback = self._debug_info[1]
298+
else:
299+
callback_name = None
300+
source_traceback = None
292301

293-
info.append(cb_name)
302+
if callback_name is not None:
303+
info.append(callback_name)
304+
elif self.callback is not None:
305+
info.append(format_callback_name(self.callback))
294306

295-
if self._source_traceback is not None:
296-
frame = self._source_traceback[-1]
307+
if source_traceback is not None:
308+
frame = source_traceback[-1]
297309
info.append('created at {}:{}'.format(frame[0], frame[1]))
298310

299311
return '<' + ' '.join(info) + '>'
@@ -305,6 +317,15 @@ cdef class TimerHandle:
305317
self._cancel()
306318

307319

320+
cdef format_callback_name(func):
321+
if hasattr(func, '__qualname__'):
322+
cb_name = getattr(func, '__qualname__')
323+
elif hasattr(func, '__name__'):
324+
cb_name = getattr(func, '__name__')
325+
else:
326+
cb_name = repr(func)
327+
return cb_name
328+
308329

309330
cdef new_Handle(Loop loop, object callback, object args, object context):
310331
cdef Handle handle

0 commit comments

Comments
 (0)