Commit f466ec51 authored by Jason Madden's avatar Jason Madden

Be more careful about the lifecycle management of watcher objects that may...

Be more careful about the lifecycle management of watcher objects that may stop themselves. Fixes #676
parent 5fafd8e6
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
- Python 2: Workaround a buffering bug in the stdlib ``io`` module - Python 2: Workaround a buffering bug in the stdlib ``io`` module
that caused ``FileObjectPosix`` to be slower than necessary in some that caused ``FileObjectPosix`` to be slower than necessary in some
cases. Reported in :issue:`675` by WGH-. cases. Reported in :issue:`675` by WGH-.
- PyPy: Fix a potential crash. Reported in :issue:`676` by Jay Oster.
1.1b6 (Oct 17, 2015) 1.1b6 (Oct 17, 2015)
==================== ====================
......
from __future__ import absolute_import # pylint: disable=too-many-lines, protected-access, redefined-outer-name
from __future__ import absolute_import, print_function
import sys import sys
import os import os
import traceback import traceback
...@@ -272,13 +273,25 @@ for _watcher_type in _watcher_types: ...@@ -272,13 +273,25 @@ for _watcher_type in _watcher_types:
{ {
// invoke self.callback() // invoke self.callback()
void* handle = ((struct gevent_%s *)watcher)->handle; void* handle = ((struct gevent_%s *)watcher)->handle;
if( python_callback(handle, revents) < 0) { int cb_result = python_callback(handle, revents);
/* in case of exception, call self.loop.handle_error */ switch(cb_result) {
python_handle_error(handle, revents); case -1:
} // in case of exception, call self.loop.handle_error;
// Code to stop the event // this function is also responsible for stopping the watcher
if (!ev_is_active(watcher)) { // and allowing memory to be freed
python_stop(handle); python_handle_error(handle, revents);
break;
case 0:
// Code to stop the event. Note that if python_callback
// has disposed of the last reference to the handle,
// `watcher` could now be invalid/disposed memory!
if (!ev_is_active(watcher)) {
python_stop(handle);
}
break;
default:
assert(cb_result == 1);
// watcher is already stopped and dead, nothing to do.
} }
} }
""" % (_watcher_type, _watcher_type, _watcher_type) """ % (_watcher_type, _watcher_type, _watcher_type)
...@@ -292,41 +305,96 @@ del thisdir, include_dirs, _watcher_type, _watcher_types ...@@ -292,41 +305,96 @@ del thisdir, include_dirs, _watcher_type, _watcher_types
libev.vfd_open = libev.vfd_get = lambda fd: fd libev.vfd_open = libev.vfd_get = lambda fd: fd
libev.vfd_free = lambda fd: None libev.vfd_free = lambda fd: None
#####
## Note on CFFI objects, callbacks and the lifecycle of watcher objects
#
# Each subclass of `watcher` allocates a C structure of the
# appropriate type e.g., struct gevent_ev_io and holds this pointer in
# its `_gwatcher` attribute. When that watcher instance is garbage
# collected, then the C structure is also freed. The C structure is
# passed to libev from the watcher's start() method and then to the
# appropriate C callback function, e.g., _gevent_ev_io_callback, which
# passes it back to python's _python_callback where we need the
# watcher instance. Therefore, as long as that callback is active (the
# watcher is started), the watcher instance must not be allowed to get
# GC'd---any access at the C level or even the FFI level to the freed
# memory could crash the process.
#
# However, the typical idiom calls for writing something like this:
# loop.io(fd, python_cb).start()
# thus forgetting the newly created watcher subclass and allowing it to be immediately
# GC'd. To combat this, when the watcher is started, it places itself into the loop's
# `_keepaliveset`, and it only removes itself when the watcher's `stop()` method is called.
# Often, this is the *only* reference keeping the watcher object, and hence its C structure,
# alive.
#
# This is slightly complicated by the fact that the python-level
# callback, called from the C callback, could choose to manually stop
# the watcher. When we return to the C level callback, we now have an
# invalid pointer, and attempting to pass it back to Python (e.g., to
# handle an error) could crash. Hence, _python_callback,
# _gevent_io_callback, and _python_handle_error cooperate to make sure
# that the watcher instance stays in the loops `_keepaliveset` while
# the C code could be running---and if it gets removed, to not call back
# to Python again.
# See also https://github.com/gevent/gevent/issues/676
####
@ffi.callback("int(void* handle, int revents)") @ffi.callback("int(void* handle, int revents)")
def _python_callback(handle, revents): def _python_callback(handle, revents):
watcher = ffi.from_handle(handle) """
if len(watcher.args) > 0 and watcher.args[0] == GEVENT_CORE_EVENTS: Returns an integer having one of three values:
watcher.args = (revents, ) + watcher.args[1:]
- -1
An exception occurred during the callback and you must call
:func:`_python_handle_error` to deal with it. The Python watcher
object will have the exception tuple saved in ``_exc_info``.
- 0
Everything went according to plan. You should check to see if the libev
watcher is still active, and call :func:`_python_stop` if so. This will
clean up the memory.
- 1
Everything went according to plan, but the watcher has already
been stopped. Its memory may no longer be valid.
"""
try: try:
# Even dereferencing the handle needs to be inside the try/except;
# if we don't return normally (e.g., a signal) then we wind up going
# to the 'onerror' handler if _cffi_supports_on_error is True, which
# is not what we want; that can permanently wedge the loop depending
# on which callback was executing
watcher = ffi.from_handle(handle)
if len(watcher.args) > 0 and watcher.args[0] == GEVENT_CORE_EVENTS:
watcher.args = (revents, ) + watcher.args[1:]
watcher.callback(*watcher.args) watcher.callback(*watcher.args)
except: except:
watcher._exc_info = sys.exc_info() watcher._exc_info = sys.exc_info()
# Depending on when the exception happened, the watcher
# may or may not have been stopped. We need to make sure its
# memory stays valid so we can stop it at the ev level if needed.
watcher.loop._keepaliveset.add(watcher)
return -1 return -1
else: else:
return 0 if watcher in watcher.loop._keepaliveset:
# It didn't stop itself
return 0
return 1 # It stopped itself
libev.python_callback = _python_callback libev.python_callback = _python_callback
# After _python_callback is called, the handle may no longer be
# valid. The callback itself might have called watcher.stop(),
# which would remove the object from loop.keepaliveset, and if
# that was the last reference to it, the handle would be GC'd.
# Therefore the other functions need to correctly deal with an
# invalid handle
@ffi.callback("void(void* handle, int revents)") @ffi.callback("void(void* handle, int revents)")
def _python_handle_error(handle, revents): def _python_handle_error(handle, revents):
try: try:
watcher = ffi.from_handle(handle) watcher = ffi.from_handle(handle)
except RuntimeError: exc_info = watcher._exc_info
return del watcher._exc_info
exc_info = watcher._exc_info
del watcher._exc_info
try:
watcher.loop.handle_error(watcher, *exc_info) watcher.loop.handle_error(watcher, *exc_info)
finally: finally:
# XXX Since we're here on an error condition, and we
# made sure that the watcher object was put in loop._keepaliveset,
# what about not stopping the watcher? Looks like a possible
# memory leak?
if revents & (libev.EV_READ | libev.EV_WRITE): if revents & (libev.EV_READ | libev.EV_WRITE):
try: try:
watcher.stop() watcher.stop()
...@@ -338,10 +406,7 @@ libev.python_handle_error = _python_handle_error ...@@ -338,10 +406,7 @@ libev.python_handle_error = _python_handle_error
@ffi.callback("void(void* handle)") @ffi.callback("void(void* handle)")
def _python_stop(handle): def _python_stop(handle):
try: watcher = ffi.from_handle(handle)
watcher = ffi.from_handle(handle)
except RuntimeError:
return
watcher.stop() watcher.stop()
libev.python_stop = _python_stop libev.python_stop = _python_stop
...@@ -909,13 +974,29 @@ class watcher(object): ...@@ -909,13 +974,29 @@ class watcher(object):
getattr(libev, '_gevent_' + self._watcher_type + '_callback'), getattr(libev, '_gevent_' + self._watcher_type + '_callback'),
*args) *args)
# A string identifying the type of libev object we watch, e.g., 'ev_io'
# This should be a class attribute.
_watcher_type = None
def _watcher_init(self, watcher_ptr, cb, *args):
"Init the watcher. Subclasses must define."
raise NotImplementedError()
def _watcher_start(self, loop_ptr, watcher_ptr):
"Start the watcher. Subclasses must define."
raise NotImplementedError()
def _watcher_stop(self, loop_ptr, watcher_ptr):
"Stop the watcher. Subclasses must define."
raise NotImplementedError()
# this is not needed, since we keep alive the watcher while it's started # this is not needed, since we keep alive the watcher while it's started
#def __del__(self): #def __del__(self):
# self._watcher_stop(self.loop._ptr, self._watcher) # self._watcher_stop(self.loop._ptr, self._watcher)
def __repr__(self): def __repr__(self):
format = self._format() formats = self._format()
result = "<%s at 0x%x%s" % (self.__class__.__name__, id(self), format) result = "<%s at 0x%x%s" % (self.__class__.__name__, id(self), formats)
if self.pending: if self.pending:
result += " pending" result += " pending"
if self.callback is not None: if self.callback is not None:
...@@ -924,6 +1005,7 @@ class watcher(object): ...@@ -924,6 +1005,7 @@ class watcher(object):
result += " args=%r" % (self.args, ) result += " args=%r" % (self.args, )
if self.callback is None and self.args is None: if self.callback is None and self.args is None:
result += " stopped" result += " stopped"
result += " handle=%s" % (self._gwatcher.handle)
return result + ">" return result + ">"
def _format(self): def _format(self):
...@@ -957,10 +1039,10 @@ class watcher(object): ...@@ -957,10 +1039,10 @@ class watcher(object):
def _get_callback(self): def _get_callback(self):
return self._callback return self._callback
def _set_callback(self, callback): def _set_callback(self, cb):
if not callable(callback) and callback is not None: if not callable(cb) and cb is not None:
raise TypeError("Expected callable, not %r" % (callback, )) raise TypeError("Expected callable, not %r" % (cb, ))
self._callback = callback self._callback = cb
callback = property(_get_callback, _set_callback) callback = property(_get_callback, _set_callback)
def start(self, callback, *args): def start(self, callback, *args):
...@@ -1072,12 +1154,12 @@ class timer(watcher): ...@@ -1072,12 +1154,12 @@ class timer(watcher):
watcher.__init__(self, loop, ref=ref, priority=priority, args=(after, repeat)) watcher.__init__(self, loop, ref=ref, priority=priority, args=(after, repeat))
def start(self, callback, *args, **kw): def start(self, callback, *args, **kw):
# XXX: Almost the same as watcher.start
if callback is None: if callback is None:
raise TypeError('callback must be callable, not None') raise TypeError('callback must be callable, not None')
update = kw.get("update", True) update = kw.get("update", True)
self.callback = callback self.callback = callback
self.args = args or _NOARGS self.args = args or _NOARGS
self._libev_unref() # LIBEV_UNREF self._libev_unref() # LIBEV_UNREF
if update: if update:
......
# Copyright (c) 2009-2011 Denis Bilenko. See LICENSE for details. # Copyright (c) 2009-2011 Denis Bilenko. See LICENSE for details.
"""Basic synchronization primitives: Event and AsyncResult""" """Basic synchronization primitives: Event and AsyncResult"""
from __future__ import print_function
import sys import sys
from gevent.hub import get_hub, getcurrent, _NONE, PY3, reraise from gevent.hub import get_hub, getcurrent, _NONE, PY3, reraise
from gevent.hub import InvalidSwitchError from gevent.hub import InvalidSwitchError
...@@ -81,7 +81,7 @@ class Event(object): ...@@ -81,7 +81,7 @@ class Event(object):
switch = getcurrent().switch switch = getcurrent().switch
self.rawlink(switch) self.rawlink(switch)
try: try:
timer = Timeout.start_new(timeout) timer = Timeout.start_new(timeout) if timeout is not None else None
try: try:
try: try:
result = self.hub.switch() result = self.hub.switch()
...@@ -91,7 +91,8 @@ class Event(object): ...@@ -91,7 +91,8 @@ class Event(object):
if ex is not timer: if ex is not timer:
raise raise
finally: finally:
timer.cancel() if timer is not None:
timer.cancel()
finally: finally:
self.unlink(switch) self.unlink(switch)
return self._flag return self._flag
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment