@cryptotaxi247 / netdata-1 / commits / 735549f18

Fix based on Coverity and Sonar audits (part 11) (#22339)

* nd-mcp: re-raise asyncio.CancelledError in reconnect-delay handler Sonar python:S7497: connect_with_backoff() caught CancelledError during the reconnect-delay wait but swallowed it with `pass`. A parent-driven cancellation (e.g., process shutdown) could be lost and the outer `while True` reconnect loop would continue. Replace `pass` with `raise` to propagate the cancellation. The preceding lines already perform local cleanup (cancelling helper tasks, clearing retry_event), so no additional cleanup is needed before re-raising. The similar handler at line 312 is a different pattern (cancel-then- await on a child task this code just cancelled); it intentionally suppresses the expected CancelledError from the awaited task and is addressed in a separate finding. * nd-mcp: drain cancelled tasks via gather() so outer cancellation propagates Sonar python:S7497: the cleanup loop after asyncio.wait() cancelled each pending task and awaited it inside `try/except CancelledError: pass`. The handler swallowed the expected CancelledError from the just-cancelled child, but it would also swallow a CancelledError from the OUTER `connect_with_backoff` coroutine if the parent arrived mid-await -- breaking shutdown propagation. Cancel all pending tasks first, then drain them via `asyncio.gather(..., return_exceptions=True)`. Per-child CancelledError is collected as a result and ignored; non-cancellation exceptions are re-raised. If `connect_with_backoff` itself is cancelled while awaiting the gather, gather propagates the cancellation unconditionally. Side-benefit: the original sequential loop would skip cancelling later tasks if an earlier task raised non-CancelledError. The gather form cancels all up front, so cleanup is complete regardless of outcome. * nd-mcp: address PR-review findings Cancel both helper tasks and clear the retry event before re-raising asyncio.CancelledError so reconnect-delay cancellation does not leave dangling state behind. --------- Co-authored-by: Costa Tsaousis <costa@netdata.cloud>

Stelios Fragkakis committed May 7, 2026 at 06:51 UTC 735549f1839a99ee788c84731594f0bb04c607a5
1 file changed +17 -8
src/web/mcp/bridges/stdio-python/nd-mcp.py
+17 -8
@@ -165,7 +165,13 @@ async def connect_with_backoff(uri, bearer_token):
165 retry_event.clear()
166
167 except asyncio.CancelledError:
168 - pass
168 + # Cancel the in-flight wait/retry tasks so they don't
169 + # outlive this coroutine, then propagate the cancellation
170 + # so the parent reconnect loop can exit cleanly.
171 + wait_task.cancel()
172 + retry_task.cancel()
173 + retry_event.clear()
174 + raise
175
176 print(f"{PROGRAM_NAME}: Connecting to {uri}...", file=sys.stderr)
177
@@ -304,13 +310,16 @@ async def connect_with_backoff(uri, bearer_token):
310 return_when=asyncio.FIRST_COMPLETED
311 )
312
307 - # Cancel the pending task
308 - for task in pending:
309 - task.cancel()
310 - try:
311 - await task
312 - except asyncio.CancelledError:
313 - pass
313 + if pending:
314 + # Drain cancelled children without suppressing cancellation of this coroutine.
315 + for task in pending:
316 + task.cancel()
317 + results = await asyncio.gather(*pending, return_exceptions=True)
318 + for result in results:
319 + if isinstance(result, asyncio.CancelledError):
320 + continue
321 + if isinstance(result, BaseException):
322 + raise result
323
324 # Ensure WebSocket is closed
325 try: