Skip to content

Commit da7b14c

Browse files
b-Tomasclaudeleandropineda
authored
Stop publishing system stats for offline robots (#91)
* Stop publishing system stats for offline robots The framework publishes system stats for every robot on every execution loop iteration, including robots the connector reports as offline. InOrbit answers those stats with a get_state request, the online status callback replies "offline", and the retained status message refreshes the robot's offline timestamp -- so an offline robot's offline_ts keeps moving forward for as long as the connector runs. Gate the publish on _is_fleet_robot_online(robot_id) inside __publish_pending_system_stats, where the stored-stats and default-values paths converge, so stats explicitly stored via publish_robot_system_stats() are dropped too rather than only the framework defaults. Nothing is lost by skipping: the forced state request only helps when the robot is online but InOrbit believes otherwise. The online check is subclass code and now runs once per robot per loop iteration instead of only on get_state, so it falls back to publishing if it raises (matching the edge-sdk's own get_state fallback) and the docstrings now require it to be cheap and non-blocking. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Update inorbit_connector/connector.py Co-authored-by: Leandro <leandropineda.lp@gmail.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Leandro <leandropineda.lp@gmail.com>
1 parent 4d918b2 commit da7b14c

6 files changed

Lines changed: 101 additions & 16 deletions

File tree

docs/contents/publishing.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,13 @@ publish_system_stats(**kwargs) -> None
123123
publish_robot_system_stats(robot_id: str, **kwargs) -> None
124124
```
125125

126-
Stores system stats (CPU, RAM, disk usage) to be published at the end of the execution loop. If no stats are stored for a robot during the loop iteration, default values are published automatically.
126+
Stores system stats (CPU, RAM, disk usage) to be published at the end of the execution loop. If no stats are stored for a robot during the loop iteration, default values are published automatically. Nothing is published for a robot whose online check reports it offline (see below).
127127

128128
All percentage values should be floats between 0.0 and 1.0 (e.g., 0.45 for 45%).
129129

130-
This ensures that system stats are always published for all robots in the fleet, even if the connector does not explicitly provide values. This is to ensure stability of the online status of the robot in the UI, as it forces state requests if the robot was to appear offline.
130+
This ensures that system stats are published for every online robot in the fleet, even if the connector does not explicitly provide values. This is to ensure stability of the online status of the robot in the UI, as it forces state requests if the robot was to appear offline.
131+
132+
Robots reported offline by `_is_robot_online()` (single-robot) / `_is_fleet_robot_online(robot_id)` (fleet) are skipped, as otherwise their state would be interpreted as online.
131133

132134
By default, zeroed values are used. To use the connector host's actual system stats as defaults, set `publish_connector_system_stats=True` when initializing the connector. See [FleetConnector constructor](specification/connector#spec-connector-fleetconnector-constructor) for details.
133135

docs/contents/specification/connector.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Return `None` if the map can’t be fetched.
9797

9898
The Edge SDK uses this callback to determine if a robot should be considered online. It is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. Default implementation returns `True`.
9999

100+
The framework also calls it once per robot on every execution loop iteration, to decide whether to publish system stats at all: while it returns `False`, no system stats are published for that robot, keeping the robot offline in InOrbit. Keep the implementation cheap and non-blocking (read cached state; no network or other blocking I/O), or the connector's event loop will stall. If it raises, the framework logs a warning and publishes anyway.
101+
100102
<a id="spec-connector-fleetconnector-lifecycle"></a>
101103
### `start()` / `join()` / `stop()`
102104

@@ -148,6 +150,8 @@ Publishes pose for one robot. If the `frame_id` differs from the last published
148150

149151
If no stats are stored for a robot during the loop iteration, default values are published automatically. By default, zeroed values are used. To use the connector host's actual system stats as defaults, set `publish_connector_system_stats=True` in the [constructor](#spec-connector-fleetconnector-constructor).
150152

153+
Stats stored for a robot whose [`_is_fleet_robot_online()`](#spec-connector-fleetconnector-is-online) returns `False` are dropped rather than published.
154+
151155
If immediate publishing is required, use `_get_robot_session(robot_id)` to access the underlying `RobotSession` and call `publish_system_stats()` directly.
152156

153157
<a id="spec-connector-fleetconnector-get-robot-session"></a>
@@ -189,7 +193,7 @@ Single-robot convenience for map fetching. The framework uses it by delegating `
189193

190194
**Optional override.**
191195

192-
Single-robot convenience for online status. The fleet-level online check delegates to this method. Called when InOrbit requests state due to a discrepancy between the robot's offline status and incoming system stats.
196+
Single-robot convenience for online status. The fleet-level online check delegates to this method. Called when InOrbit requests state due to a discrepancy between the robot's offline status and incoming system stats, and once per execution loop iteration to decide whether to publish system stats at all. See [`_is_fleet_robot_online()`](#spec-connector-fleetconnector-is-online) for the constraints that puts on the implementation.
193197

194198
<a id="spec-connector-connector-publishing"></a>
195199
### Publishing wrappers
@@ -207,5 +211,3 @@ Single-robot convenience for online status. The fleet-level online check delegat
207211
**Callable (advanced).**
208212

209213
Returns the underlying Edge SDK session for the current robot.
210-
211-

docs/contents/usage/fleet.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ All publishing methods require a `robot_id` parameter. See the [Publishing Guide
111111
- `publish_robot_pose(robot_id, x, y, yaw, frame_id)`: Publish pose for a specific robot
112112
- `publish_robot_odometry(robot_id, **kwargs)`: Publish odometry for a specific robot
113113
- `publish_robot_key_values(robot_id, **kwargs)`: Publish key-values for a specific robot
114-
- `publish_robot_system_stats(robot_id, **kwargs)`: Defer publishing of system stats for a specific robot; defaults are published if not called
114+
- `publish_robot_system_stats(robot_id, **kwargs)`: Defer publishing of system stats for a specific robot; defaults are published if not called, and nothing is published while the robot is offline
115115
- `publish_robot_map(robot_id, frame_id, is_update=False)`: Publish map for a specific robot
116116

117117
## Background tasks
@@ -178,7 +178,9 @@ def _is_fleet_robot_online(self, robot_id: str) -> bool:
178178
return self._fleet_manager.is_robot_online(robot_id)
179179
```
180180

181-
This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. The connector framework always publishes system stats for all robots (even zeroed defaults), ensuring that any online/offline discrepancy is detected and corrected.
181+
This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received. The connector framework publishes system stats for every online robot, ensuring that any online/offline discrepancy is detected and corrected.
182+
183+
It is also called once per robot on every execution loop iteration, to decide whether to publish system stats at all. While it returns `False`, no system stats are published for that robot, keeping the robot offline in InOrbit. Keep it cheap and non-blocking (read cached state; no API call in the hot path), or the connector's event loop will stall.
182184

183185
## Example Execution Loop
184186

docs/contents/usage/single-robot.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ async def _execution_loop(self) -> None:
180180

181181
Override this method to provide custom robot health checks. The default implementation assumes the robot is online if the connector is running. This callback is invoked when InOrbit sends a `get_state` request, which happens automatically when the robot is marked as offline but system stats are still being received.
182182

183+
It is also called on every execution loop iteration, to decide whether to publish system stats at all. While it returns `False`, no system stats are published, keeping the robot offline in InOrbit. Keep it cheap and non-blocking (read cached state; no API call in the hot path), or the connector's event loop will stall.
184+
183185
```python
184186
def _is_robot_online(self) -> bool:
185187
"""Check if the robot is online.
@@ -216,4 +218,3 @@ Scripts are automatically registered and can be executed from InOrbit.
216218
- **Simple connector**: [examples/simple-connector/connector.py](https://github.com/inorbit-ai/inorbit-connector-python/blob/main/examples/simple-connector/connector.py)
217219
- **Robot connector (CLI)**: [examples/robot-connector/](https://github.com/inorbit-ai/inorbit-connector-python/tree/main/examples/robot-connector)
218220
- **Examples index**: [examples/README.md](https://github.com/inorbit-ai/inorbit-connector-python/blob/main/examples/README.md)
219-

inorbit_connector/connector.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,9 @@ def publish_robot_system_stats(self, robot_id: str, **kwargs) -> None:
10731073
System stats are stored and published after the execution loop completes. If no
10741074
stats are stored for a robot, default zeroed values are published instead.
10751075
1076+
Stats stored for a robot whose _is_fleet_robot_online() returns False are
1077+
dropped rather than published. @see __publish_pending_system_stats.
1078+
10761079
Note:
10771080
If immediate publishing is required, use `_get_robot_session(robot_id)` to
10781081
access the underlying RobotSession and call `publish_system_stats()`
@@ -1104,15 +1107,22 @@ def __publish_pending_system_stats(self) -> None:
11041107
11051108
This method is called automatically at the end of each execution loop iteration.
11061109
For each robot in the fleet:
1107-
- If system stats were stored via publish_robot_system_stats(), those are
1108-
published
1110+
- If _is_fleet_robot_online(robot_id) returns False, nothing is published for
1111+
that robot and any stats stored for it are dropped
1112+
- Else if system stats were stored via publish_robot_system_stats(), those are
1113+
published
11091114
- Otherwise, default values are published (connector host stats if
11101115
publish_connector_system_stats is enabled, zeroed values otherwise).
11111116
11121117
The reason publishing system stats is deferred is to ensure at least one system
11131118
stats message is published for each robot, even if the connector does not
11141119
explicitly provide values. This ensures stability of the online status of the
11151120
robot in the UI, as it forces state requests if the robot was to appear offline.
1121+
1122+
Offline robots are skipped because that forced state request is answered with
1123+
the robot's offline status, which refreshes its offline timestamp in InOrbit on
1124+
every loop iteration. Nothing is lost: the state request only helps when the
1125+
robot is online but InOrbit believes otherwise.
11161126
"""
11171127
default_values = (
11181128
self.__get_connector_system_stats()
@@ -1130,10 +1140,22 @@ def __publish_pending_system_stats(self) -> None:
11301140
self.__pending_system_stats = {}
11311141

11321142
for robot_id, session in sessions.items():
1133-
if pending_status := pending.get(robot_id):
1134-
session.publish_system_stats(**pending_status)
1135-
else:
1136-
session.publish_system_stats(**default_values)
1143+
try:
1144+
online = self._is_fleet_robot_online(robot_id)
1145+
except Exception as e:
1146+
# Match the edge-sdk's get_state fallback: assume online on error, so
1147+
# a broken health check keeps publishing instead of muting the robot.
1148+
self._logger.warning(f"Online check failed for '{robot_id}': {e}")
1149+
online = True
1150+
if not online:
1151+
# Stats for an offline robot make InOrbit request state, and the
1152+
# offline reply refreshes the robot's offline timestamp on every
1153+
# loop iteration. Any stats stored for it are dropped.
1154+
self._logger.debug(
1155+
f"Skipping system stats publish for '{robot_id}': robot is offline"
1156+
)
1157+
continue
1158+
session.publish_system_stats(**(pending.get(robot_id) or default_values))
11371159

11381160
# Methods meant to be extended by subclasses
11391161
@abstractmethod
@@ -1210,7 +1232,11 @@ def _is_fleet_robot_online(self, robot_id: str) -> bool:
12101232
checks (e.g., API connectivity, robot state, etc.).
12111233
12121234
NOTE: State will automatically be requested from InOrbit if the robot is marked
1213-
as offline but system stats are sent.
1235+
as offline but system stats are sent. Because of that, the framework also calls
1236+
this method once per robot on every execution loop iteration to decide whether
1237+
to publish system stats at all (see __publish_pending_system_stats). Keep the
1238+
implementation cheap and non-blocking: read cached state, do not perform
1239+
network or other blocking I/O here, or the connector's event loop will stall.
12141240
12151241
Args:
12161242
robot_id (str): The robot ID to check
@@ -1303,7 +1329,12 @@ def _is_robot_online(self) -> bool:
13031329
health checks (e.g., API connectivity, robot state, etc.).
13041330
13051331
NOTE: State will automatically be requested from InOrbit if the robot is marked
1306-
as offline but system stats are sent.
1332+
as offline but system stats are sent. Because of that, the framework also calls
1333+
this method on every execution loop iteration to decide whether to publish
1334+
system stats at all: while it returns False, no system stats are published for
1335+
the robot, so its offline timestamp in InOrbit stops being refreshed. Keep the
1336+
implementation cheap and non-blocking: read cached state, do not perform
1337+
network or other blocking I/O here, or the connector's event loop will stall.
13071338
13081339
Returns:
13091340
bool: True if robot is online, False otherwise.

tests/test_connector.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,53 @@ def test_publish_pending_system_stats_mixed_stored_and_default(
736736
hdd_usage_percentage=0.0,
737737
)
738738

739+
def test_publish_pending_system_stats_skips_offline_robots(
740+
self, fleet_connector, mock_robot_session_pool
741+
):
742+
"""Test that nothing is published for robots reported offline."""
743+
# TestRobot2 is offline: publishing stats for it would make InOrbit request
744+
# state and refresh its offline timestamp on every loop iteration.
745+
fleet_connector._is_fleet_robot_online = lambda robot_id: (
746+
robot_id != "TestRobot2"
747+
)
748+
# Stored stats must be dropped too, not just the defaults.
749+
fleet_connector.publish_robot_system_stats(
750+
"TestRobot2", cpu_load_percentage=0.5
751+
)
752+
753+
fleet_connector._FleetConnector__publish_pending_system_stats()
754+
755+
session1 = fleet_connector._get_robot_session("TestRobot1")
756+
session2 = fleet_connector._get_robot_session("TestRobot2")
757+
session1.publish_system_stats.assert_called_once_with(
758+
cpu_load_percentage=0.0,
759+
ram_usage_percentage=0.0,
760+
hdd_usage_percentage=0.0,
761+
)
762+
session2.publish_system_stats.assert_not_called()
763+
assert len(fleet_connector._FleetConnector__pending_system_stats) == 0
764+
765+
def test_publish_pending_system_stats_publishes_when_online_check_fails(
766+
self, fleet_connector, mock_robot_session_pool
767+
):
768+
"""Test that a raising online check falls back to publishing."""
769+
770+
def boom(robot_id):
771+
raise RuntimeError("health check exploded")
772+
773+
fleet_connector._is_fleet_robot_online = boom
774+
775+
fleet_connector._FleetConnector__publish_pending_system_stats()
776+
777+
for robot_id in ("TestRobot1", "TestRobot2"):
778+
fleet_connector._get_robot_session(
779+
robot_id
780+
).publish_system_stats.assert_called_once_with(
781+
cpu_load_percentage=0.0,
782+
ram_usage_percentage=0.0,
783+
hdd_usage_percentage=0.0,
784+
)
785+
739786
def test_publish_connector_system_stats_uses_psutil(
740787
self, base_model, mock_robot_session_pool
741788
):

0 commit comments

Comments
 (0)