Skip to content

Fix reconnect logic - #57

Draft
jprestwo wants to merge 3 commits into
locus-noetic-develfrom
fix-reconnect-logic
Draft

Fix reconnect logic#57
jprestwo wants to merge 3 commits into
locus-noetic-develfrom
fix-reconnect-logic

Conversation

@jprestwo

Copy link
Copy Markdown

No description provided.

jprestwo added 2 commits May 27, 2026 11:27
The reconnect logic was removed years ago:
#36

The motivation for this was to reduce the amount of time the system
was left in an unconnected state if a _publisher_ went down. This
is because a severed connection to the publisher caused subscribers
to attempt a reconnect within rospy but if that failed it would go
into a retry loop and backoff the retries up to 32 seconds thus
causing delays. The assumption was that when the publisher came back
it would cause the ROS master to send a publisherUpdate, and
subscribers would then reconnect immediately.

This logic is sound but it inadvertantly introduced a different
problem: What if the subscribers connection died but the publishers
did not? What happens here is the subscriber never attempts a
reconnect (waiting for publisherUpdate) but the publisherUpdate never
comes because the publisher is still up and working. This leads to
subscribers being left unconnected indefinitely.

In the mean time after pull 36 was merged we since pulled in
additional upstream changes which added another exception catch that
effectively did the same thing: break out of the loop and never
reconnect. This patch fixes both those cases and handles the
exception by starting a reconnect loop.
The reconnect logic was hard coded to wait a maximum of 32 seconds
which isn't aggressive enough in a lot of use cases. In addition
the time it took to call self.connect() was not factored into the
overall sleep time which could result in delays much longer than
32 seconds in the worst case.

This adds parameters to control the connect timeout as well as the
maximum timeout we wait before retrying. In addition the time spent
in self.connect() is now tracked and factored into the sleep so we
wait at most the configured amount of time as expected.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Reworks rospy's TCPROS subscriber reconnect behavior so that TransportTerminated and TransportException no longer terminate receive_loop, but instead tear down the socket and retry via _reconnect(). Adds configurable backoff/timeout via two new ROS parameters and accounts for connection-attempt time when sleeping between retries.

Changes:

  • Add _get_reconnect_config() with lazy cached lookup of /tcp_reconnect_max_backoff_sec and /tcp_reconnect_connect_timeout_sec (defaults 30s).
  • Make _reconnect() use the configurable connect timeout and cap exponential backoff at the configured max, subtracting the elapsed connection time from the sleep budget.
  • Add _reset_socket_for_reconnect() and change receive_loop() so TransportTerminated/TransportException reset the socket and continue (also call _reconnect() when self.socket is None).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +888 to +902
else:
self._reconnect()

except TransportTerminated as e:
logdebug("[%s] failed to receive incoming message : %s" % (self.name, str(e)))
rospydebug("[%s] failed to receive incoming message: %s" % (self.name, traceback.format_exc()))
break
# Treat hard transport termination the same as other transient
# transport failures so subscribers can auto-reconnect.
self._reset_socket_for_reconnect()
continue

except TransportException as e:
# A transport exception means the connection has been closed from the other side.
# Clean up our side.
self.close()
# Set socket to None so we reconnect.
self._reset_socket_for_reconnect()
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not clear on what happens if, e.g., we restart a robot. Now the port numbers it's using for a given topic are different, and so we effectively have a totally separate connection for that topic, right? Does this mean we'll forever retain the previous connection?

Comment on lines +859 to +871
def _reset_socket_for_reconnect(self):
"""Best-effort socket teardown while keeping transport alive for reconnect."""
try:
if self.socket is not None:
try:
self.socket.shutdown(socket.SHUT_RDWR)
except:
pass
finally:
self.socket.close()
except:
pass
self.socket = None
Comment on lines 842 to 843
except TransportInitError:
self.socket = None
Copilot brought up a good point that if the publisher goes down the
reconnect logic prevents reconnecting to the new publisher once its
back up. To handle this check if the publisher is still active before
reconnecting. If the publisher goes down we break out and avoid the
reconnect logic and handle the reconnect via publisherUpdate,
basically using the logic of the old commit removing reconnections.
if self.dest_address is None:
raise ROSInitException("internal error with reconnection state: address not stored")

reconnect_start = time.time()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any reason why we aren't using monotonic time here and below?


return False

def _reconnect(self):

@hwoithe hwoithe May 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This function (_reconnect) returns a value now and before it didn't. Can we add the docstring type hints?

Comment on lines +838 to +844
try:
s = xmlrpcclient.ServerProxy(self.endpoint_id)
code, msg, val = s.getPublications(rospy.names.get_name())
if code == 1:
return len([t for t in val if t[0] == self.name]) > 0
except Exception:
return False

@hwoithe hwoithe May 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does this connect to the remote endpoint and asks it if it's still publishing? If the subscribers connection has just failed because of bad network conditions then this query may fail right after (the network may still be down/bad). If that is the case, we will fail here and return False. That will cause us to bail out right away in the _reconnect function. The publisher may still be advertising but we fail to ask it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Won't we still just try again?

@hwoithe hwoithe May 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We would break out immediately from the _reconnect

            if not self._publisher_is_still_advertising():
                rospywarn("publisher for [%s] is no longer advertised; giving up reconnect", self.name)
                self.close()
                return False

That would then break out of receive_loop:

                    else:
                        if not self._reconnect():
                            break

Then we would have to wait for another publisher to unwedge it, no?
We are treating failure to verify the same as "it isn't publishing anymore."

@hwoithe
hwoithe self-requested a review May 27, 2026 21:38

@automatom-locus automatom-locus left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not sure I'm very qualified for reviewing this, but we do need to make sure a change of this magnitude doesn't go into 26.0.1, IMO. This could have fairly major ramifications, and I'd like more time to vet them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants