Commit ae3ae063 authored by Jason Madden's avatar Jason Madden

Deterministically close GreenFileDescriptorIO backing FileObjectPosix if the...

Deterministically close GreenFileDescriptorIO backing FileObjectPosix if the (libuv) event loop watchers produce an error.

Especially on Python 3.7, the C-level garbage collection could kick in and close the underlying fileno at an inopportune time, killing an unrelated file, even though there were no Python-level references alive (apparently).
parent d72e5490
...@@ -33,19 +33,37 @@ class GreenFileDescriptorIO(RawIOBase): ...@@ -33,19 +33,37 @@ class GreenFileDescriptorIO(RawIOBase):
def __init__(self, fileno, mode='r', closefd=True): def __init__(self, fileno, mode='r', closefd=True):
RawIOBase.__init__(self) # Python 2: pylint:disable=no-member,non-parent-init-called RawIOBase.__init__(self) # Python 2: pylint:disable=no-member,non-parent-init-called
self._closefd = closefd self._closefd = closefd
self._fileno = fileno self._fileno = fileno
make_nonblocking(fileno) make_nonblocking(fileno)
readable = 'r' in mode readable = 'r' in mode
writable = 'w' in mode writable = 'w' in mode
self.hub = get_hub()
self.hub = get_hub()
io_watcher = self.hub.loop.io io_watcher = self.hub.loop.io
try:
if readable: if readable:
self._read_event = io_watcher(fileno, 1) self._read_event = io_watcher(fileno, 1)
if writable: if writable:
self._write_event = io_watcher(fileno, 2) self._write_event = io_watcher(fileno, 2)
except:
# If anything goes wrong, it's important to go ahead and
# close these watchers *now*, especially under libuv, so
# that they don't get eventually reclaimed by the garbage
# collector at some random time, thanks to the C level
# slot (even though we don't seem to have any actual references
# at the Python level). Previously, if we didn't close now,
# that random close in the future would cause issues if we had duplicated
# the fileno (if a wrapping with statement had closed an open fileobject,
# for example)
# test__fileobject can show a failure if this doesn't happen
# TRAVIS=true GEVENT_LOOP=libuv python -m gevent.tests.test__fileobject \
# TestFileObjectPosix.test_seek TestFileObjectThread.test_bufsize_0
self.close()
raise
def readable(self): def readable(self):
return self._read_event is not None return self._read_event is not None
...@@ -70,6 +88,17 @@ class GreenFileDescriptorIO(RawIOBase): ...@@ -70,6 +88,17 @@ class GreenFileDescriptorIO(RawIOBase):
def closed(self): def closed(self):
return self._closed return self._closed
def __destroy_events(self):
read_event = self._read_event
write_event = self._write_event
hub = self.hub
self.hub = self._read_event = self._write_event = None
if read_event is not None:
hub.cancel_wait(read_event, cancel_wait_ex, True)
if write_event is not None:
hub.cancel_wait(write_event, cancel_wait_ex, True)
def close(self): def close(self):
if self._closed: if self._closed:
return return
...@@ -77,15 +106,7 @@ class GreenFileDescriptorIO(RawIOBase): ...@@ -77,15 +106,7 @@ class GreenFileDescriptorIO(RawIOBase):
# TODO: Can we use 'read_event is not None and write_event is # TODO: Can we use 'read_event is not None and write_event is
# not None' to mean _closed? # not None' to mean _closed?
self._closed = True self._closed = True
read_event = self._read_event self.__destroy_events()
write_event = self._write_event
self._read_event = self._write_event = None
if read_event is not None:
self.hub.cancel_wait(read_event, cancel_wait_ex, True)
if write_event is not None:
self.hub.cancel_wait(write_event, cancel_wait_ex, True)
fileno = self._fileno fileno = self._fileno
if self._closefd: if self._closefd:
self._fileno = None self._fileno = None
...@@ -295,10 +316,6 @@ class FileObjectPosix(FileObjectBase): ...@@ -295,10 +316,6 @@ class FileObjectPosix(FileObjectBase):
# that code was never tested and was explicitly marked as "not used" # that code was never tested and was explicitly marked as "not used"
raise ValueError('mode can only be [rb, rU, wb], not %r' % (orig_mode,)) raise ValueError('mode can only be [rb, rU, wb], not %r' % (orig_mode,))
self._fobj = fobj
# This attribute is documented as available for non-blocking reads.
self.fileio = GreenFileDescriptorIO(fileno, mode, closefd=close)
self._orig_bufsize = bufsize self._orig_bufsize = bufsize
if bufsize < 0 or bufsize == 1: if bufsize < 0 or bufsize == 1:
...@@ -317,7 +334,14 @@ class FileObjectPosix(FileObjectBase): ...@@ -317,7 +334,14 @@ class FileObjectPosix(FileObjectBase):
# attribute. # attribute.
IOFamily = FlushingBufferedWriter IOFamily = FlushingBufferedWriter
super(FileObjectPosix, self).__init__(IOFamily(self.fileio, bufsize), close)
self._fobj = fobj
# This attribute is documented as available for non-blocking reads.
self.fileio = GreenFileDescriptorIO(fileno, mode, closefd=close)
buffered_fobj = IOFamily(self.fileio, bufsize)
super(FileObjectPosix, self).__init__(buffered_fobj, close)
def _do_close(self, fobj, closefd): def _do_close(self, fobj, closefd):
try: try:
......
...@@ -23,8 +23,6 @@ import sys ...@@ -23,8 +23,6 @@ import sys
import functools import functools
import unittest import unittest
from gevent.util import dump_stacks
from . import sysinfo from . import sysinfo
from . import six from . import six
...@@ -69,7 +67,6 @@ reraiseFlakyTestTimeoutLibuv = reraiseFlakyTestRaceCondition ...@@ -69,7 +67,6 @@ reraiseFlakyTestTimeoutLibuv = reraiseFlakyTestRaceCondition
if sysinfo.RUNNING_ON_CI or (sysinfo.PYPY and sysinfo.WIN): if sysinfo.RUNNING_ON_CI or (sysinfo.PYPY and sysinfo.WIN):
# pylint: disable=function-redefined # pylint: disable=function-redefined
def reraiseFlakyTestRaceCondition(): def reraiseFlakyTestRaceCondition():
if sysinfo.PYPY and sysinfo.WIN:
# Getting stack traces is incredibly expensive # Getting stack traces is incredibly expensive
# in pypy on win, at least in test virtual machines. # in pypy on win, at least in test virtual machines.
# It can take minutes. The traceback consistently looks like # It can take minutes. The traceback consistently looks like
...@@ -78,9 +75,13 @@ if sysinfo.RUNNING_ON_CI or (sysinfo.PYPY and sysinfo.WIN): ...@@ -78,9 +75,13 @@ if sysinfo.RUNNING_ON_CI or (sysinfo.PYPY and sysinfo.WIN):
# dump_stacks -> traceback.format_stack # dump_stacks -> traceback.format_stack
# -> traceback.extract_stack -> linecache.checkcache # -> traceback.extract_stack -> linecache.checkcache
# -> os.stat -> _structseq.structseq_new # -> os.stat -> _structseq.structseq_new
# Moreover, without overriding __repr__ or __str__,
# the msg doesn't get printed like we would want (its basically
# unreadable, all printed on one line). So skip that.
#msg = '\n'.join(dump_stacks())
msg = str(sys.exc_info()[1]) msg = str(sys.exc_info()[1])
else:
msg = '\n'.join(dump_stacks())
six.reraise(FlakyTestRaceCondition, six.reraise(FlakyTestRaceCondition,
FlakyTestRaceCondition(msg), FlakyTestRaceCondition(msg),
sys.exc_info()[2]) sys.exc_info()[2])
......
...@@ -102,7 +102,7 @@ class TestFileObjectBlock(greentest.TestCase): ...@@ -102,7 +102,7 @@ class TestFileObjectBlock(greentest.TestCase):
with open(path, 'rb') as f_raw: with open(path, 'rb') as f_raw:
try: try:
f = self._makeOne(f_raw, 'rb') f = self._makeOne(f_raw, 'rb', close=False)
except ValueError: except ValueError:
# libuv on Travis can raise EPERM # libuv on Travis can raise EPERM
# from FileObjectPosix. I can't produce it on mac os locally, # from FileObjectPosix. I can't produce it on mac os locally,
...@@ -111,12 +111,17 @@ class TestFileObjectBlock(greentest.TestCase): ...@@ -111,12 +111,17 @@ class TestFileObjectBlock(greentest.TestCase):
# That shouldn't have any effect on io watchers, though, which were # That shouldn't have any effect on io watchers, though, which were
# already being explicitly closed. # already being explicitly closed.
reraiseFlakyTestRaceConditionLibuv() reraiseFlakyTestRaceConditionLibuv()
if PY3 or hasattr(f, 'seekable'): if PY3 or hasattr(f, 'seekable'):
# On Python 3, all objects should have seekable. # On Python 3, all objects should have seekable.
# On Python 2, only our custom objects do. # On Python 2, only our custom objects do.
self.assertTrue(f.seekable()) self.assertTrue(f.seekable())
f.seek(15) f.seek(15)
self.assertEqual(15, f.tell()) self.assertEqual(15, f.tell())
# Note that a duplicate close() of the underlying
# file descriptor can look like an OSError from this line
# as we exit the with block
fileobj_data = f.read(1024) fileobj_data = f.read(1024)
self.assertEqual(native_data, s) self.assertEqual(native_data, s)
...@@ -183,23 +188,6 @@ class TestFileObjectThread(ConcurrentFileObjectMixin, ...@@ -183,23 +188,6 @@ class TestFileObjectThread(ConcurrentFileObjectMixin,
def _getTargetClass(self): def _getTargetClass(self):
return fileobject.FileObjectThread return fileobject.FileObjectThread
def tearDown(self):
# Make sure outstanding tasks have completed.
# On Travis with Python 3.7 and libuv, test_bufsize_0
# fails in os.fdopen(), claiming the file descriptor is bad.
# This would happen if we closed (garbage collected?) a FD,
# opened a pipe and got the same int FD, and then some background
# task closed that same int FD again. FileObjectThread.close()
# goes through threadpool.apply(), which is supposed to be synchronous;
# make sure it is.
gevent.get_hub().threadpool.join()
# And collect any outstanding garbage, in case some resource is "leaking"
# (We have no indication that it is, but we're flailing wildly here to try
# to understand what could be happening)
gc.collect()
super(TestFileObjectThread, self).tearDown()
# FileObjectThread uses os.fdopen() when passed a file-descriptor, # FileObjectThread uses os.fdopen() when passed a file-descriptor,
# which returns an object with a destructor that can't be # which returns an object with a destructor that can't be
# bypassed, so we can't even create one that way # bypassed, so we can't even create one that way
......
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