Python dispose() is sync; Node dispose() is async — shutdown semantics differ
Severity: Medium
Affected repos: middleware-python, middleware-node
Component boundary: middleware lifecycle parity
Symptom
- Node:
await handle.dispose() waits for the final flush to complete (bounded by shutdownFlushTimeoutMs).
- Python:
handle.dispose() returns synchronously after spawning a background flush thread, joining it with a 2-second timeout. The final flush may or may not complete.
A user porting a shutdown path from one SDK to the other will get inconsistent behavior. In Python, fast process exit (os._exit() or short-lived scripts) may drop the last window of data.
Evidence
middleware-node/src/init.ts — dispose() is async, awaits flush.
middleware-python/recost/_init.py — dispose() is sync, spawns a thread with a short join timeout.
Impact
- Cross-language users see surprising differences.
- Pythonists doing graceful shutdown via
atexit or signal handler may lose the trailing telemetry window if the runtime tears down before the thread joins.
Fix recommendation
Two options:
-
Document the difference clearly in both READMEs and CLAUDE.md. Python users should call dispose() and then time.sleep(3) before os._exit() if they want flush guarantees. Cheap.
-
Add a sync flush call that blocks the calling thread: handle.flush_blocking(timeout_s=3). Then dispose() continues to be best-effort but a deliberate user has an explicit handle.
Recommended: do both. Option 2 is the practical fix; option 1 is the rest of the safety net.
Verification
- Python test: run a script that emits an event, calls
dispose(), exits. Assert (via a mock cloud server) that the event arrived.
Related
Python
dispose()is sync; Nodedispose()is async — shutdown semantics differSeverity: Medium
Affected repos:
middleware-python,middleware-nodeComponent boundary: middleware lifecycle parity
Symptom
await handle.dispose()waits for the final flush to complete (bounded byshutdownFlushTimeoutMs).handle.dispose()returns synchronously after spawning a background flush thread, joining it with a 2-second timeout. The final flush may or may not complete.A user porting a shutdown path from one SDK to the other will get inconsistent behavior. In Python, fast process exit (
os._exit()or short-lived scripts) may drop the last window of data.Evidence
middleware-node/src/init.ts—dispose()isasync, awaits flush.middleware-python/recost/_init.py—dispose()is sync, spawns a thread with a short join timeout.Impact
atexitor signal handler may lose the trailing telemetry window if the runtime tears down before the thread joins.Fix recommendation
Two options:
Document the difference clearly in both READMEs and
CLAUDE.md. Python users should calldispose()and thentime.sleep(3)beforeos._exit()if they want flush guarantees. Cheap.Add a sync flush call that blocks the calling thread:
handle.flush_blocking(timeout_s=3). Thendispose()continues to be best-effort but a deliberate user has an explicit handle.Recommended: do both. Option 2 is the practical fix; option 1 is the rest of the safety net.
Verification
dispose(), exits. Assert (via a mock cloud server) that the event arrived.Related
dispose()is sync; Nodedispose()is async — shutdown semantics differ middleware-node#19