-
-
Notifications
You must be signed in to change notification settings - Fork 99
Fixes for WantWriteError and WantReadError handling #764
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -3,6 +3,9 @@ | |
| # prefer slower Python-based io module | ||
| import _pyio as io | ||
| import socket | ||
| import time | ||
|
|
||
| from OpenSSL import SSL | ||
|
|
||
|
|
||
| # Write only 16K at a time to sockets | ||
|
|
@@ -31,7 +34,24 @@ def _flush_unlocked(self): | |
| # so perhaps we should conditionally wrap this for perf? | ||
| n = self.raw.write(bytes(self._write_buf)) | ||
| except io.BlockingIOError as e: | ||
| # some data may have been written | ||
| # we need to remove that from the buffer before retryings | ||
| n = e.characters_written | ||
| except ( | ||
| SSL.WantReadError, | ||
| SSL.WantWriteError, | ||
| SSL.WantX509LookupError, | ||
| ): | ||
| # these errors require retries with the same data | ||
| # regardless of whether data has already been written | ||
| continue | ||
|
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. How do these leak into the generic writer? Is there an underlying layer where we could catch these and re-raise and common/generic exceptions? Can we scope accessing PyOpenSSL to the If not, we'll probably have to have a common var in |
||
| except OSError: | ||
| # This catches errors like EBADF (Bad File Descriptor) | ||
| # or EPIPE (Broken pipe), which indicate the underlying | ||
|
Comment on lines
+49
to
+50
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. These can be made more specific instead of a broad |
||
| # socket is already closed or invalid. | ||
| # Since this happens in __del__, we silently stop flushing. | ||
| self._write_buf.clear() | ||
| return # Exit the function | ||
| del self._write_buf[:n] | ||
|
|
||
|
|
||
|
|
@@ -45,9 +65,22 @@ def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): | |
|
|
||
| def read(self, *args, **kwargs): | ||
| """Capture bytes read.""" | ||
| val = super().read(*args, **kwargs) | ||
| self.bytes_read += len(val) | ||
| return val | ||
| MAX_ATTEMPTS = 10 | ||
| last_error = None | ||
| for _ in range(MAX_ATTEMPTS): | ||
| try: | ||
| val = super().read(*args, **kwargs) | ||
| except (SSL.WantReadError, SSL.WantWriteError) as ssl_want_error: | ||
|
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've basically the same concern with leaking PyOpenSSL into the outer scope here as in https://github.com/cherrypy/cheroot/pull/764/files#r2432889204. |
||
| last_error = ssl_want_error | ||
| time.sleep(0.1) | ||
| else: | ||
| self.bytes_read += len(val) | ||
| return val | ||
julianz- marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # If we get here, all attempts failed | ||
| raise TimeoutError( | ||
| 'Max retries exceeded while waiting for data.', | ||
| ) from last_error | ||
|
|
||
| def has_data(self): | ||
| """Return true if there is buffered data to read.""" | ||
|
|
||
julianz- marked this conversation as resolved.
Show resolved
Hide resolved
|
|
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. Symlink |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| Added handling for WantWriteError and WantReadError in BufferedWriter | ||
| and StreamReader to enable retries. This addresses long standing issues | ||
|
Comment on lines
+1
to
+2
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. All these are linkable in Sphinx. pyOpenSSL is plugged via intersphinx. And internal objects are exposed too. See these to discover the refs: |
||
| discussed in #245. The reliability of the fix relies on using pyOpenSSL | ||
|
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. Use Additionally, you could symlink this change note to that number as well so both will be linked in the change log. |
||
| v25.2.0 or greater, as earlier versions have known bugs that affect | ||
| the retry logic. | ||
|
|
||
| -- by :user:`julianz-` | ||
|
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. This shouldn't be needed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ positionally | |
| pre | ||
| preconfigure | ||
| py | ||
| pyOpenSSL | ||
| pytest | ||
| pythonic | ||
| readonly | ||
|
|
||
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.
Looks like this will have to become conditional/guarded since pyOpenSSL is an optional dependency. It might be a good idea to add infra for running the tests w/o optional deps but that certainly out of the scope of this PR.