Skip to content

Commit d445111

Browse files
committed
fix: Paul's reviews
1 parent 03736c8 commit d445111

11 files changed

Lines changed: 88 additions & 82 deletions

File tree

dimos/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,10 @@
4949
# From https://github.com/lcm-proj/lcm.git
5050
LCM_MAX_CHANNEL_NAME_LENGTH = 63
5151

52+
# First path segment of Zenoh key expressions for DimOS module streams. Used by
53+
# ModuleCoordinator (dimos + LCM-style "/name") and ZenohPubSub.subscribe_all
54+
# (wildcard f"{ZENOH_DIMOS_KEY_PREFIX}/**"). Rerun bridge strips this prefix for entity paths.
55+
ZENOH_DIMOS_KEY_PREFIX = "dimos"
56+
5257
# Default timeout (seconds) for thread.join() during shutdown.
5358
DEFAULT_THREAD_JOIN_TIMEOUT = 2.0

dimos/core/coordination/module_coordinator.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import threading
2424
from typing import TYPE_CHECKING, Any, cast
2525

26+
from dimos.constants import ZENOH_DIMOS_KEY_PREFIX
2627
from dimos.core.coordination.rpyc_server import RpycServer
2728
from dimos.core.coordination.worker_manager import WorkerManager
2829
from dimos.core.coordination.worker_manager_docker import WorkerManagerDocker
@@ -557,7 +558,7 @@ def _get_transport_for(blueprint: Blueprint, name: str, stream_type: type) -> Pu
557558
)
558559
from dimos.core.transport import ZenohTransport, pZenohTransport
559560

560-
zenoh_topic = f"dimos{topic}"
561+
zenoh_topic = f"{ZENOH_DIMOS_KEY_PREFIX}{topic}"
561562
transport = (
562563
pZenohTransport(zenoh_topic)
563564
if use_pickled
@@ -634,7 +635,9 @@ def _run_configurators(blueprint: Blueprint) -> None:
634635
from dimos.protocol.service.system_configurator.base import configure_system
635636
from dimos.protocol.service.system_configurator.lcm_config import lcm_configurators
636637

637-
lcm_checks = lcm_configurators() if global_config.transport == "lcm" else []
638+
# LCM is still used outside merged module transports (CLI side channels, agents, tests).
639+
# OS multicast/buffer tuning applies whenever those paths run, not only when transport=lcm.
640+
lcm_checks = lcm_configurators()
638641
configurators = [*lcm_checks, *blueprint.configurator_checks]
639642

640643
try:

dimos/core/test_zenoh_transport.py

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,37 @@
1717
Tests the conditional logic added to support Zenoh alongside LCM:
1818
- GlobalConfig transport field
1919
- _get_transport_for() branching
20-
- LCM configurator gating
20+
- LCM system configurators always run with blueprint checks (LCM is used outside transport_map)
21+
22+
Requires the ``zenoh`` extra (``eclipse-zenoh``): ``ZenohTransport`` is only defined
23+
when that dependency is installed, so this module does not load without it.
2124
"""
2225

2326
from __future__ import annotations
2427

28+
import threading
2529
from typing import cast
2630

31+
import numpy as np
2732
from pydantic import ValidationError
2833
import pytest
2934

35+
from dimos.constants import ZENOH_DIMOS_KEY_PREFIX
3036
from dimos.core.coordination.blueprints import autoconnect
3137
from dimos.core.coordination.module_coordinator import _get_transport_for, _run_configurators
3238
from dimos.core.global_config import GlobalConfig, global_config
3339
from dimos.core.module import Module
3440
from dimos.core.stream import In, Out
3541
from dimos.core.test_utils import retry_until
36-
from dimos.core.transport import ZENOH_AVAILABLE, LCMTransport, pLCMTransport
42+
from dimos.core.transport import (
43+
ZENOH_AVAILABLE,
44+
LCMTransport,
45+
ZenohTransport,
46+
pLCMTransport,
47+
pZenohTransport,
48+
)
3749
from dimos.msgs.sensor_msgs.Image import Image
50+
from dimos.protocol.service.zenohservice import close_all_zenoh_sessions
3851

3952

4053
class TypedMsg:
@@ -102,10 +115,7 @@ class TestZenohAvailableGuard:
102115
def test_zenoh_available_is_bool(self) -> None:
103116
assert isinstance(ZENOH_AVAILABLE, bool)
104117

105-
@pytest.mark.skipif(not ZENOH_AVAILABLE, reason="zenoh not installed")
106-
def test_zenoh_transport_classes_exist_when_available(self) -> None:
107-
from dimos.core.transport import ZenohTransport, pZenohTransport
108-
118+
def test_zenoh_transport_classes_exist(self) -> None:
109119
assert ZenohTransport is not None
110120
assert pZenohTransport is not None
111121

@@ -128,33 +138,24 @@ def test_lcm_pickle_transport_returned_for_untyped_when_lcm(self, mocker) -> Non
128138
transport = _get_transport_for(bp, "untyped_data", UntypedMsg)
129139
assert isinstance(transport, pLCMTransport)
130140

131-
@pytest.mark.skipif(not ZENOH_AVAILABLE, reason="zenoh not installed")
132141
def test_zenoh_transport_returned_when_transport_is_zenoh(self, mocker) -> None:
133-
from dimos.core.transport import ZenohTransport
134-
135142
mocker.patch.object(global_config, "transport", "zenoh")
136143
bp = self._make_blueprint()
137144
transport = _get_transport_for(bp, "typed_data", TypedMsg)
138145
assert isinstance(transport, ZenohTransport)
139146

140-
@pytest.mark.skipif(not ZENOH_AVAILABLE, reason="zenoh not installed")
141147
def test_zenoh_pickle_transport_returned_for_untyped_when_zenoh(self, mocker) -> None:
142-
from dimos.core.transport import pZenohTransport
143-
144148
mocker.patch.object(global_config, "transport", "zenoh")
145149
bp = self._make_blueprint()
146150
transport = _get_transport_for(bp, "untyped_data", UntypedMsg)
147151
assert isinstance(transport, pZenohTransport)
148152

149-
@pytest.mark.skipif(not ZENOH_AVAILABLE, reason="zenoh not installed")
150153
def test_zenoh_topic_uses_dimos_prefix(self, mocker) -> None:
151-
from dimos.core.transport import pZenohTransport
152-
153154
mocker.patch.object(global_config, "transport", "zenoh")
154155
bp = self._make_blueprint()
155156
transport = _get_transport_for(bp, "untyped_data", UntypedMsg)
156157
assert isinstance(transport, pZenohTransport)
157-
assert "dimos/" in transport.topic
158+
assert f"{ZENOH_DIMOS_KEY_PREFIX}/" in transport.topic
158159

159160
def test_zenoh_raises_when_not_available(self, mocker) -> None:
160161
mocker.patch.object(global_config, "transport", "zenoh")
@@ -179,7 +180,7 @@ def test_lcm_configurators_run_when_transport_is_lcm(self, mocker) -> None:
179180

180181
mock_lcm_configs.assert_called_once()
181182

182-
def test_lcm_configurators_skipped_when_transport_is_zenoh(self, mocker) -> None:
183+
def test_lcm_configurators_run_when_transport_is_zenoh(self, mocker) -> None:
183184
mocker.patch.object(global_config, "transport", "zenoh")
184185
mock_lcm_configs = mocker.patch(
185186
"dimos.protocol.service.system_configurator.lcm_config.lcm_configurators",
@@ -190,30 +191,19 @@ def test_lcm_configurators_skipped_when_transport_is_zenoh(self, mocker) -> None
190191
bp = autoconnect(ProducerModule.blueprint(), ConsumerModule.blueprint())
191192
_run_configurators(bp)
192193

193-
mock_lcm_configs.assert_not_called()
194+
mock_lcm_configs.assert_called_once()
194195

195196

196-
@pytest.mark.skipif(not ZENOH_AVAILABLE, reason="zenoh not installed")
197197
class TestZenohTransportWrapper:
198198
"""Test ZenohTransport and pZenohTransport broadcast/subscribe lifecycle."""
199199

200200
@pytest.fixture(autouse=True)
201201
def _clean_sessions(self):
202-
from dimos.protocol.service.zenohservice import _sessions
203-
204202
yield
205-
for s in _sessions.values():
206-
s.close()
207-
_sessions.clear()
203+
close_all_zenoh_sessions()
208204

209205
def test_zenoh_transport_broadcast_and_subscribe(self) -> None:
210-
import threading
211-
212-
import numpy as np
213-
214-
from dimos.core.transport import ZenohTransport
215-
216-
t = ZenohTransport("dimos/test/transport", Image)
206+
t = ZenohTransport(f"{ZENOH_DIMOS_KEY_PREFIX}/test/transport", Image)
217207
t.start()
218208

219209
received = []
@@ -230,11 +220,7 @@ def cb(msg): # type: ignore[no-untyped-def]
230220
t.stop()
231221

232222
def test_pzenoh_transport_broadcast_and_subscribe(self) -> None:
233-
import threading
234-
235-
from dimos.core.transport import pZenohTransport
236-
237-
t = pZenohTransport("dimos/test/pickle_transport")
223+
t = pZenohTransport(f"{ZENOH_DIMOS_KEY_PREFIX}/test/pickle_transport")
238224
t.start()
239225

240226
received = []
@@ -250,18 +236,14 @@ def cb(msg): # type: ignore[no-untyped-def]
250236
t.stop()
251237

252238
def test_auto_start_on_broadcast(self) -> None:
253-
from dimos.core.transport import pZenohTransport
254-
255-
t = pZenohTransport("dimos/test/autostart")
239+
t = pZenohTransport(f"{ZENOH_DIMOS_KEY_PREFIX}/test/autostart")
256240
# Don't call start() — broadcast should auto-start
257241
t.broadcast(None, "test")
258242
assert t._started
259243
t.stop()
260244

261245
def test_stop_and_restart(self) -> None:
262-
from dimos.core.transport import pZenohTransport
263-
264-
t = pZenohTransport("dimos/test/restart")
246+
t = pZenohTransport(f"{ZENOH_DIMOS_KEY_PREFIX}/test/restart")
265247
t.start()
266248
assert t._started
267249
t.stop()

dimos/protocol/pubsub/impl/test_zenohpubsub.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,20 @@
2525
from dimos.core.test_utils import retry_until
2626
from dimos.protocol.pubsub.impl.lcmpubsub import Topic
2727
from dimos.protocol.pubsub.impl.zenohpubsub import ZenohPubSubBase
28-
from dimos.protocol.service.zenohservice import _sessions
28+
from dimos.protocol.service.zenohservice import close_all_zenoh_sessions
2929

3030

3131
@pytest.fixture()
3232
def pubsub():
3333
"""Create and start a ZenohPubSubBase instance, clean up after."""
3434
# Each test gets a fresh session to avoid thread leak detection
35-
for session in _sessions.values():
36-
session.close()
37-
_sessions.clear()
35+
close_all_zenoh_sessions()
3836

3937
ps = ZenohPubSubBase()
4038
ps.start()
4139
yield ps
4240
ps.stop()
43-
# Close sessions so Zenoh's internal threads are joined
44-
for session in _sessions.values():
45-
session.close()
46-
_sessions.clear()
41+
close_all_zenoh_sessions()
4742

4843

4944
class TestZenohPubSubBase:

dimos/protocol/pubsub/impl/zenohpubsub.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import threading
2525
from typing import TYPE_CHECKING, Any
2626

27+
from dimos.constants import ZENOH_DIMOS_KEY_PREFIX
2728
from dimos.protocol.pubsub.encoders import LCMEncoderMixin, PickleEncoderMixin
2829
from dimos.protocol.pubsub.impl.lcmpubsub import Topic
2930
from dimos.protocol.pubsub.spec import AllPubSub
@@ -151,10 +152,10 @@ def on_sample(sample: zenoh.Sample) -> None:
151152

152153
def unsubscribe() -> None:
153154
nonlocal undeclared
154-
if undeclared:
155-
return
156-
undeclared = True
157155
with self._subscriber_lock:
156+
if undeclared:
157+
return
158+
undeclared = True
158159
if sub not in self._subscribers:
159160
return # Already removed by stop() — stop() owns the undeclare
160161
self._subscribers.remove(sub)
@@ -164,7 +165,7 @@ def unsubscribe() -> None:
164165

165166
def subscribe_all(self, callback: Callable[[bytes, Topic], Any]) -> Callable[[], None]:
166167
"""Subscribe to all dimos key expressions via wildcard."""
167-
return self.subscribe(Topic("dimos/**"), callback)
168+
return self.subscribe(Topic(f"{ZENOH_DIMOS_KEY_PREFIX}/**"), callback)
168169

169170
def stop(self) -> None:
170171
"""Clean up publishers and subscribers."""

dimos/protocol/service/test_zenohservice.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,14 @@
2020

2121
pytest.importorskip("zenoh")
2222

23-
from dimos.protocol.service.zenohservice import ZenohConfig, ZenohService, _sessions
23+
from dimos.protocol.service.zenohservice import ZenohConfig, ZenohService, close_all_zenoh_sessions
2424

2525

2626
@pytest.fixture(autouse=True)
2727
def _clear_sessions():
2828
"""Clear the global session cache before each test."""
2929
yield
30-
# Close and remove all sessions after each test
31-
for session in _sessions.values():
32-
session.close()
33-
_sessions.clear()
30+
close_all_zenoh_sessions()
3431

3532

3633
class TestZenohConfig:

dimos/protocol/service/zenohservice.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import atexit
1920
import json
2021
import threading
2122
from typing import Any
@@ -33,6 +34,25 @@
3334
_sessions_lock = threading.Lock()
3435

3536

37+
def close_all_zenoh_sessions() -> None:
38+
"""Close and clear every cached session in this process.
39+
40+
Safe to call when no live publishers or subscribers still reference the
41+
sessions (call after module ``stop()``). Idempotent if the cache is empty.
42+
"""
43+
with _sessions_lock:
44+
to_close = list(_sessions.values())
45+
_sessions.clear()
46+
for session in to_close:
47+
try:
48+
session.close() # type: ignore[no-untyped-call]
49+
except Exception:
50+
logger.error("Error closing Zenoh session", exc_info=True)
51+
52+
53+
atexit.register(close_all_zenoh_sessions)
54+
55+
3656
class ZenohConfig(BaseConfig):
3757
"""Configuration for Zenoh service."""
3858

@@ -83,4 +103,5 @@ def session(self) -> zenoh.Session:
83103
__all__ = [
84104
"ZenohConfig",
85105
"ZenohService",
106+
"close_all_zenoh_sessions",
86107
]

dimos/visualization/rerun/bridge.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from toolz import pipe # type: ignore[import-untyped]
4040
import typer
4141

42+
from dimos.constants import ZENOH_DIMOS_KEY_PREFIX
4243
from dimos.core.core import rpc
4344
from dimos.core.global_config import global_config
4445
from dimos.core.module import Module, ModuleConfig
@@ -321,9 +322,10 @@ def _get_entity_path(self, topic: Any) -> str:
321322
if isinstance(raw, str):
322323
topic_str = raw
323324
topic_str = topic_str.split("#")[0]
324-
# Strip Zenoh key prefix (dimos/) to match LCM entity paths
325-
if topic_str.startswith("dimos/"):
326-
topic_str = "/" + topic_str.removeprefix("dimos/")
325+
# Strip Zenoh key root to match LCM-style entity paths.
326+
_prefix = f"{ZENOH_DIMOS_KEY_PREFIX}/"
327+
if topic_str.startswith(_prefix):
328+
topic_str = "/" + topic_str.removeprefix(_prefix)
327329
return f"{self.config.entity_prefix}{topic_str}"
328330

329331
def _on_message(self, msg: Any, topic: Any) -> None:

0 commit comments

Comments
 (0)