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
ezmsg.core.settingsmeta._sanitize checks isinstance(value, (bool, int, float, str))before it checks isinstance(value, enum.Enum). IntEnum and StrEnum members are instances of int/str, so they short-circuit on the first branch and are returned verbatim, un-sanitized.
Those values then travel to the graph server and out over the settings event / snapshot / metadata wires as pickled enum members. Any subscriber that does not have the defining package installed cannot pickle.loads them, and the whole subscription dies with ModuleNotFoundError.
Where
ezmsg/core/settingsmeta.py (v3.9.0, and current dev):
_sanitize is the sanitizer for everything settings-shaped that leaves the process, so the leak reaches all of these:
SettingsSnapshotValue.structured_value and .repr_value — backendprocess.py::_settings_snapshot_value
SettingsFieldMetadata.default and .choices — settings_schema_from_type, hence ComponentMetadata.settings_schema in GraphMetadata
and therefore SettingsChangedEvent on GraphContext.subscribe_settings_events(), GraphContext.settings_snapshot(), and the component metadata a client reads back from the graph server
Observed behaviour
Our experiment-control UI runs in its own venv and deliberately does not depend on the pipelines' acquisition stack. Its settings-event forwarder died on every reconnect:
File "ezmsg/core/graphcontext.py", line 581, in _subscribe_pickled_stream
value = pickle.loads(payload)
ModuleNotFoundError: No module named 'pycbsdk'
The setting in question is ezmsg.blackrock.CereLinkSignalSettings.subscribe_rate: SampleRate, where pycbsdk.session.SampleRate is an IntEnum.
Two things make this worse than a single dropped message:
It wedges permanently. The subscriber reconnects with after_seq=0, which replays the graph server's retained event history — including the event it cannot decode. So one un-decodable retained event kills settings observation for the whole life of the graph server, not for one message. _subscribe_pickled_stream has no per-payload recovery: a failed pickle.loads propagates out of the generator and tears down the subscription.
The information was never needed._sanitize's entire job is to reduce settings to JSON-safe scalars so that observers need none of the graph's dependencies. Reordering the two checks yields {'rate': 5, 'mode': 'a', 'plain': 'x'} — a pycbsdk-free payload with no loss of anything a consumer uses (json.dumps already renders an IntEnum as its integer, so rendered output is unchanged).
Suggested fix
Move the enum.Enum check above the primitives check:
This is generic — it covers IntEnum, StrEnum, IntFlag, and any other mixin-enum from any dependency, not just this one.
Happy to open a PR with the change plus a regression test asserting _sanitize leaves no non-builtin types in its output for an IntEnum/StrEnum-valued Settings.
Possibly worth considering separately
_subscribe_pickled_stream could survive a bad payload — log and skip rather than terminate the subscription. Even with the sanitizer fixed, the wire is raw pickle, so the next accidental leak reproduces the same permanent wedge in a client that is doing nothing wrong.
These wires arguably need not be pickle at all. After this fix, SettingsChangedEvent / TopologyChangedEvent / ProfilingTraceStreamBatch contain only JSON-representable data plus UUID, enum, tuple and one opaque bytes field (SettingsSnapshotValue.serialized, which nothing in the tree ever unpickles — ezmsg-dashboard only reports serialized is not None). A JSON envelope would make the cross-venv contract structural instead of conventional. Related: GraphServer pickles snapshots under the global command lock, stalling a starting graph #257.
Summary
ezmsg.core.settingsmeta._sanitizechecksisinstance(value, (bool, int, float, str))before it checksisinstance(value, enum.Enum).IntEnumandStrEnummembers are instances ofint/str, so they short-circuit on the first branch and are returned verbatim, un-sanitized.Those values then travel to the graph server and out over the settings event / snapshot / metadata wires as pickled enum members. Any subscriber that does not have the defining package installed cannot
pickle.loadsthem, and the whole subscription dies withModuleNotFoundError.Where
ezmsg/core/settingsmeta.py(v3.9.0, and currentdev):Reproduction
Affected wires
_sanitizeis the sanitizer for everything settings-shaped that leaves the process, so the leak reaches all of these:SettingsSnapshotValue.structured_valueand.repr_value—backendprocess.py::_settings_snapshot_valueSettingsFieldMetadata.defaultand.choices—settings_schema_from_type, henceComponentMetadata.settings_schemainGraphMetadataSettingsChangedEventonGraphContext.subscribe_settings_events(),GraphContext.settings_snapshot(), and the component metadata a client reads back from the graph serverObserved behaviour
Our experiment-control UI runs in its own venv and deliberately does not depend on the pipelines' acquisition stack. Its settings-event forwarder died on every reconnect:
The setting in question is
ezmsg.blackrock.CereLinkSignalSettings.subscribe_rate: SampleRate, wherepycbsdk.session.SampleRateis anIntEnum.Two things make this worse than a single dropped message:
after_seq=0, which replays the graph server's retained event history — including the event it cannot decode. So one un-decodable retained event kills settings observation for the whole life of the graph server, not for one message._subscribe_pickled_streamhas no per-payload recovery: a failedpickle.loadspropagates out of the generator and tears down the subscription._sanitize's entire job is to reduce settings to JSON-safe scalars so that observers need none of the graph's dependencies. Reordering the two checks yields{'rate': 5, 'mode': 'a', 'plain': 'x'}— a pycbsdk-free payload with no loss of anything a consumer uses (json.dumpsalready renders anIntEnumas its integer, so rendered output is unchanged).Suggested fix
Move the
enum.Enumcheck above the primitives check:This is generic — it covers
IntEnum,StrEnum,IntFlag, and any other mixin-enum from any dependency, not just this one.Happy to open a PR with the change plus a regression test asserting
_sanitizeleaves no non-builtin types in its output for anIntEnum/StrEnum-valuedSettings.Possibly worth considering separately
_subscribe_pickled_streamcould survive a bad payload — log and skip rather than terminate the subscription. Even with the sanitizer fixed, the wire is raw pickle, so the next accidental leak reproduces the same permanent wedge in a client that is doing nothing wrong.SettingsChangedEvent/TopologyChangedEvent/ProfilingTraceStreamBatchcontain only JSON-representable data plusUUID,enum,tupleand one opaquebytesfield (SettingsSnapshotValue.serialized, which nothing in the tree ever unpickles —ezmsg-dashboardonly reportsserialized is not None). A JSON envelope would make the cross-venv contract structural instead of conventional. Related: GraphServer pickles snapshots under the global command lock, stalling a starting graph #257.