Skip to content

Fix/python tcpip socket read timeout - #3823

Merged
jmthomas merged 3 commits into
mainfrom
fix/python-tcpip-socket-read-timeout
Sep 8, 2026
Merged

Fix/python tcpip socket read timeout#3823
jmthomas merged 3 commits into
mainfrom
fix/python-tcpip-socket-read-timeout

Conversation

@jmthomas

@jmthomas jmthomas commented Sep 3, 2026

Copy link
Copy Markdown
Member

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

jmthomas and others added 2 commits September 3, 2026 14:55
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>
@jmthomas
jmthomas requested review from mcosgriff and ryanmelt and a lite review from Copilot September 3, 2026 21:03
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.30%. Comparing base (b5ee05d) to head (441afd0).
⚠️ Report is 44 commits behind head on main.

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     
Flag Coverage Δ
frontend 66.48% <ø> (+0.27%) ⬆️
python 79.38% <ø> (+0.02%) ⬆️
ruby-api 82.36% <ø> (-0.14%) ⬇️
ruby-backend 84.58% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 raise TimeoutError("Read Timeout") when select.select times out, instead of looping forever.
  • Treat only closed-socket failures from select as EOF while re-raising unexpected select failures.
  • 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.

Comment thread openc3/python/openc3/streams/tcpip_socket_stream.py Outdated
Comment thread openc3/python/test/streams/test_tcpip_socket_stream.py Outdated

@ryanmelt ryanmelt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@jmthomas
jmthomas requested a review from ryanmelt September 4, 2026 23:38
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@jmthomas
jmthomas merged commit 7a78f94 into main Sep 8, 2026
36 checks passed
@jmthomas
jmthomas deleted the fix/python-tcpip-socket-read-timeout branch September 8, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python TcpipSocketStream ignores read_timeout: a silent socket never times out

4 participants