Summary
Multiple module globals are read/written without locks:
1. _handle race
_init.py:78, 225 reads/writes _handle without a lock. Two threads racing init() can both pass the if _handle is not None guard, both call install(), and orphan the first thread's transport + aggregator (the background timer thread leaks).
2. install/uninstall vs. init/dispose desynchronization
Independent of (1), the patches and the callback registration can desynchronize. Thread A in dispose() is mid-_unpatch_* while Thread B in init() calls install(on_event) and sees _installed=True (set by A but not yet cleared). B short-circuits — the new init has no patches.
Fix
Guard init(), dispose(), install(), and uninstall() with a single module-level threading.RLock. Add tests that race both pairs.
Files
recost/_init.py
recost/_interceptor.py
tests/test_init.py
Priority
P1 — produces a silently-broken SDK ("ingest is no-op") in production restart scenarios.
Summary
Multiple module globals are read/written without locks:
1.
_handlerace_init.py:78, 225reads/writes_handlewithout a lock. Two threads racinginit()can both pass theif _handle is not Noneguard, both callinstall(), and orphan the first thread's transport + aggregator (the background timer thread leaks).2. install/uninstall vs. init/dispose desynchronization
Independent of (1), the patches and the callback registration can desynchronize. Thread A in
dispose()is mid-_unpatch_*while Thread B ininit()callsinstall(on_event)and sees_installed=True(set by A but not yet cleared). B short-circuits — the new init has no patches.Fix
Guard
init(),dispose(),install(), anduninstall()with a single module-levelthreading.RLock. Add tests that race both pairs.Files
recost/_init.pyrecost/_interceptor.pytests/test_init.pyPriority
P1 — produces a silently-broken SDK ("ingest is no-op") in production restart scenarios.