Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/pool/base.py: 56%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

588 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 self.logger.error( 

375 f"Exception {'terminating' if terminate else 'closing'} " 

376 f"connection %r", 

377 connection, 

378 exc_info=True, 

379 ) 

380 if not isinstance(e, Exception): 

381 raise 

382 

383 def _create_connection(self) -> ConnectionPoolEntry: 

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

385 

386 return _ConnectionRecord(self) 

387 

388 def _invalidate( 

389 self, 

390 connection: PoolProxiedConnection, 

391 exception: Optional[BaseException] = None, 

392 _checkin: bool = True, 

393 ) -> None: 

394 """Mark all connections established within the generation 

395 of the given connection as invalidated. 

396 

397 If this pool's last invalidate time is before when the given 

398 connection was created, update the timestamp til now. Otherwise, 

399 no action is performed. 

400 

401 Connections with a start time prior to this pool's invalidation 

402 time will be recycled upon next checkout. 

403 """ 

404 rec = getattr(connection, "_connection_record", None) 

405 if not rec or self._invalidate_time < rec.starttime: 

406 self._invalidate_time = time.time() 

407 if _checkin and getattr(connection, "is_valid", False): 

408 connection.invalidate(exception) 

409 

410 def recreate(self) -> Pool: 

411 """Return a new :class:`_pool.Pool`, of the same class as this one 

412 and configured with identical creation arguments. 

413 

414 This method is used in conjunction with :meth:`dispose` 

415 to close out an entire :class:`_pool.Pool` and create a new one in 

416 its place. 

417 

418 """ 

419 

420 raise NotImplementedError() 

421 

422 def dispose(self) -> None: 

423 """Dispose of this pool. 

424 

425 This method leaves the possibility of checked-out connections 

426 remaining open, as it only affects connections that are 

427 idle in the pool. 

428 

429 .. seealso:: 

430 

431 :meth:`Pool.recreate` 

432 

433 """ 

434 

435 raise NotImplementedError() 

436 

437 def connect(self) -> PoolProxiedConnection: 

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

439 

440 The connection is instrumented such that when its 

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

442 the pool. 

443 

444 """ 

445 return _ConnectionFairy._checkout(self) 

446 

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

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

449 

450 This method is called when an instrumented DBAPI connection 

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

452 

453 """ 

454 self._do_return_conn(record) 

455 

456 def _do_get(self) -> ConnectionPoolEntry: 

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

458 

459 raise NotImplementedError() 

460 

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

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

463 

464 raise NotImplementedError() 

465 

466 def status(self) -> str: 

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

468 raise NotImplementedError() 

469 

470 

471class ManagesConnection: 

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

473 :class:`.PoolProxiedConnection` and :class:`.ConnectionPoolEntry`. 

474 

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

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

477 

478 .. versionadded:: 2.0 

479 

480 """ 

481 

482 __slots__ = () 

483 

484 dbapi_connection: Optional[DBAPIConnection] 

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

486 

487 This is a :pep:`249`-compliant object that for traditional sync-style 

488 dialects is provided by the third-party 

489 DBAPI implementation in use. For asyncio dialects, the implementation 

490 is typically an adapter object provided by the SQLAlchemy dialect 

491 itself; the underlying asyncio object is available via the 

492 :attr:`.ManagesConnection.driver_connection` attribute. 

493 

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

495 :class:`.DBAPIConnection` protocol object 

496 

497 .. seealso:: 

498 

499 :attr:`.ManagesConnection.driver_connection` 

500 

501 :ref:`faq_dbapi_connection` 

502 

503 """ 

504 

505 driver_connection: Optional[Any] 

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

507 DBAPI or database driver. 

508 

509 For traditional :pep:`249` DBAPI implementations, this object will 

510 be the same object as that of 

511 :attr:`.ManagesConnection.dbapi_connection`. For an asyncio database 

512 driver, this will be the ultimate "connection" object used by that 

513 driver, such as the ``asyncpg.Connection`` object which will not have 

514 standard pep-249 methods. 

515 

516 .. versionadded:: 1.4.24 

517 

518 .. seealso:: 

519 

520 :attr:`.ManagesConnection.dbapi_connection` 

521 

522 :ref:`faq_dbapi_connection` 

523 

524 """ 

525 

526 @util.ro_memoized_property 

527 def info(self) -> _InfoType: 

528 """Info dictionary associated with the underlying DBAPI connection 

529 referred to by this :class:`.ManagesConnection` instance, allowing 

530 user-defined data to be associated with the connection. 

531 

532 The data in this dictionary is persistent for the lifespan 

533 of the DBAPI connection itself, including across pool checkins 

534 and checkouts. When the connection is invalidated 

535 and replaced with a new one, this dictionary is cleared. 

536 

537 For a :class:`.PoolProxiedConnection` instance that's not associated 

538 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the 

539 attribute returns a dictionary that is local to that 

540 :class:`.ConnectionPoolEntry`. Therefore the 

541 :attr:`.ManagesConnection.info` attribute will always provide a Python 

542 dictionary. 

543 

544 .. seealso:: 

545 

546 :attr:`.ManagesConnection.record_info` 

547 

548 

549 """ 

550 raise NotImplementedError() 

551 

552 @util.ro_memoized_property 

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

554 """Persistent info dictionary associated with this 

555 :class:`.ManagesConnection`. 

556 

557 Unlike the :attr:`.ManagesConnection.info` dictionary, the lifespan 

558 of this dictionary is that of the :class:`.ConnectionPoolEntry` 

559 which owns it; therefore this dictionary will persist across 

560 reconnects and connection invalidation for a particular entry 

561 in the connection pool. 

562 

563 For a :class:`.PoolProxiedConnection` instance that's not associated 

564 with a :class:`.ConnectionPoolEntry`, such as if it were detached, the 

565 attribute returns None. Contrast to the :attr:`.ManagesConnection.info` 

566 dictionary which is never None. 

567 

568 

569 .. seealso:: 

570 

571 :attr:`.ManagesConnection.info` 

572 

573 """ 

574 raise NotImplementedError() 

575 

576 def invalidate( 

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

578 ) -> None: 

579 """Mark the managed connection as invalidated. 

580 

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

582 

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

584 connection will be recycled on next checkout. 

585 

586 .. seealso:: 

587 

588 :ref:`pool_connection_invalidation` 

589 

590 

591 """ 

592 raise NotImplementedError() 

593 

594 

595class ConnectionPoolEntry(ManagesConnection): 

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

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

598 

599 The :class:`.ConnectionPoolEntry` object represents the long term 

600 maintenance of a particular connection for a pool, including expiring or 

601 invalidating that connection to have it replaced with a new one, which will 

602 continue to be maintained by that same :class:`.ConnectionPoolEntry` 

603 instance. Compared to :class:`.PoolProxiedConnection`, which is the 

604 short-term, per-checkout connection manager, this object lasts for the 

605 lifespan of a particular "slot" within a connection pool. 

606 

607 The :class:`.ConnectionPoolEntry` object is mostly visible to public-facing 

608 API code when it is delivered to connection pool event hooks, such as 

609 :meth:`_events.PoolEvents.connect` and :meth:`_events.PoolEvents.checkout`. 

610 

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

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

613 

614 """ 

615 

616 __slots__ = () 

617 

618 @property 

619 def in_use(self) -> bool: 

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

621 

622 raise NotImplementedError() 

623 

624 def close(self) -> None: 

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

626 raise NotImplementedError() 

627 

628 

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

630_NOOP_WEAKREF_CALLBACK = lambda ref: None # noqa: E731 

631 

632 

633class _ConnectionRecord(ConnectionPoolEntry): 

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

635 connection. 

636 

637 This is an internal object used by the :class:`_pool.Pool` implementation 

638 to provide context management to a DBAPI connection maintained by 

639 that :class:`_pool.Pool`. The public facing interface for this class 

640 is described by the :class:`.ConnectionPoolEntry` class. See that 

641 class for public API details. 

642 

643 .. seealso:: 

644 

645 :class:`.ConnectionPoolEntry` 

646 

647 :class:`.PoolProxiedConnection` 

648 

649 """ 

650 

651 __slots__ = ( 

652 "__pool", 

653 "fairy_ref", 

654 "finalize_callback", 

655 "fresh", 

656 "starttime", 

657 "dbapi_connection", 

658 "__weakref__", 

659 "__dict__", 

660 ) 

661 

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

663 fresh: bool 

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

665 starttime: float 

666 

667 def __init__(self, pool: Pool, connect: bool = True): 

668 self.fresh = False 

669 self.fairy_ref = None 

670 self.starttime = 0 

671 self.dbapi_connection = None 

672 

673 self.__pool = pool 

674 if connect: 

675 self.__connect() 

676 self.finalize_callback = deque() 

677 

678 dbapi_connection: Optional[DBAPIConnection] 

679 

680 @property 

681 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501 

682 if self.dbapi_connection is None: 

683 return None 

684 else: 

685 return self.__pool._dialect.get_driver_connection( 

686 self.dbapi_connection 

687 ) 

688 

689 @property 

690 @util.deprecated( 

691 "2.0", 

692 "The _ConnectionRecord.connection attribute is deprecated; " 

693 "please use 'driver_connection'", 

694 ) 

695 def connection(self) -> Optional[DBAPIConnection]: 

696 return self.dbapi_connection 

697 

698 _soft_invalidate_time: float = 0 

699 

700 @util.ro_memoized_property 

701 def info(self) -> _InfoType: 

702 return {} 

703 

704 @util.ro_memoized_property 

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

706 return {} 

707 

708 @classmethod 

709 def checkout(cls, pool: Pool) -> _ConnectionFairy: 

710 if TYPE_CHECKING: 

711 rec = cast(_ConnectionRecord, pool._do_get()) 

712 else: 

713 rec = pool._do_get() 

714 

715 try: 

716 dbapi_connection = rec.get_connection() 

717 except BaseException as err: 

718 with util.safe_reraise(): 

719 rec._checkin_failed(err, _fairy_was_created=False) 

720 

721 # not reached, for code linters only 

722 raise 

723 

724 echo = pool._should_log_debug() 

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

726 

727 # assign fairy_ref to the ConnectionRecord; note that under StaticPool, 

728 # this could be swapping out an existing fairy on that record. 

729 # 

730 # the weakref gets a no-op callback because Python clears weakrefs 

731 # that have callbacks before running any weakref callback, including 

732 # the fairy's finalizer, which relies on rec.fairy_ref() being None; 

733 # since Python 3.15, a weakref without a callback to an object in a 

734 # reference cycle isn't cleared until after finalizers run 

735 # (cpython gh-135552). #13738 

736 # 

737 # an alternative is for needs_gc to also treat a fairy whose 

738 # finalizer was already called as gone. that requires atexit=False 

739 # on the finalizer, otherwise its call at interpreter exit would 

740 # reset and check in connections that are still checked out. 

741 rec.fairy_ref = weakref.ref(fairy, _NOOP_WEAKREF_CALLBACK) 

742 

743 if echo: 

744 pool.logger.debug( 

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

746 ) 

747 return fairy 

748 

749 def _checkin_failed( 

750 self, err: BaseException, _fairy_was_created: bool = True 

751 ) -> None: 

752 try: 

753 self.invalidate(e=err) 

754 finally: 

755 # the record must be returned in any case; nothing else will 

756 # ever return it, as no _ConnectionFairy refers to it 

757 self.checkin( 

758 _fairy_was_created=_fairy_was_created, 

759 ) 

760 

761 def checkin(self, _fairy_was_created: bool = True) -> None: 

762 if self.fairy_ref is None and _fairy_was_created: 

763 # _fairy_was_created is False for the initial get connection phase; 

764 # meaning there was no _ConnectionFairy and we must unconditionally 

765 # do a checkin. 

766 # 

767 # otherwise, if fairy_was_created==True, if fairy_ref is None here 

768 # that means we were checked in already, so this looks like 

769 # a double checkin. 

770 util.warn("Double checkin attempted on %s" % self) 

771 return 

772 self.fairy_ref = None 

773 connection = self.dbapi_connection 

774 pool = self.__pool 

775 while self.finalize_callback: 

776 finalizer = self.finalize_callback.pop() 

777 if connection is not None: 

778 finalizer(connection) 

779 if pool.dispatch.checkin: 

780 pool.dispatch.checkin(connection, self) 

781 

782 pool._return_conn(self) 

783 

784 @property 

785 def in_use(self) -> bool: 

786 return self.fairy_ref is not None 

787 

788 @property 

789 def needs_gc(self) -> bool: 

790 ref = self.fairy_ref 

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

792 

793 @property 

794 def last_connect_time(self) -> float: 

795 return self.starttime 

796 

797 def close(self) -> None: 

798 if self.dbapi_connection is not None: 

799 self.__close() 

800 

801 def invalidate( 

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

803 ) -> None: 

804 # already invalidated 

805 if self.dbapi_connection is None: 

806 return 

807 if soft: 

808 self.__pool.dispatch.soft_invalidate( 

809 self.dbapi_connection, self, e 

810 ) 

811 self._log_invalidate(e, soft) 

812 self._soft_invalidate_time = time.time() 

813 return 

814 

815 try: 

816 self.__pool.dispatch.invalidate(self.dbapi_connection, self, e) 

817 self._log_invalidate(e, soft) 

818 self.__close(terminate=True) 

819 assert self.dbapi_connection is None 

820 finally: 

821 if self.dbapi_connection is not None: 

822 # the hard invalidation was interrupted before it could 

823 # close the connection, such as by an exception raised 

824 # from the invalidate event. a close that was itself 

825 # interrupted has already discarded the connection; here 

826 # it's kept so that its close can be retried, but the 

827 # next checkout recycles it rather than use it 

828 self._soft_invalidate_time = time.time() 

829 

830 def _log_invalidate(self, e: Optional[BaseException], soft: bool) -> None: 

831 if e is not None: 

832 self.__pool.logger.info( 

833 "%sInvalidate connection %r (reason: %s:%s)", 

834 "Soft " if soft else "", 

835 self.dbapi_connection, 

836 e.__class__.__name__, 

837 e, 

838 ) 

839 else: 

840 self.__pool.logger.info( 

841 "%sInvalidate connection %r", 

842 "Soft " if soft else "", 

843 self.dbapi_connection, 

844 ) 

845 

846 def get_connection(self) -> DBAPIConnection: 

847 recycle = False 

848 

849 # NOTE: the various comparisons here are assuming that measurable time 

850 # passes between these state changes. however, time.time() is not 

851 # guaranteed to have sub-second precision. comparisons of 

852 # "invalidation time" to "starttime" should perhaps use >= so that the 

853 # state change can take place assuming no measurable time has passed, 

854 # however this does not guarantee correct behavior here as if time 

855 # continues to not pass, it will try to reconnect repeatedly until 

856 # these timestamps diverge, so in that sense using > is safer. Per 

857 # https://stackoverflow.com/a/1938096/34549, Windows time.time() may be 

858 # within 16 milliseconds accuracy, so unit tests for connection 

859 # invalidation need a sleep of at least this long between initial start 

860 # time and invalidation for the logic below to work reliably. 

861 

862 if self.dbapi_connection is None: 

863 self.info.clear() 

864 self.__connect() 

865 elif ( 

866 self.__pool._recycle > -1 

867 and time.time() - self.starttime > self.__pool._recycle 

868 ): 

869 self.__pool.logger.info( 

870 "Connection %r exceeded timeout; recycling", 

871 self.dbapi_connection, 

872 ) 

873 recycle = True 

874 elif self.__pool._invalidate_time > self.starttime: 

875 self.__pool.logger.info( 

876 "Connection %r invalidated due to pool invalidation; " 

877 + "recycling", 

878 self.dbapi_connection, 

879 ) 

880 recycle = True 

881 elif self._soft_invalidate_time > self.starttime: 

882 self.__pool.logger.info( 

883 "Connection %r invalidated due to local soft invalidation; " 

884 + "recycling", 

885 self.dbapi_connection, 

886 ) 

887 recycle = True 

888 

889 if recycle: 

890 self.__close(terminate=True) 

891 self.info.clear() 

892 

893 self.__connect() 

894 

895 assert self.dbapi_connection is not None 

896 return self.dbapi_connection 

897 

898 def _is_hard_or_soft_invalidated(self) -> bool: 

899 return ( 

900 self.dbapi_connection is None 

901 or self.__pool._invalidate_time > self.starttime 

902 or (self._soft_invalidate_time > self.starttime) 

903 ) 

904 

905 def __close(self, *, terminate: bool = False) -> None: 

906 self.finalize_callback.clear() 

907 if self.__pool.dispatch.close: 

908 self.__pool.dispatch.close(self.dbapi_connection, self) 

909 assert self.dbapi_connection is not None 

910 try: 

911 self.__pool._close_connection( 

912 self.dbapi_connection, terminate=terminate 

913 ) 

914 finally: 

915 # a close that was interrupted, such as by a cancellation 

916 # reaching the await within an asyncio terminate(), is not 

917 # retried; the connection is not to be handed out again 

918 self.dbapi_connection = None 

919 

920 def __connect(self) -> None: 

921 pool = self.__pool 

922 

923 # ensure any existing connection is removed, so that if 

924 # creator fails, this attribute stays None 

925 self.dbapi_connection = None 

926 try: 

927 self.starttime = time.time() 

928 self.dbapi_connection = connection = pool._invoke_creator(self) 

929 pool.logger.debug("Created new connection %r", connection) 

930 self.fresh = True 

931 except BaseException as e: 

932 with util.safe_reraise(): 

933 pool.logger.debug("Error on connect(): %s", e) 

934 else: 

935 try: 

936 # in SQLAlchemy 1.4 the first_connect event is not used by 

937 # the engine, so this will usually not be set 

938 if pool.dispatch.first_connect: 

939 pool.dispatch.first_connect.for_modify( 

940 pool.dispatch 

941 ).exec_once_unless_exception(self.dbapi_connection, self) 

942 

943 # init of the dialect now takes place within the connect 

944 # event, so ensure a mutex is used on the first run 

945 pool.dispatch.connect.for_modify( 

946 pool.dispatch 

947 )._exec_w_sync_on_first_run(self.dbapi_connection, self) 

948 except BaseException: 

949 # the connection is established but the events that 

950 # configure it did not complete; nothing else has a 

951 # reference to it, so close it here or it is stranded. 

952 # for an asyncio driver in particular there is no other 

953 # opportunity, as the garbage collector cannot close a 

954 # connection that needs the event loop 

955 with util.safe_reraise(): 

956 try: 

957 pool._close_connection(connection, terminate=True) 

958 except Exception: 

959 pool.logger.exception( 

960 "Exception closing connection %r stranded by a " 

961 "failed connect event", 

962 connection, 

963 ) 

964 finally: 

965 # a cancellation reaching the await within an 

966 # asyncio terminate() propagates in place of the 

967 # original exception 

968 self.dbapi_connection = None 

969 

970 

971def _finalize_fairy( 

972 dbapi_connection: Optional[DBAPIConnection], 

973 connection_record: Optional[_ConnectionRecord], 

974 pool: Pool, 

975 echo: Optional[log._EchoFlagType], 

976 transaction_was_reset: bool = False, 

977 fairy: Optional[_ConnectionFairy] = None, 

978 is_gc_cleanup: bool = False, 

979) -> None: 

980 """Cleanup for a :class:`._ConnectionFairy` whether or not it's already 

981 been garbage collected. 

982 

983 When using an async dialect no IO can happen here (without using 

984 a dedicated thread), since this is called outside the greenlet 

985 context and with an already running loop. In this case function 

986 will only log a message and raise a warning. 

987 """ 

988 

989 if is_gc_cleanup: 

990 assert connection_record is not None 

991 

992 # check connection record to see that we're the current 

993 # fairy for this record. if not, then return; assume the 

994 # record is either checked in, or another fairy supersedes us 

995 # (can happen with StaticPool) 

996 if not connection_record.needs_gc: 

997 return 

998 assert dbapi_connection is None 

999 dbapi_connection = connection_record.dbapi_connection 

1000 

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

1002 dont_restore_gced = pool._dialect.is_async 

1003 

1004 if dont_restore_gced: 

1005 detach = connection_record is None or is_gc_cleanup 

1006 can_manipulate_connection = not is_gc_cleanup 

1007 can_close_or_terminate_connection = ( 

1008 not pool._dialect.is_async or pool._dialect.has_terminate 

1009 ) 

1010 requires_terminate_for_close = ( 

1011 pool._dialect.is_async and pool._dialect.has_terminate 

1012 ) 

1013 

1014 else: 

1015 detach = connection_record is None 

1016 can_manipulate_connection = can_close_or_terminate_connection = True 

1017 requires_terminate_for_close = False 

1018 

1019 if dbapi_connection is not None: 

1020 if connection_record and echo: 

1021 pool.logger.debug( 

1022 "Connection %r being returned to pool", dbapi_connection 

1023 ) 

1024 

1025 try: 

1026 if not fairy: 

1027 assert connection_record is not None 

1028 fairy = _ConnectionFairy( 

1029 pool, 

1030 dbapi_connection, 

1031 connection_record, 

1032 echo, 

1033 ) 

1034 assert fairy.dbapi_connection is dbapi_connection 

1035 

1036 fairy._reset( 

1037 pool, 

1038 transaction_was_reset=transaction_was_reset, 

1039 terminate_only=detach, 

1040 asyncio_safe=can_manipulate_connection, 

1041 ) 

1042 

1043 if detach: 

1044 if connection_record: 

1045 fairy._pool = pool 

1046 fairy.detach() 

1047 

1048 if can_close_or_terminate_connection: 

1049 if pool.dispatch.close_detached: 

1050 pool.dispatch.close_detached(dbapi_connection) 

1051 

1052 pool._close_connection( 

1053 dbapi_connection, 

1054 terminate=requires_terminate_for_close, 

1055 ) 

1056 

1057 except BaseException as e: 

1058 pool.logger.error( 

1059 "Exception during reset or similar", exc_info=True 

1060 ) 

1061 if not isinstance(e, Exception): 

1062 # a cancellation or similar propagates from here, so the 

1063 # checkin() below is not reached; return the record now, 

1064 # rather than leaving it to garbage collection of the fairy. 

1065 # the cleanup of the fairy below is not reached either, so 

1066 # detach it from the record first, so that it can't act on 

1067 # the record once that's checked out again. see #13699 

1068 if fairy is not None: 

1069 fairy._finalizer.detach() 

1070 fairy.dbapi_connection = None # type: ignore[assignment] 

1071 fairy._connection_record = None 

1072 if connection_record: 

1073 connection_record._checkin_failed(e) 

1074 raise 

1075 if connection_record: 

1076 connection_record.invalidate(e=e) 

1077 finally: 

1078 if detach and is_gc_cleanup and dont_restore_gced: 

1079 message = ( 

1080 "The garbage collector is trying to clean up " 

1081 f"non-checked-in connection {dbapi_connection!r}, " 

1082 f"""which will be { 

1083 'dropped, as it cannot be safely terminated' 

1084 if not can_close_or_terminate_connection 

1085 else 'terminated' 

1086 }. """ 

1087 "Please ensure that SQLAlchemy pooled connections are " 

1088 "returned to " 

1089 "the pool explicitly, either by calling ``close()`` " 

1090 "or by using appropriate context managers to manage " 

1091 "their lifecycle." 

1092 ) 

1093 pool.logger.error(message) 

1094 util.warn(message) 

1095 

1096 if connection_record and connection_record.in_use: 

1097 connection_record.checkin() 

1098 

1099 # give gc some help. See 

1100 # test/engine/test_pool.py::PoolEventsTest::test_checkin_event_gc[True] 

1101 # which actually started failing when pytest warnings plugin was 

1102 # turned on, due to util.warn() above 

1103 if fairy is not None: 

1104 # don't need the finalizer anymore since we are cleaning up here 

1105 fairy._finalizer.detach() 

1106 fairy.dbapi_connection = None # type: ignore[assignment] 

1107 fairy._connection_record = None 

1108 del dbapi_connection 

1109 del connection_record 

1110 del fairy 

1111 

1112 

1113class PoolProxiedConnection(ManagesConnection): 

1114 """A connection-like adapter for a :pep:`249` DBAPI connection, which 

1115 includes additional methods specific to the :class:`.Pool` implementation. 

1116 

1117 :class:`.PoolProxiedConnection` is the public-facing interface for the 

1118 internal :class:`._ConnectionFairy` implementation object; users familiar 

1119 with :class:`._ConnectionFairy` can consider this object to be equivalent. 

1120 

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

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

1123 

1124 """ 

1125 

1126 __slots__ = () 

1127 

1128 if typing.TYPE_CHECKING: 

1129 

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

1131 

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

1133 

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

1135 

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

1137 

1138 @property 

1139 def is_valid(self) -> bool: 

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

1141 to an active DBAPI connection.""" 

1142 

1143 raise NotImplementedError() 

1144 

1145 @property 

1146 def is_detached(self) -> bool: 

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

1148 from its pool.""" 

1149 

1150 raise NotImplementedError() 

1151 

1152 def detach(self) -> None: 

1153 """Separate this connection from its Pool. 

1154 

1155 This means that the connection will no longer be returned to the 

1156 pool when closed, and will instead be literally closed. The 

1157 associated :class:`.ConnectionPoolEntry` is de-associated from this 

1158 DBAPI connection. 

1159 

1160 Note that any overall connection limiting constraints imposed by a 

1161 Pool implementation may be violated after a detach, as the detached 

1162 connection is removed from the pool's knowledge and control. 

1163 

1164 """ 

1165 

1166 raise NotImplementedError() 

1167 

1168 def close(self) -> None: 

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

1170 

1171 The :meth:`.PoolProxiedConnection.close` method shadows the 

1172 :pep:`249` ``.close()`` method, altering its behavior to instead 

1173 :term:`release` the proxied connection back to the connection pool. 

1174 

1175 Upon release to the pool, whether the connection stays "opened" and 

1176 pooled in the Python process, versus actually closed out and removed 

1177 from the Python process, is based on the pool implementation in use and 

1178 its configuration and current state. 

1179 

1180 """ 

1181 raise NotImplementedError() 

1182 

1183 def __enter__(self) -> Self: 

1184 return self 

1185 

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

1187 self.close() 

1188 return None 

1189 

1190 

1191class _AdhocProxiedConnection(PoolProxiedConnection): 

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

1193 the DBAPI connection is not actually proxied. 

1194 

1195 This is used by the engine internals to pass a consistent 

1196 :class:`.PoolProxiedConnection` object to consuming dialects in response to 

1197 pool events that may not always have the :class:`._ConnectionFairy` 

1198 available. 

1199 

1200 """ 

1201 

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

1203 

1204 dbapi_connection: DBAPIConnection 

1205 _connection_record: ConnectionPoolEntry 

1206 

1207 def __init__( 

1208 self, 

1209 dbapi_connection: DBAPIConnection, 

1210 connection_record: ConnectionPoolEntry, 

1211 ): 

1212 self.dbapi_connection = dbapi_connection 

1213 self._connection_record = connection_record 

1214 self._is_valid = True 

1215 

1216 @property 

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

1218 return self._connection_record.driver_connection 

1219 

1220 @property 

1221 def connection(self) -> DBAPIConnection: 

1222 return self.dbapi_connection 

1223 

1224 @property 

1225 def is_valid(self) -> bool: 

1226 """Implement is_valid state attribute. 

1227 

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

1229 as there is no "invalidate" routine. 

1230 

1231 """ 

1232 return self._is_valid 

1233 

1234 def invalidate( 

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

1236 ) -> None: 

1237 self._is_valid = False 

1238 

1239 @util.ro_non_memoized_property 

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

1241 return self._connection_record.record_info 

1242 

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

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

1245 

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

1247 return getattr(self.dbapi_connection, key) 

1248 

1249 

1250class _ConnectionFairy(PoolProxiedConnection): 

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

1252 support. 

1253 

1254 This is an internal object used by the :class:`_pool.Pool` implementation 

1255 to provide context management to a DBAPI connection delivered by 

1256 that :class:`_pool.Pool`. The public facing interface for this class 

1257 is described by the :class:`.PoolProxiedConnection` class. See that 

1258 class for public API details. 

1259 

1260 The name "fairy" is inspired by the fact that the 

1261 :class:`._ConnectionFairy` object's lifespan is transitory, as it lasts 

1262 only for the length of a specific DBAPI connection being checked out from 

1263 the pool, and additionally that as a transparent proxy, it is mostly 

1264 invisible. 

1265 

1266 .. seealso:: 

1267 

1268 :class:`.PoolProxiedConnection` 

1269 

1270 :class:`.ConnectionPoolEntry` 

1271 

1272 

1273 """ 

1274 

1275 __slots__ = ( 

1276 "dbapi_connection", 

1277 "_connection_record", 

1278 "_finalizer", 

1279 "_echo", 

1280 "_pool", 

1281 "_counter", 

1282 "__weakref__", 

1283 "__dict__", 

1284 ) 

1285 

1286 pool: Pool 

1287 dbapi_connection: DBAPIConnection 

1288 _echo: log._EchoFlagType 

1289 

1290 def __init__( 

1291 self, 

1292 pool: Pool, 

1293 dbapi_connection: DBAPIConnection, 

1294 connection_record: _ConnectionRecord, 

1295 echo: log._EchoFlagType, 

1296 ): 

1297 self._pool = pool 

1298 self._counter = 0 

1299 self.dbapi_connection = dbapi_connection 

1300 self._connection_record = connection_record 

1301 self._echo = echo 

1302 

1303 # use weakref.finalize as a destructor 

1304 self._finalizer = weakref.finalize( 

1305 self, 

1306 _finalize_fairy, 

1307 None, 

1308 connection_record, 

1309 pool, 

1310 echo, 

1311 transaction_was_reset=False, 

1312 is_gc_cleanup=True, 

1313 ) 

1314 

1315 _connection_record: Optional[_ConnectionRecord] 

1316 

1317 @property 

1318 def driver_connection(self) -> Optional[Any]: # type: ignore[override] # mypy#4125 # noqa: E501 

1319 if self._connection_record is None: 

1320 return None 

1321 return self._connection_record.driver_connection 

1322 

1323 @property 

1324 @util.deprecated( 

1325 "2.0", 

1326 "The _ConnectionFairy.connection attribute is deprecated; " 

1327 "please use 'driver_connection'", 

1328 ) 

1329 def connection(self) -> DBAPIConnection: 

1330 return self.dbapi_connection 

1331 

1332 @classmethod 

1333 def _checkout( 

1334 cls, 

1335 pool: Pool, 

1336 threadconns: Optional[threading.local] = None, 

1337 fairy: Optional[_ConnectionFairy] = None, 

1338 ) -> _ConnectionFairy: 

1339 if not fairy: 

1340 fairy = _ConnectionRecord.checkout(pool) 

1341 

1342 if threadconns is not None: 

1343 threadconns.current = weakref.ref(fairy) 

1344 

1345 assert ( 

1346 fairy._connection_record is not None 

1347 ), "can't 'checkout' a detached connection fairy" 

1348 assert ( 

1349 fairy.dbapi_connection is not None 

1350 ), "can't 'checkout' an invalidated connection fairy" 

1351 

1352 fairy._counter += 1 

1353 if ( 

1354 not pool.dispatch.checkout and not pool._pre_ping 

1355 ) or fairy._counter != 1: 

1356 return fairy 

1357 

1358 # Pool listeners can trigger a reconnection on checkout, as well 

1359 # as the pre-pinger. 

1360 # there are three attempts made here, but note that if the database 

1361 # is not accessible from a connection standpoint, those won't proceed 

1362 # here. 

1363 

1364 attempts = 2 

1365 

1366 while attempts > 0: 

1367 connection_is_fresh = fairy._connection_record.fresh 

1368 fairy._connection_record.fresh = False 

1369 try: 

1370 if pool._pre_ping: 

1371 if not connection_is_fresh: 

1372 if fairy._echo: 

1373 pool.logger.debug( 

1374 "Pool pre-ping on connection %s", 

1375 fairy.dbapi_connection, 

1376 ) 

1377 result = pool._dialect._do_ping_w_event( 

1378 fairy.dbapi_connection 

1379 ) 

1380 if not result: 

1381 if fairy._echo: 

1382 pool.logger.debug( 

1383 "Pool pre-ping on connection %s failed, " 

1384 "will invalidate pool", 

1385 fairy.dbapi_connection, 

1386 ) 

1387 raise exc.InvalidatePoolError() 

1388 elif fairy._echo: 

1389 pool.logger.debug( 

1390 "Connection %s is fresh, skipping pre-ping", 

1391 fairy.dbapi_connection, 

1392 ) 

1393 

1394 pool.dispatch.checkout( 

1395 fairy.dbapi_connection, fairy._connection_record, fairy 

1396 ) 

1397 return fairy 

1398 except exc.DisconnectionError as e: 

1399 if e.invalidate_pool: 

1400 pool.logger.info( 

1401 "Disconnection detected on checkout, " 

1402 "invalidating all pooled connections prior to " 

1403 "current timestamp (reason: %r)", 

1404 e, 

1405 ) 

1406 fairy._connection_record.invalidate(e) 

1407 pool._invalidate(fairy, e, _checkin=False) 

1408 else: 

1409 pool.logger.info( 

1410 "Disconnection detected on checkout, " 

1411 "invalidating individual connection %s (reason: %r)", 

1412 fairy.dbapi_connection, 

1413 e, 

1414 ) 

1415 fairy._connection_record.invalidate(e) 

1416 try: 

1417 fairy.dbapi_connection = ( 

1418 fairy._connection_record.get_connection() 

1419 ) 

1420 except BaseException as err: 

1421 with util.safe_reraise(): 

1422 fairy._connection_record._checkin_failed( 

1423 err, 

1424 _fairy_was_created=True, 

1425 ) 

1426 

1427 # prevent _ConnectionFairy from being carried 

1428 # in the stack trace. Do this after the 

1429 # connection record has been checked in, so that 

1430 # if the del triggers a finalize fairy, it won't 

1431 # try to checkin a second time. 

1432 del fairy 

1433 

1434 # never called, this is for code linters 

1435 raise 

1436 

1437 attempts -= 1 

1438 except BaseException as be_outer: 

1439 with util.safe_reraise(): 

1440 rec = fairy._connection_record 

1441 if rec is not None: 

1442 rec._checkin_failed( 

1443 be_outer, 

1444 _fairy_was_created=True, 

1445 ) 

1446 

1447 # prevent _ConnectionFairy from being carried 

1448 # in the stack trace, see above 

1449 del fairy 

1450 

1451 # never called, this is for code linters 

1452 raise 

1453 

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

1455 fairy.invalidate() 

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

1457 

1458 def _checkout_existing(self) -> _ConnectionFairy: 

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

1460 

1461 def _checkin(self, transaction_was_reset: bool = False) -> None: 

1462 _finalize_fairy( 

1463 self.dbapi_connection, 

1464 self._connection_record, 

1465 self._pool, 

1466 self._echo, 

1467 transaction_was_reset=transaction_was_reset, 

1468 fairy=self, 

1469 ) 

1470 

1471 def _close(self) -> None: 

1472 self._checkin() 

1473 

1474 def _reset( 

1475 self, 

1476 pool: Pool, 

1477 transaction_was_reset: bool, 

1478 terminate_only: bool, 

1479 asyncio_safe: bool, 

1480 ) -> None: 

1481 if pool.dispatch.reset: 

1482 pool.dispatch.reset( 

1483 self.dbapi_connection, 

1484 self._connection_record, 

1485 PoolResetState( 

1486 transaction_was_reset=transaction_was_reset, 

1487 terminate_only=terminate_only, 

1488 asyncio_safe=asyncio_safe, 

1489 ), 

1490 ) 

1491 

1492 if not asyncio_safe: 

1493 return 

1494 

1495 if pool._reset_on_return is reset_rollback: 

1496 if transaction_was_reset: 

1497 if self._echo: 

1498 pool.logger.debug( 

1499 "Connection %s reset, transaction already reset", 

1500 self.dbapi_connection, 

1501 ) 

1502 else: 

1503 if self._echo: 

1504 pool.logger.debug( 

1505 "Connection %s rollback-on-return", 

1506 self.dbapi_connection, 

1507 ) 

1508 pool._dialect.do_rollback(self) 

1509 elif pool._reset_on_return is reset_commit: 

1510 if self._echo: 

1511 pool.logger.debug( 

1512 "Connection %s commit-on-return", 

1513 self.dbapi_connection, 

1514 ) 

1515 pool._dialect.do_commit(self) 

1516 

1517 @property 

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

1519 return self._pool.logger 

1520 

1521 @property 

1522 def is_valid(self) -> bool: 

1523 return self.dbapi_connection is not None 

1524 

1525 @property 

1526 def is_detached(self) -> bool: 

1527 return self._connection_record is None 

1528 

1529 @util.ro_memoized_property 

1530 def info(self) -> _InfoType: 

1531 if self._connection_record is None: 

1532 return {} 

1533 else: 

1534 return self._connection_record.info 

1535 

1536 @util.ro_non_memoized_property 

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

1538 if self._connection_record is None: 

1539 return None 

1540 else: 

1541 return self._connection_record.record_info 

1542 

1543 def invalidate( 

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

1545 ) -> None: 

1546 if self.dbapi_connection is None: 

1547 util.warn("Can't invalidate an already-closed connection.") 

1548 return 

1549 try: 

1550 if self._connection_record: 

1551 self._connection_record.invalidate(e=e, soft=soft) 

1552 finally: 

1553 if not soft: 

1554 # prevent any rollback / reset actions etc. on 

1555 # the connection, including when the invalidation above 

1556 # was interrupted. _checkin() then does no IO; it only 

1557 # returns the record to the pool 

1558 self.dbapi_connection = None # type: ignore[assignment] 

1559 

1560 # finalize 

1561 self._checkin() 

1562 

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

1564 assert self.dbapi_connection is not None 

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

1566 

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

1568 return getattr(self.dbapi_connection, key) 

1569 

1570 def detach(self) -> None: 

1571 if self._connection_record is not None: 

1572 rec = self._connection_record 

1573 rec.fairy_ref = None 

1574 

1575 # cancel the finalizer, as current behavior is that there's no 

1576 # GC "cleanup" for a detached connection. not sure if this 

1577 # is the most appropriate decision; see issue #13570 

1578 self._finalizer.detach() 

1579 

1580 rec.dbapi_connection = None 

1581 # TODO: should this be _return_conn? 

1582 self._pool._do_return_conn(self._connection_record) 

1583 

1584 # can't get the descriptor assignment to work here 

1585 # in pylance. mypy is OK w/ it 

1586 self.info = self.info.copy() # type: ignore[misc] 

1587 

1588 self._connection_record = None 

1589 

1590 if self._pool.dispatch.detach: 

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

1592 

1593 def close(self) -> None: 

1594 self._counter -= 1 

1595 if self._counter == 0: 

1596 self._checkin() 

1597 

1598 def _close_special(self, transaction_reset: bool = False) -> None: 

1599 self._counter -= 1 

1600 if self._counter == 0: 

1601 self._checkin(transaction_was_reset=transaction_reset)