-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-115514: Fix incomplete writes after close in asyncio._SelectorSocketTransport #128037
Changes from all commits
060cba1
26908b7
9a7be93
c326506
f88ae92
8669d21
182d2ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1051,6 +1051,48 @@ def test_transport_close_remove_writer(self, m_log): | |
transport.close() | ||
remove_writer.assert_called_with(self.sock_fd) | ||
|
||
def test_write_buffer_after_close(self): | ||
# gh-115514: If the transport is closed while: | ||
# * Transport write buffer is not empty | ||
# * Transport is paused | ||
# * Protocol has data in its buffer, like SSLProtocol in self._outgoing | ||
# The data is still written out. | ||
|
||
# Also tested with real SSL transport in | ||
# test.test_asyncio.test_ssl.TestSSL.test_remote_shutdown_receives_trailing_data | ||
|
||
data = memoryview(b'data') | ||
self.sock.send.return_value = 2 | ||
self.sock.send.fileno.return_value = 7 | ||
|
||
def _resume_writing(): | ||
transport.write(b"data") | ||
self.protocol.resume_writing.side_effect = None | ||
|
||
self.protocol.resume_writing.side_effect = _resume_writing | ||
|
||
transport = self.socket_transport() | ||
transport._high_water = 1 | ||
|
||
transport.write(data) | ||
|
||
self.assertTrue(transport._protocol_paused) | ||
self.assertTrue(self.sock.send.called) | ||
self.loop.assert_writer(7, transport._write_ready) | ||
|
||
transport.close() | ||
|
||
# not called, we still have data in write buffer | ||
self.assertFalse(self.protocol.connection_lost.called) | ||
|
||
self.loop.writers[7]._run() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handlers should not be run manually, please change test to not rely on it Also I think this test should really be in ssl tests not here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, could you please suggest on how to do that? The
I don't think so, this is behavior of the SocketTransport, SSL just happens to be the one prevalent user that relies on this behavior. There already happens to be a more real-world-like test in SSL that almost triggers the bad path here, but I would somehow have to make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You can use mock for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so, the socket still needs to work because it sends data through it. I can shove this there however, artificially limit the socket buffer size which triggers the problem on my computer. I'm not convinced this is really better, but I can do that in addition to or instead of the mock-based test, what do you think? index 125a6c35793..b694b4d38db 100644
--- a/Lib/test/test_asyncio/test_ssl.py
+++ b/Lib/test/test_asyncio/test_ssl.py
@@ -12,6 +12,7 @@
import tempfile
import threading
import time
+import unittest.mock
import weakref
import unittest
@@ -1410,10 +1411,22 @@ async def client(addr):
except (BrokenPipeError, ConnectionResetError):
pass
- await future
+ socket_transport = writer.transport._ssl_protocol._transport
- writer.close()
- await self.wait_closed(writer)
+ def _shrink_sock_buffer(data):
+ if socket_transport._read_ready_cb is None:
+ socket_transport._sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1)
+ return unittest.mock.DEFAULT
+
+ with unittest.mock.patch.object(
+ socket_transport, "write",
+ wraps=socket_transport.write,
+ side_effect=_shrink_sock_buffer
+ ):
+ await future
+
+ writer.close()
+ await self.wait_closed(writer)
def run(meth):
def wrapper(sock): There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think limiting the buffer with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in latest commit. Still kept the original mocked test I added - the interaction from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test isn't failing for me even if I remove your fix in ssl test There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On what platform, and what exactly are you changing? On MacOS,while on the branch from this PR, I only have to roll back the fix in EDIT: ha, just tested on Linux and it does succeed there anyway - probably the socket behaves differently. Will debug it and let you know. diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py
index 22147451fa7..60bef420331 100644
--- a/Lib/asyncio/selector_events.py
+++ b/Lib/asyncio/selector_events.py
@@ -1188,7 +1188,7 @@ def _call_connection_lost(self, exc):
try:
super()._call_connection_lost(exc)
finally:
- self._write_ready = None
+ #self._write_ready = None
if self._empty_waiter is not None:
self._empty_waiter.set_exception(
ConnectionError("Connection is closed by peer"))
@@ -1206,6 +1206,7 @@ def _reset_empty_waiter(self):
def close(self):
self._read_ready_cb = None
+ self._write_ready = None
super().close() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, it's fixed now - problem was that on Linux, the minimum write buffer size is 1024, and it can only be set before the socket is connected anyway. I switched it to use a wrapper class that fakes the full buffer by only writing half the data. On linux, the ssl test was also not deterministic, it was not sending enough data and they actually got through before |
||
# during this ^ run, the _resume_writing mock above was called and added more data | ||
|
||
self.assertEqual(transport.get_write_buffer_size(), 2) | ||
self.loop.writers[7]._run() | ||
|
||
self.assertEqual(transport.get_write_buffer_size(), 0) | ||
self.assertTrue(self.protocol.connection_lost.called) | ||
|
||
class SelectorSocketTransportBufferedProtocolTests(test_utils.TestCase): | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Fix exceptions and incomplete writes after :class:`!asyncio._SelectorTransport` | ||
is closed before writes are completed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good idea to add a comment explaining why we need this (and link the issue number as well).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean the try/finally? It calls user-supplied callback internally, I'm just doing the same thing the parent
SelectorTransport
is:cpython/Lib/asyncio/selector_events.py
Lines 904 to 916 in d0ecbdd
Do you think I should write a comment here about that?