Skip to content

Commit c852805

Browse files
committed
fix(connection): make close() idempotent and abort-safe
- check-and-set is_closed/_closing inside _calls lock so concurrent close() cannot double-teardown natives - reset _closing on aborted wait so connection stays usable - identity check for re-entrant close() on same connection
1 parent 60d8c75 commit c852805

1 file changed

Lines changed: 18 additions & 10 deletions

File tree

‎src_py/connection.py‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -490,26 +490,34 @@ def close(self) -> None:
490490
and close() returns once it has left the engine. Called from a UDF
491491
while the connection runs a query, close() raises instead.
492492
"""
493-
if self.is_closed:
494-
return
495-
496-
if self in _active_stack():
493+
if any(x is self for x in _active_stack()):
497494
msg = "close() cannot be called from a call on the same connection."
498495
raise RuntimeError(msg)
499496

500497
with self._calls:
498+
# Idempotent under concurrent close(): check-and-set inside the
499+
# lock so two threads cannot both run the teardown tail.
500+
if self.is_closed or self._closing:
501+
return
501502
if self._calls_in_flight and _called_from_udf(self):
502503
# UDFs run inside the query in flight, so waiting for it would
503504
# never end.
504505
msg = "close() cannot be called from a UDF while the connection runs a query."
505506
raise RuntimeError(msg)
506507
self._closing = True
507-
while self._calls_in_flight:
508-
# An interrupt that arrives while the query is still compiling is
509-
# cleared when execution starts, so repeat it until the call returns.
510-
with contextlib.suppress(RuntimeError):
511-
self.interrupt()
512-
self._calls.wait(timeout=0.05)
508+
try:
509+
while self._calls_in_flight:
510+
# An interrupt that arrives while the query is still compiling is
511+
# cleared when execution starts, so repeat it until the call returns.
512+
with contextlib.suppress(RuntimeError):
513+
self.interrupt()
514+
self._calls.wait(timeout=0.05)
515+
except BaseException:
516+
# Don't brick the connection if the wait is aborted
517+
# (KeyboardInterrupt, ...): teardown never ran, so allow reuse.
518+
self._closing = False
519+
self._calls.notify_all()
520+
raise
513521

514522
for query_result in list(self._query_results):
515523
query_result.close()

0 commit comments

Comments
 (0)