Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/pool/base.py: 56%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# pool/base.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
9"""Base constructs for connection pools."""
11from __future__ import annotations
13from collections import deque
14import dataclasses
15from enum import Enum
16import threading
17import time
18import typing
19from typing import Any
20from typing import Callable
21from typing import cast
22from typing import Deque
23from typing import List
24from typing import Literal
25from typing import Optional
26from typing import Protocol
27from typing import Tuple
28from typing import TYPE_CHECKING
29from typing import Union
30import weakref
32from .. import event
33from .. import exc
34from .. import log
35from .. import util
36from ..util.typing import Self
38if TYPE_CHECKING:
39 from ..engine.interfaces import DBAPIConnection
40 from ..engine.interfaces import DBAPICursor
41 from ..engine.interfaces import Dialect
42 from ..event import _DispatchCommon
43 from ..event import _ListenerFnType
44 from ..event import dispatcher
45 from ..sql._typing import _InfoType
48@dataclasses.dataclass(frozen=True)
49class PoolResetState:
50 """describes the state of a DBAPI connection as it is being passed to
51 the :meth:`.PoolEvents.reset` connection pool event.
53 .. versionadded:: 2.0.0b3
55 """
57 __slots__ = ("transaction_was_reset", "terminate_only", "asyncio_safe")
59 transaction_was_reset: bool
60 """Indicates if the transaction on the DBAPI connection was already
61 essentially "reset" back by the :class:`.Connection` object.
63 This boolean is True if the :class:`.Connection` had transactional
64 state present upon it, which was then not closed using the
65 :meth:`.Connection.rollback` or :meth:`.Connection.commit` method;
66 instead, the transaction was closed inline within the
67 :meth:`.Connection.close` method so is guaranteed to remain non-present
68 when this event is reached.
70 """
72 terminate_only: bool
73 """indicates if the connection is to be immediately terminated and
74 not checked in to the pool.
76 This occurs for connections that were invalidated, as well as asyncio
77 connections that were not cleanly handled by the calling code that
78 are instead being garbage collected. In the latter case,
79 operations can't be safely run on asyncio connections within garbage
80 collection as there is not necessarily an event loop present.
82 """
84 asyncio_safe: bool
85 """Indicates if the reset operation is occurring within a scope where
86 an enclosing event loop is expected to be present for asyncio applications.
88 Will be False in the case that the connection is being garbage collected.
90 """
93class ResetStyle(Enum):
94 """Describe options for "reset on return" behaviors."""
96 reset_rollback = 0
97 reset_commit = 1
98 reset_none = 2
101_ResetStyleArgType = Union[
102 ResetStyle,
103 Literal[True, None, False, "commit", "rollback"],
104]
105reset_rollback, reset_commit, reset_none = list(ResetStyle)
108class _ConnDialect:
109 """partial implementation of :class:`.Dialect`
110 which provides DBAPI connection methods.
112 When a :class:`_pool.Pool` is combined with an :class:`_engine.Engine`,
113 the :class:`_engine.Engine` replaces this with its own
114 :class:`.Dialect`.
116 """
118 is_async = False
119 has_terminate = False
121 def do_rollback(self, dbapi_connection: PoolProxiedConnection) -> None:
122 dbapi_connection.rollback()
124 def do_commit(self, dbapi_connection: PoolProxiedConnection) -> None:
125 dbapi_connection.commit()
127 def do_terminate(self, dbapi_connection: DBAPIConnection) -> None:
128 dbapi_connection.close()
130 def do_close(self, dbapi_connection: DBAPIConnection) -> None:
131 dbapi_connection.close()
133 def _do_ping_w_event(self, dbapi_connection: DBAPIConnection) -> bool:
134 raise NotImplementedError(
135 "The ping feature requires that a dialect is "
136 "passed to the connection pool."
137 )
139 def get_driver_connection(self, connection: DBAPIConnection) -> Any:
140 return connection
143class _AsyncConnDialect(_ConnDialect):
144 is_async = True
147class _CreatorFnType(Protocol):
148 def __call__(self) -> DBAPIConnection: ...
151class _CreatorWRecFnType(Protocol):
152 def __call__(self, rec: ConnectionPoolEntry) -> DBAPIConnection: ...
155class Pool(log.Identified, event.EventTarget):
156 """Abstract base class for connection pools."""
158 dispatch: dispatcher[Pool]
159 echo: log._EchoFlagType
161 _orig_logging_name: Optional[str]
162 _dialect: Union[_ConnDialect, Dialect] = _ConnDialect()
163 _creator_arg: Union[_CreatorFnType, _CreatorWRecFnType]
164 _invoke_creator: _CreatorWRecFnType
165 _invalidate_time: float
167 def __init__(
168 self,
169 creator: Union[_CreatorFnType, _CreatorWRecFnType],
170 recycle: int = -1,
171 echo: log._EchoFlagType = None,
172 logging_name: Optional[str] = None,
173 reset_on_return: _ResetStyleArgType = True,
174 events: Optional[List[Tuple[_ListenerFnType, str]]] = None,
175 dialect: Optional[Union[_ConnDialect, Dialect]] = None,
176 pre_ping: bool = False,
177 _dispatch: Optional[_DispatchCommon[Pool]] = None,
178 ):
179 """
180 Construct a Pool.
182 :param creator: a callable function that returns a DB-API
183 connection object. The function will be called with
184 parameters.
186 :param recycle: If set to a value other than -1, number of
187 seconds between connection recycling, which means upon
188 checkout, if this timeout is surpassed the connection will be
189 closed and replaced with a newly opened connection. Defaults to -1.
191 :param logging_name: String identifier which will be used within
192 the "name" field of logging records generated within the
193 "sqlalchemy.pool" logger. Defaults to a hexstring of the object's
194 id.
196 :param echo: if True, the connection pool will log
197 informational output such as when connections are invalidated
198 as well as when connections are recycled to the default log handler,
199 which defaults to ``sys.stdout`` for output.. If set to the string
200 ``"debug"``, the logging will include pool checkouts and checkins.
202 The :paramref:`_pool.Pool.echo` parameter can also be set from the
203 :func:`_sa.create_engine` call by using the
204 :paramref:`_sa.create_engine.echo_pool` parameter.
206 .. seealso::
208 :ref:`dbengine_logging` - further detail on how to configure
209 logging.
211 :param reset_on_return: Determine steps to take on
212 connections as they are returned to the pool, which were
213 not otherwise handled by a :class:`_engine.Connection`.
214 Available from :func:`_sa.create_engine` via the
215 :paramref:`_sa.create_engine.pool_reset_on_return` parameter.
217 :paramref:`_pool.Pool.reset_on_return` can have any of these values:
219 * ``"rollback"`` - call rollback() on the connection,
220 to release locks and transaction resources.
221 This is the default value. The vast majority
222 of use cases should leave this value set.
223 * ``"commit"`` - call commit() on the connection,
224 to release locks and transaction resources.
225 A commit here may be desirable for databases that
226 cache query plans if a commit is emitted,
227 such as Microsoft SQL Server. However, this
228 value is more dangerous than 'rollback' because
229 any data changes present on the transaction
230 are committed unconditionally.
231 * ``None`` - don't do anything on the connection.
232 This setting may be appropriate if the database / DBAPI
233 works in pure "autocommit" mode at all times, or if
234 a custom reset handler is established using the
235 :meth:`.PoolEvents.reset` event handler.
237 * ``True`` - same as 'rollback', this is here for
238 backwards compatibility.
239 * ``False`` - same as None, this is here for
240 backwards compatibility.
242 For further customization of reset on return, the
243 :meth:`.PoolEvents.reset` event hook may be used which can perform
244 any connection activity desired on reset.
246 .. seealso::
248 :ref:`pool_reset_on_return`
250 :meth:`.PoolEvents.reset`
252 :param events: a list of 2-tuples, each of the form
253 ``(callable, target)`` which will be passed to :func:`.event.listen`
254 upon construction. Provided here so that event listeners
255 can be assigned via :func:`_sa.create_engine` before dialect-level
256 listeners are applied.
258 :param dialect: a :class:`.Dialect` that will handle the job
259 of calling rollback(), close(), or commit() on DBAPI connections.
260 If omitted, a built-in "stub" dialect is used. Applications that
261 make use of :func:`_sa.create_engine` should not use this parameter
262 as it is handled by the engine creation strategy.
264 :param pre_ping: if True, the pool will emit a "ping" (typically
265 "SELECT 1", but is dialect-specific) on the connection
266 upon checkout, to test if the connection is alive or not. If not,
267 the connection is transparently re-connected and upon success, all
268 other pooled connections established prior to that timestamp are
269 invalidated. Requires that a dialect is passed as well to
270 interpret the disconnection error.
272 """
273 if logging_name:
274 self.logging_name = self._orig_logging_name = logging_name
275 else:
276 self._orig_logging_name = None
278 log.instance_logger(self, echoflag=echo)
279 self._creator = creator
280 self._recycle = recycle
281 self._invalidate_time = 0
282 self._pre_ping = pre_ping
283 self._reset_on_return = util.parse_user_argument_for_enum(
284 reset_on_return,
285 {
286 ResetStyle.reset_rollback: ["rollback", True],
287 ResetStyle.reset_none: ["none", None, False],
288 ResetStyle.reset_commit: ["commit"],
289 },
290 "reset_on_return",
291 )
293 self.echo = echo
295 if _dispatch:
296 self.dispatch._update(_dispatch, only_propagate=False)
297 if dialect:
298 self._dialect = dialect
299 if events:
300 for fn, target in events:
301 event.listen(self, target, fn)
303 @util.hybridproperty
304 def _is_asyncio(self) -> bool:
305 return self._dialect.is_async
307 @property
308 def _creator(self) -> Union[_CreatorFnType, _CreatorWRecFnType]:
309 return self._creator_arg
311 @_creator.setter
312 def _creator(
313 self, creator: Union[_CreatorFnType, _CreatorWRecFnType]
314 ) -> None:
315 self._creator_arg = creator
317 # mypy seems to get super confused assigning functions to
318 # attributes
319 self._invoke_creator = self._should_wrap_creator(creator)
321 @_creator.deleter
322 def _creator(self) -> None:
323 # needed for mock testing
324 del self._creator_arg
325 del self._invoke_creator
327 def _should_wrap_creator(
328 self, creator: Union[_CreatorFnType, _CreatorWRecFnType]
329 ) -> _CreatorWRecFnType:
330 """Detect if creator accepts a single argument, or is sent
331 as a legacy style no-arg function.
333 """
335 try:
336 argspec = util.get_callable_argspec(self._creator, no_self=True)
337 except TypeError:
338 creator_fn = cast(_CreatorFnType, creator)
339 return lambda rec: creator_fn()
341 if argspec.defaults is not None:
342 defaulted = len(argspec.defaults)
343 else:
344 defaulted = 0
345 positionals = len(argspec[0]) - defaulted
347 # look for the exact arg signature that DefaultStrategy
348 # sends us
349 if (argspec[0], argspec[3]) == (["connection_record"], (None,)):
350 return cast(_CreatorWRecFnType, creator)
351 # or just a single positional
352 elif positionals == 1:
353 return cast(_CreatorWRecFnType, creator)
354 # all other cases, just wrap and assume legacy "creator" callable
355 # thing
356 else:
357 creator_fn = cast(_CreatorFnType, creator)
358 return lambda rec: creator_fn()
360 def _close_connection(
361 self, connection: DBAPIConnection, *, terminate: bool = False
362 ) -> None:
363 self.logger.debug(
364 "%s connection %r",
365 "Hard-closing" if terminate else "Closing",
366 connection,
367 )
368 try:
369 if terminate:
370 self._dialect.do_terminate(connection)
371 else:
372 self._dialect.do_close(connection)
373 except BaseException as e:
374 self.logger.error(
375 f"Exception {'terminating' if terminate else 'closing'} "
376 f"connection %r",
377 connection,
378 exc_info=True,
379 )
380 if not isinstance(e, Exception):
381 raise
383 def _create_connection(self) -> ConnectionPoolEntry:
384 """Called by subclasses to create a new ConnectionRecord."""
386 return _ConnectionRecord(self)
388 def _invalidate(
389 self,
390 connection: PoolProxiedConnection,
391 exception: Optional[BaseException] = None,
392 _checkin: bool = True,
393 ) -> None:
394 """Mark all connections established within the generation
395 of the given connection as invalidated.
397 If this pool's last invalidate time is before when the given
398 connection was created, update the timestamp til now. Otherwise,
399 no action is performed.
401 Connections with a start time prior to this pool's invalidation
402 time will be recycled upon next checkout.
403 """
404 rec = getattr(connection, "_connection_record", None)
405 if not rec or self._invalidate_time < rec.starttime:
406 self._invalidate_time = time.time()
407 if _checkin and getattr(connection, "is_valid", False):
408 connection.invalidate(exception)
410 def recreate(self) -> Pool:
411 """Return a new :class:`_pool.Pool`, of the same class as this one
412 and configured with identical creation arguments.
414 This method is used in conjunction with :meth:`dispose`
415 to close out an entire :class:`_pool.Pool` and create a new one in
416 its place.
418 """
420 raise NotImplementedError()
422 def dispose(self) -> None:
423 """Dispose of this pool.
425 This method leaves the possibility of checked-out connections
426 remaining open, as it only affects connections that are
427 idle in the pool.
429 .. seealso::
431 :meth:`Pool.recreate`
433 """
435 raise NotImplementedError()
437 def connect(self) -> PoolProxiedConnection:
438 """Return a DBAPI connection from the pool.
440 The connection is instrumented such that when its
441 ``close()`` method is called, the connection will be returned to
442 the pool.
444 """
445 return _ConnectionFairy._checkout(self)
447 def _return_conn(self, record: ConnectionPoolEntry) -> None:
448 """Given a _ConnectionRecord, return it to the :class:`_pool.Pool`.
450 This method is called when an instrumented DBAPI connection
451 has its ``close()`` method called.
453 """
454 self._do_return_conn(record)
456 def _do_get(self) -> ConnectionPoolEntry:
457 """Implementation for :meth:`get`, supplied by subclasses."""
459 raise NotImplementedError()
461 def _do_return_conn(self, record: ConnectionPoolEntry) -> None:
462 """Implementation for :meth:`return_conn`, supplied by subclasses."""
464 raise NotImplementedError()
466 def status(self) -> str:
467 """Returns a brief description of the state of this pool."""
468 raise NotImplementedError()
471class ManagesConnection:
472 """Common base for the two connection-management interfaces
473 :class:`.PoolProxiedConnection` and :class:`.ConnectionPoolEntry`.
475 These two objects are typically exposed in the public facing API
476 via the connection pool event hooks, documented at :class:`.PoolEvents`.
478 .. versionadded:: 2.0
480 """
482 __slots__ = ()
484 dbapi_connection: Optional[DBAPIConnection]
485 """A reference to the actual DBAPI connection being tracked.
487 This is a :pep:`249`-compliant object that for traditional sync-style
488 dialects is provided by the third-party
489 DBAPI implementation in use. For asyncio dialects, the implementation
490 is typically an adapter object provided by the SQLAlchemy dialect
491 itself; the underlying asyncio object is available via the
492 :attr:`.ManagesConnection.driver_connection` attribute.
494 SQLAlchemy's interface for the DBAPI connection is based on the
495 :class:`.DBAPIConnection` protocol object
497 .. seealso::
499 :attr:`.ManagesConnection.driver_connection`
501 :ref:`faq_dbapi_connection`
503 """
505 driver_connection: Optional[Any]
506 """The "driver level" connection object as used by the Python
507 DBAPI or database driver.
509 For traditional :pep:`249` DBAPI implementations, this object will
510 be the same object as that of
511 :attr:`.ManagesConnection.dbapi_connection`. For an asyncio database
512 driver, this will be the ultimate "connection" object used by that
513 driver, such as the ``asyncpg.Connection`` object which will not have
514 standard pep-249 methods.
516 .. versionadded:: 1.4.24
518 .. seealso::
520 :attr:`.ManagesConnection.dbapi_connection`
522 :ref:`faq_dbapi_connection`
524 """
526 @util.ro_memoized_property
527 def info(self) -> _InfoType:
528 """Info dictionary associated with the underlying DBAPI connection
529 referred to by this :class:`.ManagesConnection` instance, allowing
530 user-defined data to be associated with the connection.
532 The data in this dictionary is persistent for the lifespan
533 of the DBAPI connection itself, including across pool checkins
534 and checkouts. When the connection is invalidated
535 and replaced with a new one, this dictionary is cleared.
537 For a :class:`.PoolProxiedConnection` instance that's not associated
538 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the
539 attribute returns a dictionary that is local to that
540 :class:`.ConnectionPoolEntry`. Therefore the
541 :attr:`.ManagesConnection.info` attribute will always provide a Python
542 dictionary.
544 .. seealso::
546 :attr:`.ManagesConnection.record_info`
549 """
550 raise NotImplementedError()
552 @util.ro_memoized_property
553 def record_info(self) -> Optional[_InfoType]:
554 """Persistent info dictionary associated with this
555 :class:`.ManagesConnection`.
557 Unlike the :attr:`.ManagesConnection.info` dictionary, the lifespan
558 of this dictionary is that of the :class:`.ConnectionPoolEntry`
559 which owns it; therefore this dictionary will persist across
560 reconnects and connection invalidation for a particular entry
561 in the connection pool.
563 For a :class:`.PoolProxiedConnection` instance that's not associated
564 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the
565 attribute returns None. Contrast to the :attr:`.ManagesConnection.info`
566 dictionary which is never None.
569 .. seealso::
571 :attr:`.ManagesConnection.info`
573 """
574 raise NotImplementedError()
576 def invalidate(
577 self, e: Optional[BaseException] = None, soft: bool = False
578 ) -> None:
579 """Mark the managed connection as invalidated.
581 :param e: an exception object indicating a reason for the invalidation.
583 :param soft: if True, the connection isn't closed; instead, this
584 connection will be recycled on next checkout.
586 .. seealso::
588 :ref:`pool_connection_invalidation`
591 """
592 raise NotImplementedError()
595class ConnectionPoolEntry(ManagesConnection):
596 """Interface for the object that maintains an individual database
597 connection on behalf of a :class:`_pool.Pool` instance.
599 The :class:`.ConnectionPoolEntry` object represents the long term
600 maintenance of a particular connection for a pool, including expiring or
601 invalidating that connection to have it replaced with a new one, which will
602 continue to be maintained by that same :class:`.ConnectionPoolEntry`
603 instance. Compared to :class:`.PoolProxiedConnection`, which is the
604 short-term, per-checkout connection manager, this object lasts for the
605 lifespan of a particular "slot" within a connection pool.
607 The :class:`.ConnectionPoolEntry` object is mostly visible to public-facing
608 API code when it is delivered to connection pool event hooks, such as
609 :meth:`_events.PoolEvents.connect` and :meth:`_events.PoolEvents.checkout`.
611 .. versionadded:: 2.0 :class:`.ConnectionPoolEntry` provides the public
612 facing interface for the :class:`._ConnectionRecord` internal class.
614 """
616 __slots__ = ()
618 @property
619 def in_use(self) -> bool:
620 """Return True the connection is currently checked out"""
622 raise NotImplementedError()
624 def close(self) -> None:
625 """Close the DBAPI connection managed by this connection pool entry."""
626 raise NotImplementedError()
629# no-op callback for weakrefs; see _ConnectionRecord.checkout
630_NOOP_WEAKREF_CALLBACK = lambda ref: None # noqa: E731
633class _ConnectionRecord(ConnectionPoolEntry):
634 """Maintains a position in a connection pool which references a pooled
635 connection.
637 This is an internal object used by the :class:`_pool.Pool` implementation
638 to provide context management to a DBAPI connection maintained by
639 that :class:`_pool.Pool`. The public facing interface for this class
640 is described by the :class:`.ConnectionPoolEntry` class. See that
641 class for public API details.
643 .. seealso::
645 :class:`.ConnectionPoolEntry`
647 :class:`.PoolProxiedConnection`
649 """
651 __slots__ = (
652 "__pool",
653 "fairy_ref",
654 "finalize_callback",
655 "fresh",
656 "starttime",
657 "dbapi_connection",
658 "__weakref__",
659 "__dict__",
660 )
662 finalize_callback: Deque[Callable[[DBAPIConnection], None]]
663 fresh: bool
664 fairy_ref: Optional[weakref.ref[_ConnectionFairy]]
665 starttime: float
667 def __init__(self, pool: Pool, connect: bool = True):
668 self.fresh = False
669 self.fairy_ref = None
670 self.starttime = 0
671 self.dbapi_connection = None
673 self.__pool = pool
674 if connect:
675 self.__connect()
676 self.finalize_callback = deque()
678 dbapi_connection: Optional[DBAPIConnection]
680 @property
681 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501
682 if self.dbapi_connection is None:
683 return None
684 else:
685 return self.__pool._dialect.get_driver_connection(
686 self.dbapi_connection
687 )
689 @property
690 @util.deprecated(
691 "2.0",
692 "The _ConnectionRecord.connection attribute is deprecated; "
693 "please use 'driver_connection'",
694 )
695 def connection(self) -> Optional[DBAPIConnection]:
696 return self.dbapi_connection
698 _soft_invalidate_time: float = 0
700 @util.ro_memoized_property
701 def info(self) -> _InfoType:
702 return {}
704 @util.ro_memoized_property
705 def record_info(self) -> Optional[_InfoType]:
706 return {}
708 @classmethod
709 def checkout(cls, pool: Pool) -> _ConnectionFairy:
710 if TYPE_CHECKING:
711 rec = cast(_ConnectionRecord, pool._do_get())
712 else:
713 rec = pool._do_get()
715 try:
716 dbapi_connection = rec.get_connection()
717 except BaseException as err:
718 with util.safe_reraise():
719 rec._checkin_failed(err, _fairy_was_created=False)
721 # not reached, for code linters only
722 raise
724 echo = pool._should_log_debug()
725 fairy = _ConnectionFairy(pool, dbapi_connection, rec, echo)
727 # assign fairy_ref to the ConnectionRecord; note that under StaticPool,
728 # this could be swapping out an existing fairy on that record.
729 #
730 # the weakref gets a no-op callback because Python clears weakrefs
731 # that have callbacks before running any weakref callback, including
732 # the fairy's finalizer, which relies on rec.fairy_ref() being None;
733 # since Python 3.15, a weakref without a callback to an object in a
734 # reference cycle isn't cleared until after finalizers run
735 # (cpython gh-135552). #13738
736 #
737 # an alternative is for needs_gc to also treat a fairy whose
738 # finalizer was already called as gone. that requires atexit=False
739 # on the finalizer, otherwise its call at interpreter exit would
740 # reset and check in connections that are still checked out.
741 rec.fairy_ref = weakref.ref(fairy, _NOOP_WEAKREF_CALLBACK)
743 if echo:
744 pool.logger.debug(
745 "Connection %r checked out from pool", dbapi_connection
746 )
747 return fairy
749 def _checkin_failed(
750 self, err: BaseException, _fairy_was_created: bool = True
751 ) -> None:
752 try:
753 self.invalidate(e=err)
754 finally:
755 # the record must be returned in any case; nothing else will
756 # ever return it, as no _ConnectionFairy refers to it
757 self.checkin(
758 _fairy_was_created=_fairy_was_created,
759 )
761 def checkin(self, _fairy_was_created: bool = True) -> None:
762 if self.fairy_ref is None and _fairy_was_created:
763 # _fairy_was_created is False for the initial get connection phase;
764 # meaning there was no _ConnectionFairy and we must unconditionally
765 # do a checkin.
766 #
767 # otherwise, if fairy_was_created==True, if fairy_ref is None here
768 # that means we were checked in already, so this looks like
769 # a double checkin.
770 util.warn("Double checkin attempted on %s" % self)
771 return
772 self.fairy_ref = None
773 connection = self.dbapi_connection
774 pool = self.__pool
775 while self.finalize_callback:
776 finalizer = self.finalize_callback.pop()
777 if connection is not None:
778 finalizer(connection)
779 if pool.dispatch.checkin:
780 pool.dispatch.checkin(connection, self)
782 pool._return_conn(self)
784 @property
785 def in_use(self) -> bool:
786 return self.fairy_ref is not None
788 @property
789 def needs_gc(self) -> bool:
790 ref = self.fairy_ref
791 return ref is not None and ref() is None
793 @property
794 def last_connect_time(self) -> float:
795 return self.starttime
797 def close(self) -> None:
798 if self.dbapi_connection is not None:
799 self.__close()
801 def invalidate(
802 self, e: Optional[BaseException] = None, soft: bool = False
803 ) -> None:
804 # already invalidated
805 if self.dbapi_connection is None:
806 return
807 if soft:
808 self.__pool.dispatch.soft_invalidate(
809 self.dbapi_connection, self, e
810 )
811 self._log_invalidate(e, soft)
812 self._soft_invalidate_time = time.time()
813 return
815 try:
816 self.__pool.dispatch.invalidate(self.dbapi_connection, self, e)
817 self._log_invalidate(e, soft)
818 self.__close(terminate=True)
819 assert self.dbapi_connection is None
820 finally:
821 if self.dbapi_connection is not None:
822 # the hard invalidation was interrupted before it could
823 # close the connection, such as by an exception raised
824 # from the invalidate event. a close that was itself
825 # interrupted has already discarded the connection; here
826 # it's kept so that its close can be retried, but the
827 # next checkout recycles it rather than use it
828 self._soft_invalidate_time = time.time()
830 def _log_invalidate(self, e: Optional[BaseException], soft: bool) -> None:
831 if e is not None:
832 self.__pool.logger.info(
833 "%sInvalidate connection %r (reason: %s:%s)",
834 "Soft " if soft else "",
835 self.dbapi_connection,
836 e.__class__.__name__,
837 e,
838 )
839 else:
840 self.__pool.logger.info(
841 "%sInvalidate connection %r",
842 "Soft " if soft else "",
843 self.dbapi_connection,
844 )
846 def get_connection(self) -> DBAPIConnection:
847 recycle = False
849 # NOTE: the various comparisons here are assuming that measurable time
850 # passes between these state changes. however, time.time() is not
851 # guaranteed to have sub-second precision. comparisons of
852 # "invalidation time" to "starttime" should perhaps use >= so that the
853 # state change can take place assuming no measurable time has passed,
854 # however this does not guarantee correct behavior here as if time
855 # continues to not pass, it will try to reconnect repeatedly until
856 # these timestamps diverge, so in that sense using > is safer. Per
857 # https://stackoverflow.com/a/1938096/34549, Windows time.time() may be
858 # within 16 milliseconds accuracy, so unit tests for connection
859 # invalidation need a sleep of at least this long between initial start
860 # time and invalidation for the logic below to work reliably.
862 if self.dbapi_connection is None:
863 self.info.clear()
864 self.__connect()
865 elif (
866 self.__pool._recycle > -1
867 and time.time() - self.starttime > self.__pool._recycle
868 ):
869 self.__pool.logger.info(
870 "Connection %r exceeded timeout; recycling",
871 self.dbapi_connection,
872 )
873 recycle = True
874 elif self.__pool._invalidate_time > self.starttime:
875 self.__pool.logger.info(
876 "Connection %r invalidated due to pool invalidation; "
877 + "recycling",
878 self.dbapi_connection,
879 )
880 recycle = True
881 elif self._soft_invalidate_time > self.starttime:
882 self.__pool.logger.info(
883 "Connection %r invalidated due to local soft invalidation; "
884 + "recycling",
885 self.dbapi_connection,
886 )
887 recycle = True
889 if recycle:
890 self.__close(terminate=True)
891 self.info.clear()
893 self.__connect()
895 assert self.dbapi_connection is not None
896 return self.dbapi_connection
898 def _is_hard_or_soft_invalidated(self) -> bool:
899 return (
900 self.dbapi_connection is None
901 or self.__pool._invalidate_time > self.starttime
902 or (self._soft_invalidate_time > self.starttime)
903 )
905 def __close(self, *, terminate: bool = False) -> None:
906 self.finalize_callback.clear()
907 if self.__pool.dispatch.close:
908 self.__pool.dispatch.close(self.dbapi_connection, self)
909 assert self.dbapi_connection is not None
910 try:
911 self.__pool._close_connection(
912 self.dbapi_connection, terminate=terminate
913 )
914 finally:
915 # a close that was interrupted, such as by a cancellation
916 # reaching the await within an asyncio terminate(), is not
917 # retried; the connection is not to be handed out again
918 self.dbapi_connection = None
920 def __connect(self) -> None:
921 pool = self.__pool
923 # ensure any existing connection is removed, so that if
924 # creator fails, this attribute stays None
925 self.dbapi_connection = None
926 try:
927 self.starttime = time.time()
928 self.dbapi_connection = connection = pool._invoke_creator(self)
929 pool.logger.debug("Created new connection %r", connection)
930 self.fresh = True
931 except BaseException as e:
932 with util.safe_reraise():
933 pool.logger.debug("Error on connect(): %s", e)
934 else:
935 try:
936 # in SQLAlchemy 1.4 the first_connect event is not used by
937 # the engine, so this will usually not be set
938 if pool.dispatch.first_connect:
939 pool.dispatch.first_connect.for_modify(
940 pool.dispatch
941 ).exec_once_unless_exception(self.dbapi_connection, self)
943 # init of the dialect now takes place within the connect
944 # event, so ensure a mutex is used on the first run
945 pool.dispatch.connect.for_modify(
946 pool.dispatch
947 )._exec_w_sync_on_first_run(self.dbapi_connection, self)
948 except BaseException:
949 # the connection is established but the events that
950 # configure it did not complete; nothing else has a
951 # reference to it, so close it here or it is stranded.
952 # for an asyncio driver in particular there is no other
953 # opportunity, as the garbage collector cannot close a
954 # connection that needs the event loop
955 with util.safe_reraise():
956 try:
957 pool._close_connection(connection, terminate=True)
958 except Exception:
959 pool.logger.exception(
960 "Exception closing connection %r stranded by a "
961 "failed connect event",
962 connection,
963 )
964 finally:
965 # a cancellation reaching the await within an
966 # asyncio terminate() propagates in place of the
967 # original exception
968 self.dbapi_connection = None
971def _finalize_fairy(
972 dbapi_connection: Optional[DBAPIConnection],
973 connection_record: Optional[_ConnectionRecord],
974 pool: Pool,
975 echo: Optional[log._EchoFlagType],
976 transaction_was_reset: bool = False,
977 fairy: Optional[_ConnectionFairy] = None,
978 is_gc_cleanup: bool = False,
979) -> None:
980 """Cleanup for a :class:`._ConnectionFairy` whether or not it's already
981 been garbage collected.
983 When using an async dialect no IO can happen here (without using
984 a dedicated thread), since this is called outside the greenlet
985 context and with an already running loop. In this case function
986 will only log a message and raise a warning.
987 """
989 if is_gc_cleanup:
990 assert connection_record is not None
992 # check connection record to see that we're the current
993 # fairy for this record. if not, then return; assume the
994 # record is either checked in, or another fairy supersedes us
995 # (can happen with StaticPool)
996 if not connection_record.needs_gc:
997 return
998 assert dbapi_connection is None
999 dbapi_connection = connection_record.dbapi_connection
1001 # null pool is not _is_asyncio but can be used also with async dialects
1002 dont_restore_gced = pool._dialect.is_async
1004 if dont_restore_gced:
1005 detach = connection_record is None or is_gc_cleanup
1006 can_manipulate_connection = not is_gc_cleanup
1007 can_close_or_terminate_connection = (
1008 not pool._dialect.is_async or pool._dialect.has_terminate
1009 )
1010 requires_terminate_for_close = (
1011 pool._dialect.is_async and pool._dialect.has_terminate
1012 )
1014 else:
1015 detach = connection_record is None
1016 can_manipulate_connection = can_close_or_terminate_connection = True
1017 requires_terminate_for_close = False
1019 if dbapi_connection is not None:
1020 if connection_record and echo:
1021 pool.logger.debug(
1022 "Connection %r being returned to pool", dbapi_connection
1023 )
1025 try:
1026 if not fairy:
1027 assert connection_record is not None
1028 fairy = _ConnectionFairy(
1029 pool,
1030 dbapi_connection,
1031 connection_record,
1032 echo,
1033 )
1034 assert fairy.dbapi_connection is dbapi_connection
1036 fairy._reset(
1037 pool,
1038 transaction_was_reset=transaction_was_reset,
1039 terminate_only=detach,
1040 asyncio_safe=can_manipulate_connection,
1041 )
1043 if detach:
1044 if connection_record:
1045 fairy._pool = pool
1046 fairy.detach()
1048 if can_close_or_terminate_connection:
1049 if pool.dispatch.close_detached:
1050 pool.dispatch.close_detached(dbapi_connection)
1052 pool._close_connection(
1053 dbapi_connection,
1054 terminate=requires_terminate_for_close,
1055 )
1057 except BaseException as e:
1058 pool.logger.error(
1059 "Exception during reset or similar", exc_info=True
1060 )
1061 if not isinstance(e, Exception):
1062 # a cancellation or similar propagates from here, so the
1063 # checkin() below is not reached; return the record now,
1064 # rather than leaving it to garbage collection of the fairy.
1065 # the cleanup of the fairy below is not reached either, so
1066 # detach it from the record first, so that it can't act on
1067 # the record once that's checked out again. see #13699
1068 if fairy is not None:
1069 fairy._finalizer.detach()
1070 fairy.dbapi_connection = None # type: ignore[assignment]
1071 fairy._connection_record = None
1072 if connection_record:
1073 connection_record._checkin_failed(e)
1074 raise
1075 if connection_record:
1076 connection_record.invalidate(e=e)
1077 finally:
1078 if detach and is_gc_cleanup and dont_restore_gced:
1079 message = (
1080 "The garbage collector is trying to clean up "
1081 f"non-checked-in connection {dbapi_connection!r}, "
1082 f"""which will be {
1083 'dropped, as it cannot be safely terminated'
1084 if not can_close_or_terminate_connection
1085 else 'terminated'
1086 }. """
1087 "Please ensure that SQLAlchemy pooled connections are "
1088 "returned to "
1089 "the pool explicitly, either by calling ``close()`` "
1090 "or by using appropriate context managers to manage "
1091 "their lifecycle."
1092 )
1093 pool.logger.error(message)
1094 util.warn(message)
1096 if connection_record and connection_record.in_use:
1097 connection_record.checkin()
1099 # give gc some help. See
1100 # test/engine/test_pool.py::PoolEventsTest::test_checkin_event_gc[True]
1101 # which actually started failing when pytest warnings plugin was
1102 # turned on, due to util.warn() above
1103 if fairy is not None:
1104 # don't need the finalizer anymore since we are cleaning up here
1105 fairy._finalizer.detach()
1106 fairy.dbapi_connection = None # type: ignore[assignment]
1107 fairy._connection_record = None
1108 del dbapi_connection
1109 del connection_record
1110 del fairy
1113class PoolProxiedConnection(ManagesConnection):
1114 """A connection-like adapter for a :pep:`249` DBAPI connection, which
1115 includes additional methods specific to the :class:`.Pool` implementation.
1117 :class:`.PoolProxiedConnection` is the public-facing interface for the
1118 internal :class:`._ConnectionFairy` implementation object; users familiar
1119 with :class:`._ConnectionFairy` can consider this object to be equivalent.
1121 .. versionadded:: 2.0 :class:`.PoolProxiedConnection` provides the public-
1122 facing interface for the :class:`._ConnectionFairy` internal class.
1124 """
1126 __slots__ = ()
1128 if typing.TYPE_CHECKING:
1130 def commit(self) -> None: ...
1132 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor: ...
1134 def rollback(self) -> None: ...
1136 def __getattr__(self, key: str) -> Any: ...
1138 @property
1139 def is_valid(self) -> bool:
1140 """Return True if this :class:`.PoolProxiedConnection` still refers
1141 to an active DBAPI connection."""
1143 raise NotImplementedError()
1145 @property
1146 def is_detached(self) -> bool:
1147 """Return True if this :class:`.PoolProxiedConnection` is detached
1148 from its pool."""
1150 raise NotImplementedError()
1152 def detach(self) -> None:
1153 """Separate this connection from its Pool.
1155 This means that the connection will no longer be returned to the
1156 pool when closed, and will instead be literally closed. The
1157 associated :class:`.ConnectionPoolEntry` is de-associated from this
1158 DBAPI connection.
1160 Note that any overall connection limiting constraints imposed by a
1161 Pool implementation may be violated after a detach, as the detached
1162 connection is removed from the pool's knowledge and control.
1164 """
1166 raise NotImplementedError()
1168 def close(self) -> None:
1169 """Release this connection back to the pool.
1171 The :meth:`.PoolProxiedConnection.close` method shadows the
1172 :pep:`249` ``.close()`` method, altering its behavior to instead
1173 :term:`release` the proxied connection back to the connection pool.
1175 Upon release to the pool, whether the connection stays "opened" and
1176 pooled in the Python process, versus actually closed out and removed
1177 from the Python process, is based on the pool implementation in use and
1178 its configuration and current state.
1180 """
1181 raise NotImplementedError()
1183 def __enter__(self) -> Self:
1184 return self
1186 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
1187 self.close()
1188 return None
1191class _AdhocProxiedConnection(PoolProxiedConnection):
1192 """provides the :class:`.PoolProxiedConnection` interface for cases where
1193 the DBAPI connection is not actually proxied.
1195 This is used by the engine internals to pass a consistent
1196 :class:`.PoolProxiedConnection` object to consuming dialects in response to
1197 pool events that may not always have the :class:`._ConnectionFairy`
1198 available.
1200 """
1202 __slots__ = ("dbapi_connection", "_connection_record", "_is_valid")
1204 dbapi_connection: DBAPIConnection
1205 _connection_record: ConnectionPoolEntry
1207 def __init__(
1208 self,
1209 dbapi_connection: DBAPIConnection,
1210 connection_record: ConnectionPoolEntry,
1211 ):
1212 self.dbapi_connection = dbapi_connection
1213 self._connection_record = connection_record
1214 self._is_valid = True
1216 @property
1217 def driver_connection(self) -> Any: # type: ignore[override] # mypy#4125
1218 return self._connection_record.driver_connection
1220 @property
1221 def connection(self) -> DBAPIConnection:
1222 return self.dbapi_connection
1224 @property
1225 def is_valid(self) -> bool:
1226 """Implement is_valid state attribute.
1228 for the adhoc proxied connection it's assumed the connection is valid
1229 as there is no "invalidate" routine.
1231 """
1232 return self._is_valid
1234 def invalidate(
1235 self, e: Optional[BaseException] = None, soft: bool = False
1236 ) -> None:
1237 self._is_valid = False
1239 @util.ro_non_memoized_property
1240 def record_info(self) -> Optional[_InfoType]:
1241 return self._connection_record.record_info
1243 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor:
1244 return self.dbapi_connection.cursor(*args, **kwargs)
1246 def __getattr__(self, key: Any) -> Any:
1247 return getattr(self.dbapi_connection, key)
1250class _ConnectionFairy(PoolProxiedConnection):
1251 """Proxies a DBAPI connection and provides return-on-dereference
1252 support.
1254 This is an internal object used by the :class:`_pool.Pool` implementation
1255 to provide context management to a DBAPI connection delivered by
1256 that :class:`_pool.Pool`. The public facing interface for this class
1257 is described by the :class:`.PoolProxiedConnection` class. See that
1258 class for public API details.
1260 The name "fairy" is inspired by the fact that the
1261 :class:`._ConnectionFairy` object's lifespan is transitory, as it lasts
1262 only for the length of a specific DBAPI connection being checked out from
1263 the pool, and additionally that as a transparent proxy, it is mostly
1264 invisible.
1266 .. seealso::
1268 :class:`.PoolProxiedConnection`
1270 :class:`.ConnectionPoolEntry`
1273 """
1275 __slots__ = (
1276 "dbapi_connection",
1277 "_connection_record",
1278 "_finalizer",
1279 "_echo",
1280 "_pool",
1281 "_counter",
1282 "__weakref__",
1283 "__dict__",
1284 )
1286 pool: Pool
1287 dbapi_connection: DBAPIConnection
1288 _echo: log._EchoFlagType
1290 def __init__(
1291 self,
1292 pool: Pool,
1293 dbapi_connection: DBAPIConnection,
1294 connection_record: _ConnectionRecord,
1295 echo: log._EchoFlagType,
1296 ):
1297 self._pool = pool
1298 self._counter = 0
1299 self.dbapi_connection = dbapi_connection
1300 self._connection_record = connection_record
1301 self._echo = echo
1303 # use weakref.finalize as a destructor
1304 self._finalizer = weakref.finalize(
1305 self,
1306 _finalize_fairy,
1307 None,
1308 connection_record,
1309 pool,
1310 echo,
1311 transaction_was_reset=False,
1312 is_gc_cleanup=True,
1313 )
1315 _connection_record: Optional[_ConnectionRecord]
1317 @property
1318 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501
1319 if self._connection_record is None:
1320 return None
1321 return self._connection_record.driver_connection
1323 @property
1324 @util.deprecated(
1325 "2.0",
1326 "The _ConnectionFairy.connection attribute is deprecated; "
1327 "please use 'driver_connection'",
1328 )
1329 def connection(self) -> DBAPIConnection:
1330 return self.dbapi_connection
1332 @classmethod
1333 def _checkout(
1334 cls,
1335 pool: Pool,
1336 threadconns: Optional[threading.local] = None,
1337 fairy: Optional[_ConnectionFairy] = None,
1338 ) -> _ConnectionFairy:
1339 if not fairy:
1340 fairy = _ConnectionRecord.checkout(pool)
1342 if threadconns is not None:
1343 threadconns.current = weakref.ref(fairy)
1345 assert (
1346 fairy._connection_record is not None
1347 ), "can't 'checkout' a detached connection fairy"
1348 assert (
1349 fairy.dbapi_connection is not None
1350 ), "can't 'checkout' an invalidated connection fairy"
1352 fairy._counter += 1
1353 if (
1354 not pool.dispatch.checkout and not pool._pre_ping
1355 ) or fairy._counter != 1:
1356 return fairy
1358 # Pool listeners can trigger a reconnection on checkout, as well
1359 # as the pre-pinger.
1360 # there are three attempts made here, but note that if the database
1361 # is not accessible from a connection standpoint, those won't proceed
1362 # here.
1364 attempts = 2
1366 while attempts > 0:
1367 connection_is_fresh = fairy._connection_record.fresh
1368 fairy._connection_record.fresh = False
1369 try:
1370 if pool._pre_ping:
1371 if not connection_is_fresh:
1372 if fairy._echo:
1373 pool.logger.debug(
1374 "Pool pre-ping on connection %s",
1375 fairy.dbapi_connection,
1376 )
1377 result = pool._dialect._do_ping_w_event(
1378 fairy.dbapi_connection
1379 )
1380 if not result:
1381 if fairy._echo:
1382 pool.logger.debug(
1383 "Pool pre-ping on connection %s failed, "
1384 "will invalidate pool",
1385 fairy.dbapi_connection,
1386 )
1387 raise exc.InvalidatePoolError()
1388 elif fairy._echo:
1389 pool.logger.debug(
1390 "Connection %s is fresh, skipping pre-ping",
1391 fairy.dbapi_connection,
1392 )
1394 pool.dispatch.checkout(
1395 fairy.dbapi_connection, fairy._connection_record, fairy
1396 )
1397 return fairy
1398 except exc.DisconnectionError as e:
1399 if e.invalidate_pool:
1400 pool.logger.info(
1401 "Disconnection detected on checkout, "
1402 "invalidating all pooled connections prior to "
1403 "current timestamp (reason: %r)",
1404 e,
1405 )
1406 fairy._connection_record.invalidate(e)
1407 pool._invalidate(fairy, e, _checkin=False)
1408 else:
1409 pool.logger.info(
1410 "Disconnection detected on checkout, "
1411 "invalidating individual connection %s (reason: %r)",
1412 fairy.dbapi_connection,
1413 e,
1414 )
1415 fairy._connection_record.invalidate(e)
1416 try:
1417 fairy.dbapi_connection = (
1418 fairy._connection_record.get_connection()
1419 )
1420 except BaseException as err:
1421 with util.safe_reraise():
1422 fairy._connection_record._checkin_failed(
1423 err,
1424 _fairy_was_created=True,
1425 )
1427 # prevent _ConnectionFairy from being carried
1428 # in the stack trace. Do this after the
1429 # connection record has been checked in, so that
1430 # if the del triggers a finalize fairy, it won't
1431 # try to checkin a second time.
1432 del fairy
1434 # never called, this is for code linters
1435 raise
1437 attempts -= 1
1438 except BaseException as be_outer:
1439 with util.safe_reraise():
1440 rec = fairy._connection_record
1441 if rec is not None:
1442 rec._checkin_failed(
1443 be_outer,
1444 _fairy_was_created=True,
1445 )
1447 # prevent _ConnectionFairy from being carried
1448 # in the stack trace, see above
1449 del fairy
1451 # never called, this is for code linters
1452 raise
1454 pool.logger.info("Reconnection attempts exhausted on checkout")
1455 fairy.invalidate()
1456 raise exc.InvalidRequestError("This connection is closed")
1458 def _checkout_existing(self) -> _ConnectionFairy:
1459 return _ConnectionFairy._checkout(self._pool, fairy=self)
1461 def _checkin(self, transaction_was_reset: bool = False) -> None:
1462 _finalize_fairy(
1463 self.dbapi_connection,
1464 self._connection_record,
1465 self._pool,
1466 self._echo,
1467 transaction_was_reset=transaction_was_reset,
1468 fairy=self,
1469 )
1471 def _close(self) -> None:
1472 self._checkin()
1474 def _reset(
1475 self,
1476 pool: Pool,
1477 transaction_was_reset: bool,
1478 terminate_only: bool,
1479 asyncio_safe: bool,
1480 ) -> None:
1481 if pool.dispatch.reset:
1482 pool.dispatch.reset(
1483 self.dbapi_connection,
1484 self._connection_record,
1485 PoolResetState(
1486 transaction_was_reset=transaction_was_reset,
1487 terminate_only=terminate_only,
1488 asyncio_safe=asyncio_safe,
1489 ),
1490 )
1492 if not asyncio_safe:
1493 return
1495 if pool._reset_on_return is reset_rollback:
1496 if transaction_was_reset:
1497 if self._echo:
1498 pool.logger.debug(
1499 "Connection %s reset, transaction already reset",
1500 self.dbapi_connection,
1501 )
1502 else:
1503 if self._echo:
1504 pool.logger.debug(
1505 "Connection %s rollback-on-return",
1506 self.dbapi_connection,
1507 )
1508 pool._dialect.do_rollback(self)
1509 elif pool._reset_on_return is reset_commit:
1510 if self._echo:
1511 pool.logger.debug(
1512 "Connection %s commit-on-return",
1513 self.dbapi_connection,
1514 )
1515 pool._dialect.do_commit(self)
1517 @property
1518 def _logger(self) -> log._IdentifiedLoggerType:
1519 return self._pool.logger
1521 @property
1522 def is_valid(self) -> bool:
1523 return self.dbapi_connection is not None
1525 @property
1526 def is_detached(self) -> bool:
1527 return self._connection_record is None
1529 @util.ro_memoized_property
1530 def info(self) -> _InfoType:
1531 if self._connection_record is None:
1532 return {}
1533 else:
1534 return self._connection_record.info
1536 @util.ro_non_memoized_property
1537 def record_info(self) -> Optional[_InfoType]:
1538 if self._connection_record is None:
1539 return None
1540 else:
1541 return self._connection_record.record_info
1543 def invalidate(
1544 self, e: Optional[BaseException] = None, soft: bool = False
1545 ) -> None:
1546 if self.dbapi_connection is None:
1547 util.warn("Can't invalidate an already-closed connection.")
1548 return
1549 try:
1550 if self._connection_record:
1551 self._connection_record.invalidate(e=e, soft=soft)
1552 finally:
1553 if not soft:
1554 # prevent any rollback / reset actions etc. on
1555 # the connection, including when the invalidation above
1556 # was interrupted. _checkin() then does no IO; it only
1557 # returns the record to the pool
1558 self.dbapi_connection = None # type: ignore[assignment]
1560 # finalize
1561 self._checkin()
1563 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor:
1564 assert self.dbapi_connection is not None
1565 return self.dbapi_connection.cursor(*args, **kwargs)
1567 def __getattr__(self, key: str) -> Any:
1568 return getattr(self.dbapi_connection, key)
1570 def detach(self) -> None:
1571 if self._connection_record is not None:
1572 rec = self._connection_record
1573 rec.fairy_ref = None
1575 # cancel the finalizer, as current behavior is that there's no
1576 # GC "cleanup" for a detached connection. not sure if this
1577 # is the most appropriate decision; see issue #13570
1578 self._finalizer.detach()
1580 rec.dbapi_connection = None
1581 # TODO: should this be _return_conn?
1582 self._pool._do_return_conn(self._connection_record)
1584 # can't get the descriptor assignment to work here
1585 # in pylance. mypy is OK w/ it
1586 self.info = self.info.copy() # type: ignore[misc]
1588 self._connection_record = None
1590 if self._pool.dispatch.detach:
1591 self._pool.dispatch.detach(self.dbapi_connection, rec)
1593 def close(self) -> None:
1594 self._counter -= 1
1595 if self._counter == 0:
1596 self._checkin()
1598 def _close_special(self, transaction_reset: bool = False) -> None:
1599 self._counter -= 1
1600 if self._counter == 0:
1601 self._checkin(transaction_was_reset=transaction_reset)