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

590 statements  

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 

7 

8 

9"""Base constructs for connection pools.""" 

10 

11from __future__ import annotations 

12 

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 

31 

32from .. import event 

33from .. import exc 

34from .. import log 

35from .. import util 

36from ..util.typing import Self 

37 

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 

46 

47 

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. 

52 

53 .. versionadded:: 2.0.0b3 

54 

55 """ 

56 

57 __slots__ = ("transaction_was_reset", "terminate_only", "asyncio_safe") 

58 

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. 

62 

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. 

69 

70 """ 

71 

72 terminate_only: bool 

73 """indicates if the connection is to be immediately terminated and 

74 not checked in to the pool. 

75 

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. 

81 

82 """ 

83 

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. 

87 

88 Will be False in the case that the connection is being garbage collected. 

89 

90 """ 

91 

92 

93class ResetStyle(Enum): 

94 """Describe options for "reset on return" behaviors.""" 

95 

96 reset_rollback = 0 

97 reset_commit = 1 

98 reset_none = 2 

99 

100 

101_ResetStyleArgType = Union[ 

102 ResetStyle, 

103 Literal[True, None, False, "commit", "rollback"], 

104] 

105reset_rollback, reset_commit, reset_none = list(ResetStyle) 

106 

107 

108class _ConnDialect: 

109 """partial implementation of :class:`.Dialect` 

110 which provides DBAPI connection methods. 

111 

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`. 

115 

116 """ 

117 

118 is_async = False 

119 has_terminate = False 

120 

121 def do_rollback(self, dbapi_connection: PoolProxiedConnection) -> None: 

122 dbapi_connection.rollback() 

123 

124 def do_commit(self, dbapi_connection: PoolProxiedConnection) -> None: 

125 dbapi_connection.commit() 

126 

127 def do_terminate(self, dbapi_connection: DBAPIConnection) -> None: 

128 dbapi_connection.close() 

129 

130 def do_close(self, dbapi_connection: DBAPIConnection) -> None: 

131 dbapi_connection.close() 

132 

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 ) 

138 

139 def get_driver_connection(self, connection: DBAPIConnection) -> Any: 

140 return connection 

141 

142 

143class _AsyncConnDialect(_ConnDialect): 

144 is_async = True 

145 

146 

147class _CreatorFnType(Protocol): 

148 def __call__(self) -> DBAPIConnection: ... 

149 

150 

151class _CreatorWRecFnType(Protocol): 

152 def __call__(self, rec: ConnectionPoolEntry) -> DBAPIConnection: ... 

153 

154 

155class Pool(log.Identified, event.EventTarget): 

156 """Abstract base class for connection pools.""" 

157 

158 dispatch: dispatcher[Pool] 

159 echo: log._EchoFlagType 

160 

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 

166 

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. 

181 

182 :param creator: a callable function that returns a DB-API 

183 connection object. The function will be called with 

184 parameters. 

185 

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. 

190 

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. 

195 

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. 

201 

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. 

205 

206 .. seealso:: 

207 

208 :ref:`dbengine_logging` - further detail on how to configure 

209 logging. 

210 

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. 

216 

217 :paramref:`_pool.Pool.reset_on_return` can have any of these values: 

218 

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. 

236 

237 * ``True`` - same as 'rollback', this is here for 

238 backwards compatibility. 

239 * ``False`` - same as None, this is here for 

240 backwards compatibility. 

241 

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. 

245 

246 .. seealso:: 

247 

248 :ref:`pool_reset_on_return` 

249 

250 :meth:`.PoolEvents.reset` 

251 

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. 

257 

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. 

263 

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. 

271 

272 """ 

273 if logging_name: 

274 self.logging_name = self._orig_logging_name = logging_name 

275 else: 

276 self._orig_logging_name = None 

277 

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 ) 

292 

293 self.echo = echo 

294 

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) 

302 

303 @util.hybridproperty 

304 def _is_asyncio(self) -> bool: 

305 return self._dialect.is_async 

306 

307 @property 

308 def _creator(self) -> Union[_CreatorFnType, _CreatorWRecFnType]: 

309 return self._creator_arg 

310 

311 @_creator.setter 

312 def _creator( 

313 self, creator: Union[_CreatorFnType, _CreatorWRecFnType] 

314 ) -> None: 

315 self._creator_arg = creator 

316 

317 # mypy seems to get super confused assigning functions to 

318 # attributes 

319 self._invoke_creator = self._should_wrap_creator(creator) 

320 

321 @_creator.deleter 

322 def _creator(self) -> None: 

323 # needed for mock testing 

324 del self._creator_arg 

325 del self._invoke_creator 

326 

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. 

332 

333 """ 

334 

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() 

340 

341 if argspec.defaults is not None: 

342 defaulted = len(argspec.defaults) 

343 else: 

344 defaulted = 0 

345 positionals = len(argspec[0]) - defaulted 

346 

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() 

359 

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 

397 

398 def _create_connection(self) -> ConnectionPoolEntry: 

399 """Called by subclasses to create a new ConnectionRecord.""" 

400 

401 return _ConnectionRecord(self) 

402 

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. 

411 

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. 

415 

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) 

424 

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. 

428 

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. 

432 

433 """ 

434 

435 raise NotImplementedError() 

436 

437 def dispose(self) -> None: 

438 """Dispose of this pool. 

439 

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. 

443 

444 .. seealso:: 

445 

446 :meth:`Pool.recreate` 

447 

448 """ 

449 

450 raise NotImplementedError() 

451 

452 def connect(self) -> PoolProxiedConnection: 

453 """Return a DBAPI connection from the pool. 

454 

455 The connection is instrumented such that when its 

456 ``close()`` method is called, the connection will be returned to 

457 the pool. 

458 

459 """ 

460 return _ConnectionFairy._checkout(self) 

461 

462 def _return_conn(self, record: ConnectionPoolEntry) -> None: 

463 """Given a _ConnectionRecord, return it to the :class:`_pool.Pool`. 

464 

465 This method is called when an instrumented DBAPI connection 

466 has its ``close()`` method called. 

467 

468 """ 

469 self._do_return_conn(record) 

470 

471 def _do_get(self) -> ConnectionPoolEntry: 

472 """Implementation for :meth:`get`, supplied by subclasses.""" 

473 

474 raise NotImplementedError() 

475 

476 def _do_return_conn(self, record: ConnectionPoolEntry) -> None: 

477 """Implementation for :meth:`return_conn`, supplied by subclasses.""" 

478 

479 raise NotImplementedError() 

480 

481 def status(self) -> str: 

482 """Returns a brief description of the state of this pool.""" 

483 raise NotImplementedError() 

484 

485 

486class ManagesConnection: 

487 """Common base for the two connection-management interfaces 

488 :class:`.PoolProxiedConnection` and :class:`.ConnectionPoolEntry`. 

489 

490 These two objects are typically exposed in the public facing API 

491 via the connection pool event hooks, documented at :class:`.PoolEvents`. 

492 

493 .. versionadded:: 2.0 

494 

495 """ 

496 

497 __slots__ = () 

498 

499 dbapi_connection: Optional[DBAPIConnection] 

500 """A reference to the actual DBAPI connection being tracked. 

501 

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. 

508 

509 SQLAlchemy's interface for the DBAPI connection is based on the 

510 :class:`.DBAPIConnection` protocol object 

511 

512 .. seealso:: 

513 

514 :attr:`.ManagesConnection.driver_connection` 

515 

516 :ref:`faq_dbapi_connection` 

517 

518 """ 

519 

520 driver_connection: Optional[Any] 

521 """The "driver level" connection object as used by the Python 

522 DBAPI or database driver. 

523 

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. 

530 

531 .. versionadded:: 1.4.24 

532 

533 .. seealso:: 

534 

535 :attr:`.ManagesConnection.dbapi_connection` 

536 

537 :ref:`faq_dbapi_connection` 

538 

539 """ 

540 

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. 

546 

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. 

551 

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. 

558 

559 .. seealso:: 

560 

561 :attr:`.ManagesConnection.record_info` 

562 

563 

564 """ 

565 raise NotImplementedError() 

566 

567 @util.ro_memoized_property 

568 def record_info(self) -> Optional[_InfoType]: 

569 """Persistent info dictionary associated with this 

570 :class:`.ManagesConnection`. 

571 

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. 

577 

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. 

582 

583 

584 .. seealso:: 

585 

586 :attr:`.ManagesConnection.info` 

587 

588 """ 

589 raise NotImplementedError() 

590 

591 def invalidate( 

592 self, e: Optional[BaseException] = None, soft: bool = False 

593 ) -> None: 

594 """Mark the managed connection as invalidated. 

595 

596 :param e: an exception object indicating a reason for the invalidation. 

597 

598 :param soft: if True, the connection isn't closed; instead, this 

599 connection will be recycled on next checkout. 

600 

601 .. seealso:: 

602 

603 :ref:`pool_connection_invalidation` 

604 

605 

606 """ 

607 raise NotImplementedError() 

608 

609 

610class ConnectionPoolEntry(ManagesConnection): 

611 """Interface for the object that maintains an individual database 

612 connection on behalf of a :class:`_pool.Pool` instance. 

613 

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. 

621 

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`. 

625 

626 .. versionadded:: 2.0 :class:`.ConnectionPoolEntry` provides the public 

627 facing interface for the :class:`._ConnectionRecord` internal class. 

628 

629 """ 

630 

631 __slots__ = () 

632 

633 @property 

634 def in_use(self) -> bool: 

635 """Return True the connection is currently checked out""" 

636 

637 raise NotImplementedError() 

638 

639 def close(self) -> None: 

640 """Close the DBAPI connection managed by this connection pool entry.""" 

641 raise NotImplementedError() 

642 

643 

644# no-op callback for weakrefs; see _ConnectionRecord.checkout 

645_NOOP_WEAKREF_CALLBACK = lambda ref: None # noqa: E731 

646 

647 

648class _ConnectionRecord(ConnectionPoolEntry): 

649 """Maintains a position in a connection pool which references a pooled 

650 connection. 

651 

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. 

657 

658 .. seealso:: 

659 

660 :class:`.ConnectionPoolEntry` 

661 

662 :class:`.PoolProxiedConnection` 

663 

664 """ 

665 

666 __slots__ = ( 

667 "__pool", 

668 "fairy_ref", 

669 "finalize_callback", 

670 "fresh", 

671 "starttime", 

672 "dbapi_connection", 

673 "__weakref__", 

674 "__dict__", 

675 ) 

676 

677 finalize_callback: Deque[Callable[[DBAPIConnection], None]] 

678 fresh: bool 

679 fairy_ref: Optional[weakref.ref[_ConnectionFairy]] 

680 starttime: float 

681 

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 

687 

688 self.__pool = pool 

689 if connect: 

690 self.__connect() 

691 self.finalize_callback = deque() 

692 

693 dbapi_connection: Optional[DBAPIConnection] 

694 

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 ) 

703 

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 

712 

713 _soft_invalidate_time: float = 0 

714 

715 @util.ro_memoized_property 

716 def info(self) -> _InfoType: 

717 return {} 

718 

719 @util.ro_memoized_property 

720 def record_info(self) -> Optional[_InfoType]: 

721 return {} 

722 

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() 

729 

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) 

735 

736 # not reached, for code linters only 

737 raise 

738 

739 echo = pool._should_log_debug() 

740 fairy = _ConnectionFairy(pool, dbapi_connection, rec, echo) 

741 

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) 

757 

758 if echo: 

759 pool.logger.debug( 

760 "Connection %r checked out from pool", dbapi_connection 

761 ) 

762 return fairy 

763 

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 ) 

775 

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) 

796 

797 pool._return_conn(self) 

798 

799 @property 

800 def in_use(self) -> bool: 

801 return self.fairy_ref is not None 

802 

803 @property 

804 def needs_gc(self) -> bool: 

805 ref = self.fairy_ref 

806 return ref is not None and ref() is None 

807 

808 @property 

809 def last_connect_time(self) -> float: 

810 return self.starttime 

811 

812 def close(self) -> None: 

813 if self.dbapi_connection is not None: 

814 self.__close() 

815 

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 

829 

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() 

844 

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 ) 

860 

861 def get_connection(self) -> DBAPIConnection: 

862 recycle = False 

863 

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. 

876 

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 

903 

904 if recycle: 

905 self.__close(terminate=True) 

906 self.info.clear() 

907 

908 self.__connect() 

909 

910 assert self.dbapi_connection is not None 

911 return self.dbapi_connection 

912 

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 ) 

919 

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 

934 

935 def __connect(self) -> None: 

936 pool = self.__pool 

937 

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) 

957 

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 

984 

985 

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. 

997 

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

1003 

1004 if is_gc_cleanup: 

1005 assert connection_record is not None 

1006 

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 

1015 

1016 # null pool is not _is_asyncio but can be used also with async dialects 

1017 dont_restore_gced = pool._dialect.is_async 

1018 

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 ) 

1028 

1029 else: 

1030 detach = connection_record is None 

1031 can_manipulate_connection = can_close_or_terminate_connection = True 

1032 requires_terminate_for_close = False 

1033 

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 ) 

1039 

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 

1050 

1051 fairy._reset( 

1052 pool, 

1053 transaction_was_reset=transaction_was_reset, 

1054 terminate_only=detach, 

1055 asyncio_safe=can_manipulate_connection, 

1056 ) 

1057 

1058 if detach: 

1059 if connection_record: 

1060 fairy._pool = pool 

1061 fairy.detach() 

1062 

1063 if can_close_or_terminate_connection: 

1064 if pool.dispatch.close_detached: 

1065 pool.dispatch.close_detached(dbapi_connection) 

1066 

1067 pool._close_connection( 

1068 dbapi_connection, 

1069 terminate=requires_terminate_for_close, 

1070 ) 

1071 

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) 

1110 

1111 if connection_record and connection_record.in_use: 

1112 connection_record.checkin() 

1113 

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 

1126 

1127 

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. 

1131 

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. 

1135 

1136 .. versionadded:: 2.0 :class:`.PoolProxiedConnection` provides the public- 

1137 facing interface for the :class:`._ConnectionFairy` internal class. 

1138 

1139 """ 

1140 

1141 __slots__ = () 

1142 

1143 if typing.TYPE_CHECKING: 

1144 

1145 def commit(self) -> None: ... 

1146 

1147 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor: ... 

1148 

1149 def rollback(self) -> None: ... 

1150 

1151 def __getattr__(self, key: str) -> Any: ... 

1152 

1153 @property 

1154 def is_valid(self) -> bool: 

1155 """Return True if this :class:`.PoolProxiedConnection` still refers 

1156 to an active DBAPI connection.""" 

1157 

1158 raise NotImplementedError() 

1159 

1160 @property 

1161 def is_detached(self) -> bool: 

1162 """Return True if this :class:`.PoolProxiedConnection` is detached 

1163 from its pool.""" 

1164 

1165 raise NotImplementedError() 

1166 

1167 def detach(self) -> None: 

1168 """Separate this connection from its Pool. 

1169 

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. 

1174 

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. 

1178 

1179 """ 

1180 

1181 raise NotImplementedError() 

1182 

1183 def close(self) -> None: 

1184 """Release this connection back to the pool. 

1185 

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. 

1189 

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. 

1194 

1195 """ 

1196 raise NotImplementedError() 

1197 

1198 def __enter__(self) -> Self: 

1199 return self 

1200 

1201 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 

1202 self.close() 

1203 return None 

1204 

1205 

1206class _AdhocProxiedConnection(PoolProxiedConnection): 

1207 """provides the :class:`.PoolProxiedConnection` interface for cases where 

1208 the DBAPI connection is not actually proxied. 

1209 

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. 

1214 

1215 """ 

1216 

1217 __slots__ = ("dbapi_connection", "_connection_record", "_is_valid") 

1218 

1219 dbapi_connection: DBAPIConnection 

1220 _connection_record: ConnectionPoolEntry 

1221 

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 

1230 

1231 @property 

1232 def driver_connection(self) -> Any: # type: ignore[override] # mypy#4125 

1233 return self._connection_record.driver_connection 

1234 

1235 @property 

1236 def connection(self) -> DBAPIConnection: 

1237 return self.dbapi_connection 

1238 

1239 @property 

1240 def is_valid(self) -> bool: 

1241 """Implement is_valid state attribute. 

1242 

1243 for the adhoc proxied connection it's assumed the connection is valid 

1244 as there is no "invalidate" routine. 

1245 

1246 """ 

1247 return self._is_valid 

1248 

1249 def invalidate( 

1250 self, e: Optional[BaseException] = None, soft: bool = False 

1251 ) -> None: 

1252 self._is_valid = False 

1253 

1254 @util.ro_non_memoized_property 

1255 def record_info(self) -> Optional[_InfoType]: 

1256 return self._connection_record.record_info 

1257 

1258 def cursor(self, *args: Any, **kwargs: Any) -> DBAPICursor: 

1259 return self.dbapi_connection.cursor(*args, **kwargs) 

1260 

1261 def __getattr__(self, key: Any) -> Any: 

1262 return getattr(self.dbapi_connection, key) 

1263 

1264 

1265class _ConnectionFairy(PoolProxiedConnection): 

1266 """Proxies a DBAPI connection and provides return-on-dereference 

1267 support. 

1268 

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. 

1274 

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. 

1280 

1281 .. seealso:: 

1282 

1283 :class:`.PoolProxiedConnection` 

1284 

1285 :class:`.ConnectionPoolEntry` 

1286 

1287 

1288 """ 

1289 

1290 __slots__ = ( 

1291 "dbapi_connection", 

1292 "_connection_record", 

1293 "_finalizer", 

1294 "_echo", 

1295 "_pool", 

1296 "_counter", 

1297 "__weakref__", 

1298 "__dict__", 

1299 ) 

1300 

1301 pool: Pool 

1302 dbapi_connection: DBAPIConnection 

1303 _echo: log._EchoFlagType 

1304 

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 

1317 

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 ) 

1329 

1330 _connection_record: Optional[_ConnectionRecord] 

1331 

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 

1337 

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 

1346 

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) 

1356 

1357 if threadconns is not None: 

1358 threadconns.current = weakref.ref(fairy) 

1359 

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" 

1366 

1367 fairy._counter += 1 

1368 if ( 

1369 not pool.dispatch.checkout and not pool._pre_ping 

1370 ) or fairy._counter != 1: 

1371 return fairy 

1372 

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. 

1378 

1379 attempts = 2 

1380 

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 ) 

1408 

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 ) 

1441 

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 

1448 

1449 # never called, this is for code linters 

1450 raise 

1451 

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 ) 

1461 

1462 # prevent _ConnectionFairy from being carried 

1463 # in the stack trace, see above 

1464 del fairy 

1465 

1466 # never called, this is for code linters 

1467 raise 

1468 

1469 pool.logger.info("Reconnection attempts exhausted on checkout") 

1470 fairy.invalidate() 

1471 raise exc.InvalidRequestError("This connection is closed") 

1472 

1473 def _checkout_existing(self) -> _ConnectionFairy: 

1474 return _ConnectionFairy._checkout(self._pool, fairy=self) 

1475 

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 ) 

1485 

1486 def _close(self) -> None: 

1487 self._checkin() 

1488 

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 ) 

1506 

1507 if not asyncio_safe: 

1508 return 

1509 

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) 

1531 

1532 @property 

1533 def _logger(self) -> log._IdentifiedLoggerType: 

1534 return self._pool.logger 

1535 

1536 @property 

1537 def is_valid(self) -> bool: 

1538 return self.dbapi_connection is not None 

1539 

1540 @property 

1541 def is_detached(self) -> bool: 

1542 return self._connection_record is None 

1543 

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 

1550 

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 

1557 

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] 

1574 

1575 # finalize 

1576 self._checkin() 

1577 

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) 

1581 

1582 def __getattr__(self, key: str) -> Any: 

1583 return getattr(self.dbapi_connection, key) 

1584 

1585 def detach(self) -> None: 

1586 if self._connection_record is not None: 

1587 rec = self._connection_record 

1588 rec.fairy_ref = None 

1589 

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() 

1594 

1595 rec.dbapi_connection = None 

1596 # TODO: should this be _return_conn? 

1597 self._pool._do_return_conn(self._connection_record) 

1598 

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] 

1602 

1603 self._connection_record = None 

1604 

1605 if self._pool.dispatch.detach: 

1606 self._pool.dispatch.detach(self.dbapi_connection, rec) 

1607 

1608 def close(self) -> None: 

1609 self._counter -= 1 

1610 if self._counter == 0: 

1611 self._checkin() 

1612 

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)