Fix/python tcpip socket read timeout - #3823
Conversation
select.select returns ([], [], []) on timeout, which fell into the retry branch and looped forever, so read_timeout never bounded an idle-but-open socket. Give the timeout its own branch matching the Ruby stream and write(), and treat only closed-socket errors from select as EOF. Fixes #3751 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Behavior is identical since setblocking() tests truthiness, but the file already used False on the client socket and in the MSG_DONTWAIT comment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3823 +/- ##
==========================================
+ Coverage 79.27% 79.30% +0.03%
==========================================
Files 895 896 +1
Lines 67271 67393 +122
Branches 2641 2659 +18
==========================================
+ Hits 53327 53448 +121
- Misses 13273 13275 +2
+ Partials 671 670 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
The new select() ValueError handling is overly broad (can mask real configuration/programming errors) and the new/updated tests still rely on fixed ports, which can make CI flaky.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes Python TcpipSocketStream.read() so read_timeout correctly bounds an idle-but-open socket by distinguishing select() timeouts from “retry” readiness, aligning behavior with the Ruby implementation and the existing interface-level timeout handling.
Changes:
- Fix
TcpipSocketStream.read()to raiseTimeoutError("Read Timeout")whenselect.selecttimes out, instead of looping forever. - Treat only closed-socket failures from
selectas EOF while re-raising unexpectedselectfailures. - Add targeted unit tests covering timeout, retry-on-readable, disconnect signaling, and select error handling.
File summaries
| File | Description |
|---|---|
| openc3/python/openc3/streams/tcpip_socket_stream.py | Adds explicit timeout handling and refines select() error handling in read() to prevent infinite retry loops. |
| openc3/python/test/streams/test_tcpip_socket_stream.py | Adds regression/unit tests for read timeout behavior and related select/EOF cases; introduces a reusable TCP server helper. |
| openc3/python/openc3/interfaces/tcpip_server_interface.py | Uses setblocking(False) for clarity/consistency. |
Review details
Suppressed comments (2)
openc3/python/test/streams/test_tcpip_socket_stream.py:161
- This new test binds to a fixed TCP port (20004). Fixed ports can make the suite flaky under parallel runs or if the port is in use. Prefer binding to port 0 and connecting to
server.server_address[1].
server = ReusableTCPServer(("localhost", 20004), MyTCPHandler)
threading.Thread(target=server.handle_request).start()
rs = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
rs.connect(("localhost", 20004))
openc3/python/test/streams/test_tcpip_socket_stream.py:183
- This test binds to a fixed TCP port (20002). To avoid intermittent failures due to port collisions, bind to an ephemeral port (0) and use
server.server_address[1]for the client connection.
server = ReusableTCPServer(("localhost", 20002), MyTCPHandler)
threading.Thread(target=server.handle_request).start()
rs = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
rs.connect(("localhost", 20002))
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ryanmelt
left a comment
There was a problem hiding this comment.
Implement Copilot suggestions
select.select raises ValueError for a closed socket, a negative timeout, and an out of range fd. Only the closed socket case is a disconnect, so check fileno() before returning EOF and re-raise everything else instead of hiding a real error behind a clean EOF. Bind the socket tests to an ephemeral port so concurrent runs cannot collide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|



What changed
Python's select.select returns ([], [], []) on timeout, which fell into the retry branch and looped forever, so read_timeout never bounded an idle-but-open socket. Give the timeout its own branch matching the Ruby stream and write(), and treat only closed-socket errors from select as EOF.
Why it changed
closes #3751
Testing strategy
Unit tests