mirror of
https://github.com/python/cpython.git
synced 2025-09-27 10:50:04 +00:00
bpo-31976: Fix race condition when flushing a file is slow. (#4331)
This commit is contained in:
parent
4652bf2acc
commit
9703f092ab
4 changed files with 58 additions and 8 deletions
19
Lib/_pyio.py
19
Lib/_pyio.py
|
@ -1188,11 +1188,11 @@ class BufferedWriter(_BufferedIOMixin):
|
||||||
return self.raw.writable()
|
return self.raw.writable()
|
||||||
|
|
||||||
def write(self, b):
|
def write(self, b):
|
||||||
if self.closed:
|
|
||||||
raise ValueError("write to closed file")
|
|
||||||
if isinstance(b, str):
|
if isinstance(b, str):
|
||||||
raise TypeError("can't write str to binary stream")
|
raise TypeError("can't write str to binary stream")
|
||||||
with self._write_lock:
|
with self._write_lock:
|
||||||
|
if self.closed:
|
||||||
|
raise ValueError("write to closed file")
|
||||||
# XXX we can implement some more tricks to try and avoid
|
# XXX we can implement some more tricks to try and avoid
|
||||||
# partial writes
|
# partial writes
|
||||||
if len(self._write_buf) > self.buffer_size:
|
if len(self._write_buf) > self.buffer_size:
|
||||||
|
@ -1253,6 +1253,21 @@ class BufferedWriter(_BufferedIOMixin):
|
||||||
self._flush_unlocked()
|
self._flush_unlocked()
|
||||||
return _BufferedIOMixin.seek(self, pos, whence)
|
return _BufferedIOMixin.seek(self, pos, whence)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
with self._write_lock:
|
||||||
|
if self.raw is None or self.closed:
|
||||||
|
return
|
||||||
|
# We have to release the lock and call self.flush() (which will
|
||||||
|
# probably just re-take the lock) in case flush has been overridden in
|
||||||
|
# a subclass or the user set self.flush to something. This is the same
|
||||||
|
# behavior as the C implementation.
|
||||||
|
try:
|
||||||
|
# may raise BlockingIOError or BrokenPipeError etc
|
||||||
|
self.flush()
|
||||||
|
finally:
|
||||||
|
with self._write_lock:
|
||||||
|
self.raw.close()
|
||||||
|
|
||||||
|
|
||||||
class BufferedRWPair(BufferedIOBase):
|
class BufferedRWPair(BufferedIOBase):
|
||||||
|
|
||||||
|
|
|
@ -168,6 +168,22 @@ class PyMisbehavedRawIO(MisbehavedRawIO, pyio.RawIOBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SlowFlushRawIO(MockRawIO):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.in_flush = threading.Event()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.in_flush.set()
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
class CSlowFlushRawIO(SlowFlushRawIO, io.RawIOBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PySlowFlushRawIO(SlowFlushRawIO, pyio.RawIOBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CloseFailureIO(MockRawIO):
|
class CloseFailureIO(MockRawIO):
|
||||||
closed = 0
|
closed = 0
|
||||||
|
|
||||||
|
@ -1726,6 +1742,18 @@ class BufferedWriterTest(unittest.TestCase, CommonBufferedTests):
|
||||||
self.assertRaises(OSError, b.close) # exception not swallowed
|
self.assertRaises(OSError, b.close) # exception not swallowed
|
||||||
self.assertTrue(b.closed)
|
self.assertTrue(b.closed)
|
||||||
|
|
||||||
|
def test_slow_close_from_thread(self):
|
||||||
|
# Issue #31976
|
||||||
|
rawio = self.SlowFlushRawIO()
|
||||||
|
bufio = self.tp(rawio, 8)
|
||||||
|
t = threading.Thread(target=bufio.close)
|
||||||
|
t.start()
|
||||||
|
rawio.in_flush.wait()
|
||||||
|
self.assertRaises(ValueError, bufio.write, b'spam')
|
||||||
|
self.assertTrue(bufio.closed)
|
||||||
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CBufferedWriterTest(BufferedWriterTest, SizeofTest):
|
class CBufferedWriterTest(BufferedWriterTest, SizeofTest):
|
||||||
tp = io.BufferedWriter
|
tp = io.BufferedWriter
|
||||||
|
@ -4085,7 +4113,8 @@ def load_tests(*args):
|
||||||
# Put the namespaces of the IO module we are testing and some useful mock
|
# Put the namespaces of the IO module we are testing and some useful mock
|
||||||
# classes in the __dict__ of each test.
|
# classes in the __dict__ of each test.
|
||||||
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
|
mocks = (MockRawIO, MisbehavedRawIO, MockFileIO, CloseFailureIO,
|
||||||
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead)
|
MockNonBlockWriterIO, MockUnseekableIO, MockRawIOWithoutRead,
|
||||||
|
SlowFlushRawIO)
|
||||||
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
|
all_members = io.__all__ + ["IncrementalNewlineDecoder"]
|
||||||
c_io_ns = {name : getattr(io, name) for name in all_members}
|
c_io_ns = {name : getattr(io, name) for name in all_members}
|
||||||
py_io_ns = {name : getattr(pyio, name) for name in all_members}
|
py_io_ns = {name : getattr(pyio, name) for name in all_members}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Fix race condition when flushing a file is slow, which can cause a
|
||||||
|
segfault if closing the file from another thread.
|
|
@ -347,9 +347,10 @@ _enter_buffered_busy(buffered *self)
|
||||||
}
|
}
|
||||||
|
|
||||||
#define IS_CLOSED(self) \
|
#define IS_CLOSED(self) \
|
||||||
|
(!self->buffer || \
|
||||||
(self->fast_closed_checks \
|
(self->fast_closed_checks \
|
||||||
? _PyFileIO_closed(self->raw) \
|
? _PyFileIO_closed(self->raw) \
|
||||||
: buffered_closed(self))
|
: buffered_closed(self)))
|
||||||
|
|
||||||
#define CHECK_CLOSED(self, error_msg) \
|
#define CHECK_CLOSED(self, error_msg) \
|
||||||
if (IS_CLOSED(self)) { \
|
if (IS_CLOSED(self)) { \
|
||||||
|
@ -1971,14 +1972,17 @@ _io_BufferedWriter_write_impl(buffered *self, Py_buffer *buffer)
|
||||||
Py_off_t offset;
|
Py_off_t offset;
|
||||||
|
|
||||||
CHECK_INITIALIZED(self)
|
CHECK_INITIALIZED(self)
|
||||||
if (IS_CLOSED(self)) {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "write to closed file");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ENTER_BUFFERED(self))
|
if (!ENTER_BUFFERED(self))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
/* Issue #31976: Check for closed file after acquiring the lock. Another
|
||||||
|
thread could be holding the lock while closing the file. */
|
||||||
|
if (IS_CLOSED(self)) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "write to closed file");
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fast path: the data to write can be fully buffered. */
|
/* Fast path: the data to write can be fully buffered. */
|
||||||
if (!VALID_READ_BUFFER(self) && !VALID_WRITE_BUFFER(self)) {
|
if (!VALID_READ_BUFFER(self) && !VALID_WRITE_BUFFER(self)) {
|
||||||
self->pos = 0;
|
self->pos = 0;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue