You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
homie: snapshot _nodes/_children before iteration to fix MQTT-thread race (SDK-e3k)
publish_nodes() and refresh_tree() were iterating self._nodes /
self._children live while the main thread mutated them via add_node() /
Device(parent=...) construction. On a SPAN G2 panel this crashed the
MQTT loop thread on initial broker connect:
File ".../ebus_sdk/homie.py", line 1501, in publish_nodes
for node in self._nodes.values():
RuntimeError: dictionary changed size during iteration
systemd then SIGKILLed the unresponsive process; the restart recovered.
Both call sites now iterate through a list() snapshot, matching the
defensive pattern already in place in delete_all_from_mqtt() (line
1246) and delete() (line 1305). Adds two deterministic regression
tests that fail on the unfixed code and pass with the snapshot.
refresh_tree() doesn't actually crash without the fix (Python lists
don't raise on mutation-during-iteration the way dicts do), but
CPython's list iterator would pull a half-constructed child appended
mid-cascade into the current reconnect republish — so the snapshot is
the correct semantics either way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+9-1Lines changed: 9 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,13 @@ All notable changes to `ebus-sdk` are recorded here. Format follows [Keep a Chan
4
4
5
5
## [Unreleased]
6
6
7
+
## [0.2.2] — 2026-06-12
8
+
9
+
### Fixed
10
+
11
+
-`Device.publish_nodes()` no longer crashes with `RuntimeError: dictionary changed size during iteration` when the main thread adds a node while the MQTT loop thread is publishing on initial broker connect. Iteration now goes through a `list(self._nodes.values())` snapshot — matching the defensive pattern already used in `delete_all_from_mqtt()`. Hit on a SPAN G2 panel immediately after deploy; systemd subsequently SIGKILLed the unresponsive process, restart recovered.
12
+
-`Device.refresh_tree()` similarly snapshots `self._children` before recursing. Lists don't raise on mutation-during-iteration, but CPython's list iterator would otherwise pull a half-constructed child added mid-cascade into the current reconnect republish — matching the defensive pattern in `Device.delete()`.
13
+
7
14
## [0.2.1] — 2026-06-12
8
15
9
16
### Added
@@ -53,7 +60,8 @@ The 0.2.0 release introduces first-class parent/child device trees on both the d
53
60
54
61
Initial public release on PyPI. See `git log v0.1.2` for the surface that shipped.
0 commit comments