@@ -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