Skip to content

Lifecycle fixes#151

Open
gonzalocasas wants to merge 3 commits into
mainfrom
lifecycle-fixes
Open

Lifecycle fixes#151
gonzalocasas wants to merge 3 commits into
mainfrom
lifecycle-fixes

Conversation

@gonzalocasas
Copy link
Copy Markdown
Contributor

@gonzalocasas gonzalocasas commented May 30, 2026

I've seen roslibpy failing to connect when used to connect and disconnect multiple times (usually on CI during integration tests), so this PR tries to address that problem.

What type of change is this?

  • Bug fix in a backwards-compatible manner.
  • New feature in a backwards-compatible manner.
  • Breaking change: bug fix or new feature that involve incompatible API changes.
  • Other (e.g. doc update, configuration, etc)

Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code.

  • I added a line to the CHANGELOG.rst file in the Unreleased section under the most fitting heading (e.g. Added, Changed, Removed).
  • I ran all tests on my computer and it's all green (i.e. invoke test).
  • I ran lint on my computer and there are no errors (i.e. invoke check).
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added necessary documentation (if appropriate)

gonzalocasas and others added 3 commits May 30, 2026 12:50
TwistedEventLoopManager.__init__ instantiated PythonLoggingObserver and
started it; the observer was only stopped in manager.terminate(), which
in turn was only invoked by Ros.terminate() (which stops the reactor
permanently). Downstream code that follows the documented
RosClient.__exit__ -> Ros.close() pattern never called terminate(), so
every Ros instance added a permanent observer to twisted's
globalLogPublisher.

In long-running pytest sessions that create many short-lived Ros
instances (e.g. compas_fab's integration suite), this accumulated tens
of observers and slowed every twisted log emission linearly. It also
contributed to occasional RosTimeoutError flakes on the next
Ros.run() after many cycles.

Make the observer a process-wide singleton: TwistedEventLoopManager
lazily starts a single module-level PythonLoggingObserver via a thread-
safe initializer, and terminate() now stops it once and clears the
sentinel. Verified by a new tests/ros1/test_lifecycle.py case that
constructs 10 Ros instances and asserts at most one observer is added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously Ros.close() returned as soon as proto.send_close() had been
dispatched onto the reactor thread: it set wait_disconnect inside the
on_ready callback, before clientConnectionLost ever fired. Downstream
callers (compas_fab's RosClient.__exit__ -> Ros.close() -> immediately
construct a new Ros) could then race the previous socket's TCP teardown,
and on contended reactors the next Ros.run() occasionally tripped its
10s connection timeout.

close() now registers a one-shot listener on the factory's "close" event
(emitted from clientConnectionLost when the underlying TCP connection
has actually been lost) and only sets wait_disconnect from there. The
"closing" event still fires synchronously inside the on_ready callback
before send_close(), so handlers that depend on that ordering are
unaffected.

If the disconnect doesn't complete within the timeout the listener is
detached before raising RosTimeoutError, so a subsequent reconnect
cycle on the same factory won't leak a reference to this Ros instance.

Verified by a new test_close_blocks_until_disconnect case in
tests/ros1/test_lifecycle.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant