Found live, against the real test server on saresb-cons-05: a fresh
full-namespace recording only attached to 18 of 13366 channels, yet the
report showed thousands of channels already "with data" - a real epics.PV
is not hashable (no __hash__), so it can never be a dict key, weak or
otherwise. _auto_monitor_refs[self.pv]/.get(self.pv) raised `TypeError:
unhashable type: 'PV'` on every real PV - but only *after*
pv.add_callback() had already registered the actual callback (which was
already collecting real data). RecordingSession's per-channel
`except Exception` then counted the TypeError as an ordinary attach
failure, and since the exception meant self._monitors[name] was never set
either, stop() never reached that callback - a leaked, permanently-running
subscription per channel, for nearly every channel.
The unit tests didn't catch this because the fake PV they use is a plain
object, hashable by identity by default - nothing about it exercised the
real constraint. Fixed by keying _auto_monitor_refs on id(self.pv) instead
(a plain dict, not weak: safe here because every CallbackEpics using a
given PV holds a live strong reference to it for as long as its entry
exists, and the entry is removed the moment the count reaches zero, so an
id() already in the dict is never a stale/reused one) - the actual pyepics
PV was never the problem, WeakKeyDictionary's hashability requirement was.
Also made FakePV explicitly unhashable (__hash__ = None) so the test suite
exercises this constraint for real going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>