Skip to content

fix(interface): make connect_interface a no-op when already connected - #3828

Merged
jmthomas merged 2 commits into
mainfrom
fix/connect-interface-already-connected
Sep 9, 2026
Merged

fix(interface): make connect_interface a no-op when already connected#3828
jmthomas merged 2 commits into
mainfrom
fix/connect-interface-already-connected

Conversation

@jmthomas

@jmthomas jmthomas commented Sep 3, 2026

Copy link
Copy Markdown
Member

What changed

The redundant-disconnect check and the state change that records it now share one critical section; splitting them let a second thread disconnect the interface twice. attempting() gained the already-connected guard, so the reconnect path calls attempt_connection() directly to avoid being ignored when interface cleanup raises or leaves connected? true.

Why it changed

Connecting a CONNECTED interface set state to ATTEMPTING without touching the socket, so a working connection was torn down and rebuilt only once the blocking read returned, costing a full read_timeout. Interface connect now closes any existing stream instead of abandoning it to the GC, and disconnect no longer skips cleanup based on connected?.

Fixes #3798

Testing strategy

Mostly unit tests. Also ran the demo and connected and disconnect. Ran script to test multiple connect / disconnect:

for interface in ["INST_INT", "INST2_INT"]:
  disconnect_interface(interface)
  wait(3)
  disconnect_interface(interface)
  wait(3)
  connect_interface(interface)
  wait(3)
  connect_interface(interface)
  wait(3)

while watching the CmdTlmServer. Observed Connect ignored, already connected messages. Finally connected EXAMPLE_INT and TEMPLATE_INT and then restarted their backend interfaces to observe reconnect.

Connecting a CONNECTED interface set state to ATTEMPTING without touching
the socket, so a working connection was torn down and rebuilt only once
the blocking read returned, costing a full read_timeout. Interface connect
now closes any existing stream instead of abandoning it to the GC, and
disconnect no longer skips cleanup based on connected?.

The redundant-disconnect check and the state change that records it now
share one critical section; splitting them let a second thread disconnect
the interface twice. attempting() gained the already-connected guard, so
the reconnect path calls attempt_connection() directly to avoid being
ignored when interface cleanup raises or leaves connected? true.

Fixes #3798

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@jmthomas
jmthomas requested review from calmonroe and ryanmelt and a lite review from Copilot September 3, 2026 22:46
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.30%. Comparing base (b5ee05d) to head (09b7a0c).
⚠️ Report is 44 commits behind head on main.

Files with missing lines Patch % Lines
openc3/lib/openc3/interfaces/serial_interface.rb 0.00% 1 Missing ⚠️
...lib/openc3/microservices/interface_microservice.rb 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3828      +/-   ##
==========================================
+ Coverage   79.27%   79.30%   +0.03%     
==========================================
  Files         895      896       +1     
  Lines       67271    67399     +128     
  Branches     2641     2608      -33     
==========================================
+ Hits        53327    53453     +126     
- Misses      13273    13276       +3     
+ Partials      671      670       -1     
Flag Coverage Δ
frontend 66.56% <ø> (+0.35%) ⬆️
python 79.37% <ø> (+<0.01%) ⬆️
ruby-api 82.22% <ø> (-0.28%) ⬇️
ruby-backend 84.58% <90.90%> (-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

InterfaceThread#connect now calls disconnect without handling exceptions, which can break reconnection if an interface’s disconnect raises.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR tightens the interface connection state machine to make connect_interface a safe no-op when an interface is already CONNECTED, while also addressing a disconnect race that could trigger duplicate disconnects and improving resource cleanup to avoid leaking sockets/streams.

Changes:

  • Add an “already connected” guard in attempting() and introduce attempt_connection() to ensure reconnect paths still force a state transition.
  • Fix a potential double-disconnect race by combining the redundant-disconnect check and state recording in a single critical section; move reconnect-delay sleep outside the mutex.
  • Ensure connection implementations cleanly close any existing stream/socket before overwriting it; add/expand unit tests and update scripting API docs.
File summaries
File Description
openc3/spec/microservices/interface_microservice_spec.rb Adds Ruby specs covering redundant connect no-op, reconnect edge cases, and cleanup behavior.
openc3/python/test/microservices/test_interface_microservice.py Adds Python tests mirroring the “already connected” no-op and reconnect edge cases.
openc3/python/openc3/microservices/interface_microservice.py Implements CONNECTED no-op for redundant connect, refactors reconnect path, and improves disconnect synchronization.
openc3/python/openc3/interfaces/tcpip_client_interface.py Closes any existing stream before creating a new TCP/IP client stream.
openc3/python/openc3/interfaces/serial_interface.py Closes any existing stream before creating a new serial stream.
openc3/python/openc3/interfaces/mqtt_stream_interface.py Closes any existing stream before creating a new MQTT stream.
openc3/lib/openc3/tools/cmd_tlm_server/interface_thread.rb Adds pre-connect disconnect to avoid leaking existing connections in the CmdTlmServer interface thread.
openc3/lib/openc3/microservices/interface_microservice.rb Implements CONNECTED no-op for redundant connect and fixes disconnect synchronization/reconnect path in Ruby microservice.
openc3/lib/openc3/interfaces/tcpip_client_interface.rb Closes any existing stream before creating a new TCP/IP client stream.
openc3/lib/openc3/interfaces/serial_interface.rb Closes any existing stream before creating a new serial stream.
openc3/lib/openc3/interfaces/mqtt_stream_interface.rb Closes any existing stream before creating a new MQTT stream.
docs.openc3.com/docs/guides/scripting-api.md Documents the no-op behavior for redundant connect_interface and the updated disconnect semantics.
Review details
  • Files reviewed: 12/12 changed files
  • Comments generated: 1
  • 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/lib/openc3/tools/cmd_tlm_server/interface_thread.rb
Comment thread openc3/lib/openc3/tools/cmd_tlm_server/interface_thread.rb
The pre-connect disconnect can raise, which would prevent the reconnect
and potentially kill the interface thread. Log the error and continue,
matching the InterfaceMicroservice#connect behavior.

🤖 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:27
@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@jmthomas
jmthomas merged commit 422a837 into main Sep 9, 2026
36 checks passed
@jmthomas
jmthomas deleted the fix/connect-interface-already-connected branch September 9, 2026 15:28
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.

connect_interface on an already-CONNECTED interface — intentional, or should it be a safe no-op?

4 participants