Skip to content

Zenoh is the default transport, pinned to localhost - #3617

Open
leshy wants to merge 10 commits into
mainfrom
ivan/feat/zenoh-default
Open

Zenoh is the default transport, pinned to localhost#3617
leshy wants to merge 10 commits into
mainfrom
ivan/feat/zenoh-default

Conversation

@leshy

@leshy leshy commented Aug 22, 2026

Copy link
Copy Markdown
Member

dimos/core/transport_factory.py will need a rewrite, we need dimos normalized qos, would be nice to start removing pickling and defining custom lcm_msg everywhere, but will do this later.

rpc also needs conversion to json

Every platform now defaults to zenoh; --transport=lcm is the opt-out.

A stock peer used to listen on tcp/*:<ephemeral>, reachable from the LAN,
even though its multicast scouting was already loopback-only. The listener
now follows the same rule the scouting interface does: pinned to
tcp/127.0.0.1:0 while discovery never leaves loopback, handed back to
zenoh's default the moment scouting is turned outward. Dialing out is
untouched, so --robot-ip still reaches a robot. A client has no listener
to pin, and a router still has to name its own.

Zenoh also needed the isolation LCM has had: conftest gives every xdist
worker its own bus so sibling workers can't leak into each other, and
zenoh's loopback discovery group was shared by all of them. ZENOH_SCOUT_ADDR
moves each worker onto its own group, which is a session setting like any
other -- so it rides the wire to native modules too, and the Rust side
parses and applies it (deny_unknown_fields would have refused the launch
otherwise).
@mintlify

mintlify Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
dimensional 🟢 Ready View Preview Aug 22, 2026, 11:51 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

leshy added 4 commits August 23, 2026 03:07
A module travels to its worker process by pickle, and getattr on it hands
back a bound method -- which drags the whole instance, transports and RPC
service included. LCMService has always shed its runtime handles for this;
the zenoh classes never did, so the worker died on `cannot pickle
'builtins.Session'` the moment zenoh became the default. ZenohService,
ZenohPubSubBase and ZenohRPC now drop the same kind of state (session,
publishers, subscribers, queryables, call pool, locks) and rebuild it on
the far side.

The reason that took so long to see: _worker_loop caught BrokenPipeError
and EOFError around conn.send, so a TypeError there killed the worker
mid-send and left the coordinator blocked on a pipe nobody would answer.
An unsendable response is now reported to the caller instead.

Two fixes to the commit before this one. ZENOH_SCOUT_ADDR was set with
setdefault, but the pytest controller runs conftest first and exports it,
so every worker inherited one group and the isolation never fired -- the
neighbouring LCM and MCP lines assign, and so does this one now. And the
thread monitor flagged zenoh's pyo3-closure threads: they belong to a
pooled session that outlives the test on purpose, since sharing one
session is the point of the pool and closing it per test would cut
module-scoped fixtures off from their transports.
These tests each pinned one backend while the code under test followed the
configured one, so the two halves landed on different buses the moment the
default moved.

The async-module fixtures built pLCMTransport by hand, published into it,
and waited on a queue their zenoh-side modules never fed; they use the
transport factory now, same as production. test_remapping and
test_fleet_blueprint asserted LCM-shaped topic strings, which are only
correct for one backend -- transport_topic spells them for whichever is
active. test_autoconnect and the webrtc raw-pin test are about pinning
itself, so their pins now sit on the active backend, where the backend
switch leaves them alone and the tests keep meaning something either way.
A native module's arg is the transport's channel, so the factory produces
the expectation rather than a literal.

e2e_tests is pinned to LCM instead: LcmSpy sniffs the raw LCM bus and
observes nothing at all under zenoh, so the package cannot be ported
without a zenoh spy. The env var reaches the dimos CLI these tests spawn.
The session-wide ZENOH_SCOUT_ADDR export leaked into "stock default"
configs; the wire goldens had to dodge it by hand. Pin it like every
other zenoh field.
Two stale claims said TF stayed on LCM; the bridge listens only on the
configured backend (bridge.py), dual-listen is an explicit override.
@leshy
leshy force-pushed the ivan/feat/zenoh-default branch from d477c7f to 0e2265f Compare August 23, 2026 00:09
@leshy leshy changed the title Ivan/feat/zenoh default Zenoh is the default transport, pinned to localhost Aug 23, 2026
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.50000% with 29 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
dimos/protocol/pubsub/impl/zenohpubsub.py 16.66% 10 Missing ⚠️
dimos/visualization/rerun/websocket_server.py 70.00% 7 Missing and 2 partials ⚠️
dimos/core/coordination/python_worker.py 0.00% 6 Missing ⚠️
dimos/cli/bake/test_bake_e2e.py 0.00% 3 Missing ⚠️
dimos/conftest.py 85.71% 1 Missing ⚠️
@@            Coverage Diff             @@
##             main    #3617      +/-   ##
==========================================
- Coverage   77.26%   77.25%   -0.02%     
==========================================
  Files        1262     1262              
  Lines      120302   120423     +121     
  Branches    10600    10607       +7     
==========================================
+ Hits        92953    93030      +77     
- Misses      24284    24330      +46     
+ Partials     3065     3063       -2     
Flag Coverage Δ
OS-ubuntu-24.04-arm 72.15% <85.50%> (-0.01%) ⬇️
OS-ubuntu-latest 74.05% <85.50%> (-0.01%) ⬇️
Py-3.10 ?
Py-3.11 74.05% <85.50%> (-0.01%) ⬇️
Py-3.12 74.05% <85.50%> (-0.01%) ⬇️
Py-3.13 ?
Py-3.14 74.05% <85.50%> (-0.01%) ⬇️
Py-3.14t ?
SelfHosted-Large 30.06% <37.50%> (+0.04%) ⬆️
SelfHosted-Linux 35.14% <37.00%> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
dimos/core/conftest.py 100.00% <100.00%> (ø)
dimos/core/coordination/test_module_coordinator.py 98.37% <100.00%> (+<0.01%) ⬆️
dimos/core/coordination/test_namespace.py 100.00% <100.00%> (ø)
dimos/core/global_config.py 85.85% <100.00%> (-0.55%) ⬇️
...s/core/test_async_module_dispatch_serialization.py 97.56% <100.00%> (ø)
dimos/core/test_async_module_handles.py 100.00% <100.00%> (ø)
dimos/core/test_async_module_main.py 98.75% <100.00%> (ø)
dimos/core/test_async_module_process_observable.py 100.00% <100.00%> (ø)
dimos/core/test_async_module_rpc.py 100.00% <100.00%> (ø)
dimos/core/test_async_module_rpc_sync_to_async.py 100.00% <100.00%> (ø)
... and 17 more

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

When the default flipped to zenoh, these tests followed it and LCM
silently lost its only module/coordinator/RPC coverage. An each_transport
fixture pins the backend per param; the bus-coupled fixtures depend on
it, the in-process main() tests stay single.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Zenoh now defaults to localhost-scoped discovery while preserving local communication and explicitly configured robot connections. The WebSocket server reports restart as unsupported, but stopping it while a viewer remains connected can leave the listening port bound and prevent a replacement server from starting. The earlier report that restart uses a closed loop is no longer present: the exercised restart path raises the documented RuntimeError instead.

Confidence Score: 4/5

Do not merge until WebSocket shutdown reliably releases its listening socket when a viewer is connected.

A connected viewer can leave the configured WebSocket port unavailable after shutdown, blocking a replacement viewer service from binding it.

Files Needing Attention: dimos/visualization/rerun/websocket_server.py

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for a posted P1 finding and linked it to the corresponding review comment.
  • T-Rex ran a focused websocket lifecycle probe to support the P1 finding and captured both the probe source and its observed output.
  • T-Rex exercised a real two-process Zenoh harness with loopback discovery and a configured direct TCP endpoint, verifying local-message on the default setup and direct-message on the direct-endpoint setup; the focused configuration and wire tests completed successfully.
  • T-Rex documented Zenoh test progression, noting an initial pre-run state and the final result where all 26 focused tests passed.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (4): Last reviewed commit: "rerun: a stopped websocket server says s..." | Re-trigger Greptile

leshy added 2 commits August 23, 2026 03:36
A session's callback threads are non-daemon, so any script that touched
the default pool hung past its last line (the executable docs timed out
at 120s). atexit can't help -- interpreter shutdown joins those threads
first -- so the pool close rides threading's own shutdown hook, which
runs before the join. Default pool only; an explicit pool has an owner.

The refreshed doc results show the zenoh-era config output.
loopback_session() predates scout_addr, and the rust side deliberately
has no defaults, so the host rejected its launch config and exited
before listening. The listener wait now surfaces the host's stderr in
the assertion, which is the diagnostic CI was missing.
stop() raced its own teardown: Module.stop() killed the event loop
before the serve coroutine closed the listening socket, leaking the
bound port for the life of the process. Zenoh's stop timing loses that
race reliably (LCM won it, which is why main never saw this). The next
test in the same xdist worker then hit EADDRINUSE -- swallowed in a
never-checked future -- and start() waited on _server_ready without a
timeout until pytest-timeout shot the worker: 'node down' in CI.

stop() now waits for the serve future so the port is free on return,
start() surfaces the bind error and bounds the wait, and the tests bind
port 0 and read back bound_port, retiring the shared hardcoded ports.
Comment thread dimos/visualization/rerun/websocket_server.py Outdated
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Aug 23, 2026
Restart was never a module lifecycle -- the loop dies with stop() -- but
a second start() failed on a bare assert while stale state (_server_ready,
bound_port, client_connected) kept claiming a listener existed. Now stop()
clears them and start() on a stopped server raises with words.
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Aug 23, 2026
Comment thread dimos/visualization/rerun/websocket_server.py
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Required CI checks have passed on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant