Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/pool/base.py: 55%
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 if (
375 terminate
376 and self._dialect.has_terminate
377 and util.is_cancellation_exception(e)
378 ):
379 # the asyncio terminate() force-closes the connection before
380 # letting a cancellation of its graceful close propagate, so
381 # the connection is closed and there is no error to report.
382 # see #13772
383 self.logger.debug(
384 "Cancellation during terminate of connection %r; "
385 "connection was force-closed",
386 connection,
387 )
388 else:
389 self.logger.error(
390 f"Exception {'terminating' if terminate else 'closing'} "
391 f"connection %r",
392 connection,
393 exc_info=True,
394 )
395 if not isinstance(e, Exception):
396 raise
398 def _create_connection(self) -> ConnectionPoolEntry:
399 """Called by subclasses to create a new ConnectionRecord."""
401 return _ConnectionRecord(self)
403 def _invalidate(
404 self,
405 connection: PoolProxiedConnection,
406 exception: Optional[BaseException] = None,
407 _checkin: bool = True,
408 ) -> None:
409 """Mark all connections established within the generation
410 of the given connection as invalidated.
412 If this pool's last invalidate time is before when the given
413 connection was created, update the timestamp til now. Otherwise,
414 no action is performed.
416 Connections with a start time prior to this pool's invalidation
417 time will be recycled upon next checkout.
418 """
419 rec = getattr(connection, "_connection_record", None)
420 if not rec or self._invalidate_time < rec.starttime:
421 self._invalidate_time = time.time()
422 if _checkin and getattr(connection, "is_valid", False):
423 connection.invalidate(exception)
425 def recreate(self) -> Pool:
426 """Return a new :class:`_pool.Pool`, of the same class as this one
427 and configured with identical creation arguments.
429 This method is used in conjunction with :meth:`dispose`
430 to close out an entire :class:`_pool.Pool` and create a new one in
431 its place.
433 """
435 raise NotImplementedError()
437 def dispose(self) -> None:
438 """Dispose of this pool.
440 This method leaves the possibility of checked-out connections
441 remaining open, as it only affects connections that are
442 idle in the pool.
444 .. seealso::
446 :meth:`Pool.recreate`
448 """
450 raise NotImplementedError()
452 def connect(self) -> PoolProxiedConnection:
453 """Return a DBAPI connection from the pool.
455 The connection is instrumented such that when its
456 ``close()`` method is called, the connection will be returned to
457 the pool.
459 """
460 return _ConnectionFairy._checkout(self)
462 def _return_conn(self, record: ConnectionPoolEntry) -> None:
463 """Given a _ConnectionRecord, return it to the :class:`_pool.Pool`.
465 This method is called when an instrumented DBAPI connection
466 has its ``close()`` method called.
468 """
469 self._do_return_conn(record)
471 def _do_get(self) -> ConnectionPoolEntry:
472 """Implementation for :meth:`get`, supplied by subclasses."""
474 raise NotImplementedError()
476 def _do_return_conn(self, record: ConnectionPoolEntry) -> None:
477 """Implementation for :meth:`return_conn`, supplied by subclasses."""
479 raise NotImplementedError()
481 def status(self) -> str:
482 """Returns a brief description of the state of this pool."""
483 raise NotImplementedError()
486class ManagesConnection:
487 """Common base for the two connection-management interfaces
488 :class:`.PoolProxiedConnection` and :class:`.ConnectionPoolEntry`.
490 These two objects are typically exposed in the public facing API
491 via the connection pool event hooks, documented at :class:`.PoolEvents`.
493 .. versionadded:: 2.0
495 """
497 __slots__ = ()
499 dbapi_connection: Optional[DBAPIConnection]
500 """A reference to the actual DBAPI connection being tracked.
502 This is a :pep:`249`-compliant object that for traditional sync-style
503 dialects is provided by the third-party
504 DBAPI implementation in use. For asyncio dialects, the implementation
505 is typically an adapter object provided by the SQLAlchemy dialect
506 itself; the underlying asyncio object is available via the
507 :attr:`.ManagesConnection.driver_connection` attribute.
509 SQLAlchemy's interface for the DBAPI connection is based on the
510 :class:`.DBAPIConnection` protocol object
512 .. seealso::
514 :attr:`.ManagesConnection.driver_connection`
516 :ref:`faq_dbapi_connection`
518 """
520 driver_connection: Optional[Any]
521 """The "driver level" connection object as used by the Python
522 DBAPI or database driver.
524 For traditional :pep:`249` DBAPI implementations, this object will
525 be the same object as that of
526 :attr:`.ManagesConnection.dbapi_connection`. For an asyncio database
527 driver, this will be the ultimate "connection" object used by that
528 driver, such as the ``asyncpg.Connection`` object which will not have
529 standard pep-249 methods.
531 .. versionadded:: 1.4.24
533 .. seealso::
535 :attr:`.ManagesConnection.dbapi_connection`
537 :ref:`faq_dbapi_connection`
539 """
541 @util.ro_memoized_property
542 def info(self) -> _InfoType:
543 """Info dictionary associated with the underlying DBAPI connection
544 referred to by this :class:`.ManagesConnection` instance, allowing
545 user-defined data to be associated with the connection.
547 The data in this dictionary is persistent for the lifespan
548 of the DBAPI connection itself, including across pool checkins
549 and checkouts. When the connection is invalidated
550 and replaced with a new one, this dictionary is cleared.
552 For a :class:`.PoolProxiedConnection` instance that's not associated
553 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the
554 attribute returns a dictionary that is local to that
555 :class:`.ConnectionPoolEntry`. Therefore the
556 :attr:`.ManagesConnection.info` attribute will always provide a Python
557 dictionary.
559 .. seealso::
561 :attr:`.ManagesConnection.record_info`
564 """
565 raise NotImplementedError()
567 @util.ro_memoized_property
568 def record_info(self) -> Optional[_InfoType]:
569 """Persistent info dictionary associated with this
570 :class:`.ManagesConnection`.
572 Unlike the :attr:`.ManagesConnection.info` dictionary, the lifespan
573 of this dictionary is that of the :class:`.ConnectionPoolEntry`
574 which owns it; therefore this dictionary will persist across
575 reconnects and connection invalidation for a particular entry
576 in the connection pool.
578 For a :class:`.PoolProxiedConnection` instance that's not associated
579 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the
580 attribute returns None. Contrast to the :attr:`.ManagesConnection.info`
581 dictionary which is never None.
584 .. seealso::
586 :attr:`.ManagesConnection.info`
588 """
589 raise NotImplementedError()
591 def invalidate(
592 self, e: Optional[BaseException] = None, soft: bool = False
593 ) -> None:
594 """Mark the managed connection as invalidated.
596 :param e: an exception object indicating a reason for the invalidation.
598 :param soft: if True, the connection isn't closed; instead, this
599 connection will be recycled on next checkout.
601 .. seealso::
603 :ref:`pool_connection_invalidation`
606 """
607 raise NotImplementedError()
610class ConnectionPoolEntry(ManagesConnection):
611 """Interface for the object that maintains an individual database
612 connection on behalf of a :class:`_pool.Pool` instance.
614 The :class:`.ConnectionPoolEntry` object represents the long term
615 maintenance of a particular connection for a pool, including expiring or
616 invalidating that connection to have it replaced with a new one, which will
617 continue to be maintained by that same :class:`.ConnectionPoolEntry`
618 instance. Compared to :class:`.PoolProxiedConnection`, which is the
619 short-term, per-checkout connection manager, this object lasts for the
620 lifespan of a particular "slot" within a connection pool.
622 The :class:`.ConnectionPoolEntry` object is mostly visible to public-facing
623 API code when it is delivered to connection pool event hooks, such as
624 :meth:`_events.PoolEvents.connect` and :meth:`_events.PoolEvents.checkout`.
626 .. versionadded:: 2.0 :class:`.ConnectionPoolEntry` provides the public
627 facing interface for the :class:`._ConnectionRecord` internal class.
629 """
631 __slots__ = ()
633 @property
634 def in_use(self) -> bool:
635 """Return True the connection is currently checked out"""
637 raise NotImplementedError()
639 def close(self) -> None:
640 """Close the DBAPI connection managed by this connection pool entry."""
641 raise NotImplementedError()
644# no-op callback for weakrefs; see _ConnectionRecord.checkout
645_NOOP_WEAKREF_CALLBACK = lambda ref: None # noqa: E731
648class _ConnectionRecord(ConnectionPoolEntry):
649 """Maintains a position in a connection pool which references a pooled
650 connection.
652 This is an internal object used by the :class:`_pool.Pool` implementation
653 to provide context management to a DBAPI connection maintained by
654 that :class:`_pool.Pool`. The public facing interface for this class
655 is described by the :class:`.ConnectionPoolEntry` class. See that
656 class for public API details.
658 .. seealso::
660 :class:`.ConnectionPoolEntry`
662 :class:`.PoolProxiedConnection`
664 """
666 __slots__ = (
667 "__pool",
668 "fairy_ref",
669 "finalize_callback",
670 "fresh",
671 "starttime",
672 "dbapi_connection",
673 "__weakref__",
674 "__dict__",
675 )
677 finalize_callback: Deque[Callable[[DBAPIConnection], None]]
678 fresh: bool
679 fairy_ref: Optional[weakref.ref[_ConnectionFairy]]
680 starttime: float
682 def __init__(self, pool: Pool, connect: bool = True):
683 self.fresh = False
684 self.fairy_ref = None
685 self.starttime = 0
686 self.dbapi_connection = None
688 self.__pool = pool
689 if connect:
690 self.__connect()
691 self.finalize_callback = deque()
693 dbapi_connection: Optional[DBAPIConnection]
695 @property
696 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501
697 if self.dbapi_connection is None:
698 return None
699 else:
700 return self.__pool._dialect.get_driver_connection(
701 self.dbapi_connection
702 )
704 @property
705 @util.deprecated(
706 "2.0",
707 "The _ConnectionRecord.connection attribute is deprecated; "
708 "please use 'driver_connection'",
709 )
710 def connection(self) -> Optional[DBAPIConnection]:
711 return self.dbapi_connection
713 _soft_invalidate_time: float = 0
715 @util.ro_memoized_property
716 def info(self) -> _InfoType:
717 return {}
719 @util.ro_memoized_property
720 def record_info(self) -> Optional[_InfoType]:
721 return {}
723 @classmethod
724 def checkout(cls, pool: Pool) -> _ConnectionFairy:
725 if TYPE_CHECKING:
726 rec = cast(_ConnectionRecord, pool._do_get())
727 else:
728 rec = pool._do_get()
730 try:
731 dbapi_connection = rec.get_connection()
732 except BaseException as err:
733 with util.safe_reraise():
734 rec._checkin_failed(err, _fairy_was_created=False)
736 # not reached, for code linters only
737 raise
739 echo = pool._should_log_debug()
740 fairy = _ConnectionFairy(pool, dbapi_connection, rec, echo)
742 # assign fairy_ref to the ConnectionRecord; note that under StaticPool,
743 # this could be swapping out an existing fairy on that record.
744 #
745 # the weakref gets a no-op callback because Python clears weakrefs
746 # that have callbacks before running any weakref callback, including
747 # the fairy's finalizer, which relies on rec.fairy_ref() being None;
748 # since Python 3.15, a weakref without a callback to an object in a
749 # reference cycle isn't cleared until after finalizers run
750 # (cpython gh-135552). #13738
751 #
752 # an alternative is for needs_gc to also treat a fairy whose
753 # finalizer was already called as gone. that requires atexit=False
754 # on the finalizer, otherwise its call at interpreter exit would
755 # reset and check in connections that are still checked out.
756 rec.fairy_ref = weakref.ref(fairy, _NOOP_WEAKREF_CALLBACK)
758 if echo:
759 pool.logger.debug(
760 "Connection %r checked out from pool", dbapi_connection
761 )
762 return fairy
764 def _checkin_failed(
765 self, err: BaseException, _fairy_was_created: bool = True
766 ) -> None:
767 try:
768 self.invalidate(e=err)
769 finally:
770 # the record must be returned in any case; nothing else will
771 # ever return it, as no _ConnectionFairy refers to it
772 self.checkin(
773 _fairy_was_created=_fairy_was_created,
774 )
776 def checkin(self, _fairy_was_created: bool = True) -> None:
777 if self.fairy_ref is None and _fairy_was_created:
778 # _fairy_was_created is False for the initial get connection phase;
779 # meaning there was no _ConnectionFairy and we must unconditionally
780 # do a checkin.
781 #
782 # otherwise, if fairy_was_created==True, if fairy_ref is None here
783 # that means we were checked in already, so this looks like
784 # a double checkin.
785 util.warn("Double checkin attempted on %s" % self)
786 return
787 self.fairy_ref = None
788 connection = self.dbapi_connection
789 pool = self.__pool
790 while self.finalize_callback:
791 finalizer = self.finalize_callback.pop()
792 if connection is not None:
793 finalizer(connection)
794 if pool.dispatch.checkin:
795 pool.dispatch.checkin(connection, self)
797 pool._return_conn(self)
799 @property
800 def in_use(self) -> bool:
801 return self.fairy_ref is not None
803 @property
804 def needs_gc(self) -> bool:
805 ref = self.fairy_ref
806 return ref is not None and ref() is None
808 @property
809 def last_connect_time(self) -> float:
810 return self.starttime
812 def close(self) -> None:
813 if self.dbapi_connection is not None:
814 self.__close()
816 def invalidate(
817 self, e: Optional[BaseException] = None, soft: bool = False
818 ) -> None:
819 # already invalidated
820 if self.dbapi_connection is None:
821 return
822 if soft:
823 self.__pool.dispatch.soft_invalidate(
824 self.dbapi_connection, self, e
825 )
826 self._log_invalidate(e, soft)
827 self._soft_invalidate_time = time.time()
828 return
830 try:
831 self.__pool.dispatch.invalidate(self.dbapi_connection, self, e)
832 self._log_invalidate(e, soft)
833 self.__close(terminate=True)
834 assert self.dbapi_connection is None
835 finally:
836 if self.dbapi_connection is not None:
837 # the hard invalidation was interrupted before it could
838 # close the connection, such as by an exception raised
839 # from the invalidate event. a close that was itself
840 # interrupted has already discarded the connection; here
841 # it's kept so that its close can be retried, but the
842 # next checkout recycles it rather than use it
843 self._soft_invalidate_time = time.time()
845 def _log_invalidate(self, e: Optional[BaseException], soft: bool) -> None:
846 if e is not None:
847 self.__pool.logger.info(
848 "%sInvalidate connection %r (reason: %s:%s)",
849 "Soft " if soft else "",
850 self.dbapi_connection,
851 e.__class__.__name__,
852 e,
853 )
854 else:
855 self.__pool.logger.info(
856 "%sInvalidate connection %r",
857 "Soft " if soft else "",
858 self.dbapi_connection,
859 )
861 def get_connection(self) -> DBAPIConnection:
862 recycle = False
864 # NOTE: the various comparisons here are assuming that measurable time
865 # passes between these state changes. however, time.time() is not
866 # guaranteed to have sub-second precision. comparisons of
867 # "invalidation time" to "starttime" should perhaps use >= so that the
868 # state change can take place assuming no measurable time has passed,
869 # however this does not guarantee correct behavior here as if time
870 # continues to not pass, it will try to reconnect repeatedly until
871 # these timestamps diverge, so in that sense using > is safer. Per
872 # https://stackoverflow.com/a/1938096/34549, Windows time.time() may be
873 # within 16 milliseconds accuracy, so unit tests for connection
874 # invalidation need a sleep of at least this long between initial start
875 # time and invalidation for the logic below to work reliably.
877 if self.dbapi_connection is None:
878 self.info.clear()
879 self.__connect()
880 elif (
881 self.__pool._recycle > -1
882 and time.time() - self.starttime > self.__pool._recycle
883 ):
884 self.__pool.logger.info(
885 "Connection %r exceeded timeout; recycling",
886 self.dbapi_connection,
887 )
888 recycle = True
889 elif self.__pool._invalidate_time > self.starttime:
890 self.__pool.logger.info(
891 "Connection %r invalidated due to pool invalidation; "
892 + "recycling",
893 self.dbapi_connection,
894 )
895 recycle = True
896 elif self._soft_invalidate_time > self.starttime:
897 self.__pool.logger.info(
898 "Connection %r invalidated due to local soft invalidation; "
899 + "recycling",
900 self.dbapi_connection,
901 )
902 recycle = True
904 if recycle:
905 self.__close(terminate=True)
906 self.info.clear()
908 self.__connect()
910 assert self.dbapi_connection is not None
911 return self.dbapi_connection
913 def _is_hard_or_soft_invalidated(self) -> bool:
914 return (
915 self.dbapi_connection is None
916 or self.__pool._invalidate_time > self.starttime
917 or (self._soft_invalidate_time > self.starttime)
918 )
920 def __close(self, *, terminate: bool = False) -> None:
921 self.finalize_callback.clear()
922 if self.__pool.dispatch.close:
923 self.__pool.dispatch.close(self.dbapi_connection, self)
924 assert self.dbapi_connection is not None
925 try:
926 self.__pool._close_connection(
927 self.dbapi_connection, terminate=terminate
928 )
929 finally:
930 # a close that was interrupted, such as by a cancellation
931 # reaching the await within an asyncio terminate(), is not
932 # retried; the connection is not to be handed out again
933 self.dbapi_connection = None
935 def __connect(self) -> None:
936 pool = self.__pool
938 # ensure any existing connection is removed, so that if
939 # creator fails, this attribute stays None
940 self.dbapi_connection = None
941 try:
942 self.starttime = time.time()
943 self.dbapi_connection = connection = pool._invoke_creator(self)
944 pool.logger.debug("Created new connection %r", connection)
945 self.fresh = True
946 except BaseException as e:
947 with util.safe_reraise():
948 pool.logger.debug("Error on connect(): %s", e)
949 else:
950 try:
951 # in SQLAlchemy 1.4 the first_connect event is not used by
952 # the engine, so this will usually not be set
953 if pool.dispatch.first_connect:
954 pool.dispatch.first_connect.for_modify(
955 pool.dispatch
956 ).exec_once_unless_exception(self.dbapi_connection, self)
958 # init of the dialect now takes place within the connect
959 # event, so ensure a mutex is used on the first run
960 pool.dispatch.connect.for_modify(
961 pool.dispatch
962 )._exec_w_sync_on_first_run(self.dbapi_connection, self)
963 except BaseException:
964 # the connection is established but the events that
965 # configure it did not complete; nothing else has a
966 # reference to it, so close it here or it is stranded.
967 # for an asyncio driver in particular there is no other
968 # opportunity, as the garbage collector cannot close a
969 # connection that needs the event loop
970 with util.safe_reraise():
971 try:
972 pool._close_connection(connection, terminate=True)
973 except Exception:
974 pool.logger.exception(
975 "Exception closing connection %r stranded by a "
976 "failed connect event",
977 connection,
978 )
979 finally:
980 # a cancellation reaching the await within an
981 # asyncio terminate() propagates in place of the
982 # original exception
983 self.dbapi_connection = None
986def _finalize_fairy(
987 dbapi_connection: Optional[DBAPIConnection],
988 connection_record: Optional[_ConnectionRecord],
989 pool: Pool,
990 echo: Optional[log._EchoFlagType],
991 transaction_was_reset: bool = False,
992 fairy: Optional[_ConnectionFairy] = None,
993 is_gc_cleanup: bool = False,
994) -> None:
995 """Cleanup for a :class:`._ConnectionFairy` whether or not it's already
996 been garbage collected.
998 When using an async dialect no IO can happen here (without using
999 a dedicated thread), since this is called outside the greenlet
1000 context and with an already running loop. In this case function
1001 will only log a message and raise a warning.
1002 """
1004 if is_gc_cleanup:
1005 assert connection_record is not None
1007 # check connection record to see that we're the current
1008 # fairy for this record. if not, then return; assume the
1009 # record is either checked in, or another fairy supersedes us
1010 # (can happen with StaticPool)
1011 if not connection_record.needs_gc:
1012 return
1013 assert dbapi_connection is None
1014 dbapi_connection = connection_record.dbapi_connection
1016 # null pool is not _is_asyncio but can be used also with async dialects
1017 dont_restore_gced = pool._dialect.is_async
1019 if dont_restore_gced:
1020 detach = connection_record is None or is_gc_cleanup
1021 can_manipulate_connection = not is_gc_cleanup
1022 can_close_or_terminate_connection = (
1023 not pool._dialect.is_async or pool._dialect.has_terminate
1024 )
1025 requires_terminate_for_close = (
1026 pool._dialect.is_async and pool._dialect.has_terminate
1027 )
1029 else:
1030 detach = connection_record is None
1031 can_manipulate_connection = can_close_or_terminate_connection = True
1032 requires_terminate_for_close = False
1034 if dbapi_connection is not None:
1035 if connection_record and echo:
1036 pool.logger.debug(
1037 "Connection %r being returned to pool", dbapi_connection
1038 )
1040 try:
1041 if not fairy:
1042 assert connection_record is not None
1043 fairy = _ConnectionFairy(
1044 pool,
1045 dbapi_connection,
1046 connection_record,
1047 echo,
1048 )
1049 assert fairy.dbapi_connection is dbapi_connection
1051 fairy._reset(
1052 pool,
1053 transaction_was_reset=transaction_was_reset,
1054 terminate_only=detach,
1055 asyncio_safe=can_manipulate_connection,
1056 )
1058 if detach:
1059 if connection_record:
1060 fairy._pool = pool
1061 fairy.detach()
1063 if can_close_or_terminate_connection:
1064 if pool.dispatch.close_detached:
1065 pool.dispatch.close_detached(dbapi_connection)
1067 pool._close_connection(
1068 dbapi_connection,
1069 terminate=requires_terminate_for_close,
1070 )
1072 except BaseException as e:
1073 pool.logger.error(
1074 "Exception during reset or similar", exc_info=True
1075 )
1076 if not isinstance(e, Exception):
1077 # a cancellation or similar propagates from here, so the
1078 # checkin() below is not reached; return the record now,
1079 # rather than leaving it to garbage collection of the fairy.
1080 # the cleanup of the fairy below is not reached either, so
1081 # detach it from the record first, so that it can't act on
1082 # the record once that's checked out again. see #13699
1083 if fairy is not None:
1084 fairy._finalizer.detach()
1085 fairy.dbapi_connection = None # type: ignore[assignment]
1086 fairy._connection_record = None
1087 if connection_record:
1088 connection_record._checkin_failed(e)
1089 raise
1090 if connection_record:
1091 connection_record.invalidate(e=e)
1092 finally:
1093 if detach and is_gc_cleanup and dont_restore_gced:
1094 message = (
1095 "The garbage collector is trying to clean up "
1096 f"non-checked-in connection {dbapi_connection!r}, "
1097 f"""which will be {
1098 'dropped, as it cannot be safely terminated'
1099 if not can_close_or_terminate_connection
1100 else 'terminated'
1101 }. """
1102 "Please ensure that SQLAlchemy pooled connections are "
1103 "returned to "
1104 "the pool explicitly, either by calling ``close()`` "
1105 "or by using appropriate context managers to manage "
1106 "their lifecycle."
1107 )
1108 pool.logger.error(message)
1109 util.warn(message)
1111 if connection_record and connection_record.in_use:
1112 connection_record.checkin()
1114 # give gc some help. See
1115 # test/engine/test_pool.py::PoolEventsTest::test_checkin_event_gc[True]
1116 # which actually started failing when pytest warnings plugin was
1117 # turned on, due to util.warn() above
1118 if fairy is not None:
1119 # don't need the finalizer anymore since we are cleaning up here
1120 fairy._finalizer.detach()
1121 fairy.dbapi_connection = None # type: ignore[assignment]
1122 fairy._connection_record = None
1123 del dbapi_connection
1124 del connection_record
1125 del fairy
1128class PoolProxiedConnection(ManagesConnection):
1129 """A connection-like adapter for a :pep:`249` DBAPI connection, which
1130 includes additional methods specific to the :class:`.Pool` implementation.
1132 :class:`.PoolProxiedConnection` is the public-facing interface for the
1133 internal :class:`._ConnectionFairy` implementation object; users familiar
1134 with :class:`._ConnectionFairy` can consider this object to be equivalent.
1136 .. versionadded:: 2.0 :class:`.PoolProxiedConnection` provides the public-
1137 facing interface for the :class:`._ConnectionFairy` internal class.
1139 """
1141 __slots__ = ()
1143 if typing.TYPE_CHECKING:
1145 def commit(self) -> None: ...
1147 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor: ...
1149 def rollback(self) -> None: ...
1151 def __getattr__(self, key: str) -> Any: ...
1153 @property
1154 def is_valid(self) -> bool:
1155 """Return True if this :class:`.PoolProxiedConnection` still refers
1156 to an active DBAPI connection."""
1158 raise NotImplementedError()
1160 @property
1161 def is_detached(self) -> bool:
1162 """Return True if this :class:`.PoolProxiedConnection` is detached
1163 from its pool."""
1165 raise NotImplementedError()
1167 def detach(self) -> None:
1168 """Separate this connection from its Pool.
1170 This means that the connection will no longer be returned to the
1171 pool when closed, and will instead be literally closed. The
1172 associated :class:`.ConnectionPoolEntry` is de-associated from this
1173 DBAPI connection.
1175 Note that any overall connection limiting constraints imposed by a
1176 Pool implementation may be violated after a detach, as the detached
1177 connection is removed from the pool's knowledge and control.
1179 """
1181 raise NotImplementedError()
1183 def close(self) -> None:
1184 """Release this connection back to the pool.
1186 The :meth:`.PoolProxiedConnection.close` method shadows the
1187 :pep:`249` ``.close()`` method, altering its behavior to instead
1188 :term:`release` the proxied connection back to the connection pool.
1190 Upon release to the pool, whether the connection stays "opened" and
1191 pooled in the Python process, versus actually closed out and removed
1192 from the Python process, is based on the pool implementation in use and
1193 its configuration and current state.
1195 """
1196 raise NotImplementedError()
1198 def __enter__(self) -> Self:
1199 return self
1201 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
1202 self.close()
1203 return None
1206class _AdhocProxiedConnection(PoolProxiedConnection):
1207 """provides the :class:`.PoolProxiedConnection` interface for cases where
1208 the DBAPI connection is not actually proxied.
1210 This is used by the engine internals to pass a consistent
1211 :class:`.PoolProxiedConnection` object to consuming dialects in response to
1212 pool events that may not always have the :class:`._ConnectionFairy`
1213 available.
1215 """
1217 __slots__ = ("dbapi_connection", "_connection_record", "_is_valid")
1219 dbapi_connection: DBAPIConnection
1220 _connection_record: ConnectionPoolEntry
1222 def __init__(
1223 self,
1224 dbapi_connection: DBAPIConnection,
1225 connection_record: ConnectionPoolEntry,
1226 ):
1227 self.dbapi_connection = dbapi_connection
1228 self._connection_record = connection_record
1229 self._is_valid = True
1231 @property
1232 def driver_connection(self) -> Any: # type: ignore[override] # mypy#4125
1233 return self._connection_record.driver_connection
1235 @property
1236 def connection(self) -> DBAPIConnection:
1237 return self.dbapi_connection
1239 @property
1240 def is_valid(self) -> bool:
1241 """Implement is_valid state attribute.
1243 for the adhoc proxied connection it's assumed the connection is valid
1244 as there is no "invalidate" routine.
1246 """
1247 return self._is_valid
1249 def invalidate(
1250 self, e: Optional[BaseException] = None, soft: bool = False
1251 ) -> None:
1252 self._is_valid = False
1254 @util.ro_non_memoized_property
1255 def record_info(self) -> Optional[_InfoType]:
1256 return self._connection_record.record_info
1258 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor:
1259 return self.dbapi_connection.cursor(*args, **kwargs)
1261 def __getattr__(self, key: Any) -> Any:
1262 return getattr(self.dbapi_connection, key)
1265class _ConnectionFairy(PoolProxiedConnection):
1266 """Proxies a DBAPI connection and provides return-on-dereference
1267 support.
1269 This is an internal object used by the :class:`_pool.Pool` implementation
1270 to provide context management to a DBAPI connection delivered by
1271 that :class:`_pool.Pool`. The public facing interface for this class
1272 is described by the :class:`.PoolProxiedConnection` class. See that
1273 class for public API details.
1275 The name "fairy" is inspired by the fact that the
1276 :class:`._ConnectionFairy` object's lifespan is transitory, as it lasts
1277 only for the length of a specific DBAPI connection being checked out from
1278 the pool, and additionally that as a transparent proxy, it is mostly
1279 invisible.
1281 .. seealso::
1283 :class:`.PoolProxiedConnection`
1285 :class:`.ConnectionPoolEntry`
1288 """
1290 __slots__ = (
1291 "dbapi_connection",
1292 "_connection_record",
1293 "_finalizer",
1294 "_echo",
1295 "_pool",
1296 "_counter",
1297 "__weakref__",
1298 "__dict__",
1299 )
1301 pool: Pool
1302 dbapi_connection: DBAPIConnection
1303 _echo: log._EchoFlagType
1305 def __init__(
1306 self,
1307 pool: Pool,
1308 dbapi_connection: DBAPIConnection,
1309 connection_record: _ConnectionRecord,
1310 echo: log._EchoFlagType,
1311 ):
1312 self._pool = pool
1313 self._counter = 0
1314 self.dbapi_connection = dbapi_connection
1315 self._connection_record = connection_record
1316 self._echo = echo
1318 # use weakref.finalize as a destructor
1319 self._finalizer = weakref.finalize(
1320 self,
1321 _finalize_fairy,
1322 None,
1323 connection_record,
1324 pool,
1325 echo,
1326 transaction_was_reset=False,
1327 is_gc_cleanup=True,
1328 )
1330 _connection_record: Optional[_ConnectionRecord]
1332 @property
1333 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501
1334 if self._connection_record is None:
1335 return None
1336 return self._connection_record.driver_connection
1338 @property
1339 @util.deprecated(
1340 "2.0",
1341 "The _ConnectionFairy.connection attribute is deprecated; "
1342 "please use 'driver_connection'",
1343 )
1344 def connection(self) -> DBAPIConnection:
1345 return self.dbapi_connection
1347 @classmethod
1348 def _checkout(
1349 cls,
1350 pool: Pool,
1351 threadconns: Optional[threading.local] = None,
1352 fairy: Optional[_ConnectionFairy] = None,
1353 ) -> _ConnectionFairy:
1354 if not fairy:
1355 fairy = _ConnectionRecord.checkout(pool)
1357 if threadconns is not None:
1358 threadconns.current = weakref.ref(fairy)
1360 assert (
1361 fairy._connection_record is not None
1362 ), "can't 'checkout' a detached connection fairy"
1363 assert (
1364 fairy.dbapi_connection is not None
1365 ), "can't 'checkout' an invalidated connection fairy"
1367 fairy._counter += 1
1368 if (
1369 not pool.dispatch.checkout and not pool._pre_ping
1370 ) or fairy._counter != 1:
1371 return fairy
1373 # Pool listeners can trigger a reconnection on checkout, as well
1374 # as the pre-pinger.
1375 # there are three attempts made here, but note that if the database
1376 # is not accessible from a connection standpoint, those won't proceed
1377 # here.
1379 attempts = 2
1381 while attempts > 0:
1382 connection_is_fresh = fairy._connection_record.fresh
1383 fairy._connection_record.fresh = False
1384 try:
1385 if pool._pre_ping:
1386 if not connection_is_fresh:
1387 if fairy._echo:
1388 pool.logger.debug(
1389 "Pool pre-ping on connection %s",
1390 fairy.dbapi_connection,
1391 )
1392 result = pool._dialect._do_ping_w_event(
1393 fairy.dbapi_connection
1394 )
1395 if not result:
1396 if fairy._echo:
1397 pool.logger.debug(
1398 "Pool pre-ping on connection %s failed, "
1399 "will invalidate pool",
1400 fairy.dbapi_connection,
1401 )
1402 raise exc.InvalidatePoolError()
1403 elif fairy._echo:
1404 pool.logger.debug(
1405 "Connection %s is fresh, skipping pre-ping",
1406 fairy.dbapi_connection,
1407 )
1409 pool.dispatch.checkout(
1410 fairy.dbapi_connection, fairy._connection_record, fairy
1411 )
1412 return fairy
1413 except exc.DisconnectionError as e:
1414 if e.invalidate_pool:
1415 pool.logger.info(
1416 "Disconnection detected on checkout, "
1417 "invalidating all pooled connections prior to "
1418 "current timestamp (reason: %r)",
1419 e,
1420 )
1421 fairy._connection_record.invalidate(e)
1422 pool._invalidate(fairy, e, _checkin=False)
1423 else:
1424 pool.logger.info(
1425 "Disconnection detected on checkout, "
1426 "invalidating individual connection %s (reason: %r)",
1427 fairy.dbapi_connection,
1428 e,
1429 )
1430 fairy._connection_record.invalidate(e)
1431 try:
1432 fairy.dbapi_connection = (
1433 fairy._connection_record.get_connection()
1434 )
1435 except BaseException as err:
1436 with util.safe_reraise():
1437 fairy._connection_record._checkin_failed(
1438 err,
1439 _fairy_was_created=True,
1440 )
1442 # prevent _ConnectionFairy from being carried
1443 # in the stack trace. Do this after the
1444 # connection record has been checked in, so that
1445 # if the del triggers a finalize fairy, it won't
1446 # try to checkin a second time.
1447 del fairy
1449 # never called, this is for code linters
1450 raise
1452 attempts -= 1
1453 except BaseException as be_outer:
1454 with util.safe_reraise():
1455 rec = fairy._connection_record
1456 if rec is not None:
1457 rec._checkin_failed(
1458 be_outer,
1459 _fairy_was_created=True,
1460 )
1462 # prevent _ConnectionFairy from being carried
1463 # in the stack trace, see above
1464 del fairy
1466 # never called, this is for code linters
1467 raise
1469 pool.logger.info("Reconnection attempts exhausted on checkout")
1470 fairy.invalidate()
1471 raise exc.InvalidRequestError("This connection is closed")
1473 def _checkout_existing(self) -> _ConnectionFairy:
1474 return _ConnectionFairy._checkout(self._pool, fairy=self)
1476 def _checkin(self, transaction_was_reset: bool = False) -> None:
1477 _finalize_fairy(
1478 self.dbapi_connection,
1479 self._connection_record,
1480 self._pool,
1481 self._echo,
1482 transaction_was_reset=transaction_was_reset,
1483 fairy=self,
1484 )
1486 def _close(self) -> None:
1487 self._checkin()
1489 def _reset(
1490 self,
1491 pool: Pool,
1492 transaction_was_reset: bool,
1493 terminate_only: bool,
1494 asyncio_safe: bool,
1495 ) -> None:
1496 if pool.dispatch.reset:
1497 pool.dispatch.reset(
1498 self.dbapi_connection,
1499 self._connection_record,
1500 PoolResetState(
1501 transaction_was_reset=transaction_was_reset,
1502 terminate_only=terminate_only,
1503 asyncio_safe=asyncio_safe,
1504 ),
1505 )
1507 if not asyncio_safe:
1508 return
1510 if pool._reset_on_return is reset_rollback:
1511 if transaction_was_reset:
1512 if self._echo:
1513 pool.logger.debug(
1514 "Connection %s reset, transaction already reset",
1515 self.dbapi_connection,
1516 )
1517 else:
1518 if self._echo:
1519 pool.logger.debug(
1520 "Connection %s rollback-on-return",
1521 self.dbapi_connection,
1522 )
1523 pool._dialect.do_rollback(self)
1524 elif pool._reset_on_return is reset_commit:
1525 if self._echo:
1526 pool.logger.debug(
1527 "Connection %s commit-on-return",
1528 self.dbapi_connection,
1529 )
1530 pool._dialect.do_commit(self)
1532 @property
1533 def _logger(self) -> log._IdentifiedLoggerType:
1534 return self._pool.logger
1536 @property
1537 def is_valid(self) -> bool:
1538 return self.dbapi_connection is not None
1540 @property
1541 def is_detached(self) -> bool:
1542 return self._connection_record is None
1544 @util.ro_memoized_property
1545 def info(self) -> _InfoType:
1546 if self._connection_record is None:
1547 return {}
1548 else:
1549 return self._connection_record.info
1551 @util.ro_non_memoized_property
1552 def record_info(self) -> Optional[_InfoType]:
1553 if self._connection_record is None:
1554 return None
1555 else:
1556 return self._connection_record.record_info
1558 def invalidate(
1559 self, e: Optional[BaseException] = None, soft: bool = False
1560 ) -> None:
1561 if self.dbapi_connection is None:
1562 util.warn("Can't invalidate an already-closed connection.")
1563 return
1564 try:
1565 if self._connection_record:
1566 self._connection_record.invalidate(e=e, soft=soft)
1567 finally:
1568 if not soft:
1569 # prevent any rollback / reset actions etc. on
1570 # the connection, including when the invalidation above
1571 # was interrupted. _checkin() then does no IO; it only
1572 # returns the record to the pool
1573 self.dbapi_connection = None # type: ignore[assignment]
1575 # finalize
1576 self._checkin()
1578 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor:
1579 assert self.dbapi_connection is not None
1580 return self.dbapi_connection.cursor(*args, **kwargs)
1582 def __getattr__(self, key: str) -> Any:
1583 return getattr(self.dbapi_connection, key)
1585 def detach(self) -> None:
1586 if self._connection_record is not None:
1587 rec = self._connection_record
1588 rec.fairy_ref = None
1590 # cancel the finalizer, as current behavior is that there's no
1591 # GC "cleanup" for a detached connection. not sure if this
1592 # is the most appropriate decision; see issue #13570
1593 self._finalizer.detach()
1595 rec.dbapi_connection = None
1596 # TODO: should this be _return_conn?
1597 self._pool._do_return_conn(self._connection_record)
1599 # can't get the descriptor assignment to work here
1600 # in pylance. mypy is OK w/ it
1601 self.info = self.info.copy() # type: ignore[misc]
1603 self._connection_record = None
1605 if self._pool.dispatch.detach:
1606 self._pool.dispatch.detach(self.dbapi_connection, rec)
1608 def close(self) -> None:
1609 self._counter -= 1
1610 if self._counter == 0:
1611 self._checkin()
1613 def _close_special(self, transaction_reset: bool = False) -> None:
1614 self._counter -= 1
1615 if self._counter == 0:
1616 self._checkin(transaction_was_reset=transaction_reset)