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

587 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 

629class _ConnectionRecord(ConnectionPoolEntry): 

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

631 connection. 

632 

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

634 to provide context management to a DBAPI connection maintained by 

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

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

637 class for public API details. 

638 

639 .. seealso:: 

640 

641 :class:`.ConnectionPoolEntry` 

642 

643 :class:`.PoolProxiedConnection` 

644 

645 """ 

646 

647 __slots__ = ( 

648 "__pool", 

649 "fairy_ref", 

650 "finalize_callback", 

651 "fresh", 

652 "starttime", 

653 "dbapi_connection", 

654 "__weakref__", 

655 "__dict__", 

656 ) 

657 

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

659 fresh: bool 

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

661 starttime: float 

662 

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

664 self.fresh = False 

665 self.fairy_ref = None 

666 self.starttime = 0 

667 self.dbapi_connection = None 

668 

669 self.__pool = pool 

670 if connect: 

671 self.__connect() 

672 self.finalize_callback = deque() 

673 

674 dbapi_connection: Optional[DBAPIConnection] 

675 

676 @property 

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

678 if self.dbapi_connection is None: 

679 return None 

680 else: 

681 return self.__pool._dialect.get_driver_connection( 

682 self.dbapi_connection 

683 ) 

684 

685 @property 

686 @util.deprecated( 

687 "2.0", 

688 "The _ConnectionRecord.connection attribute is deprecated; " 

689 "please use 'driver_connection'", 

690 ) 

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

692 return self.dbapi_connection 

693 

694 _soft_invalidate_time: float = 0 

695 

696 @util.ro_memoized_property 

697 def info(self) -> _InfoType: 

698 return {} 

699 

700 @util.ro_memoized_property 

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

702 return {} 

703 

704 @classmethod 

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

706 if TYPE_CHECKING: 

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

708 else: 

709 rec = pool._do_get() 

710 

711 try: 

712 dbapi_connection = rec.get_connection() 

713 except BaseException as err: 

714 with util.safe_reraise(): 

715 rec._checkin_failed(err, _fairy_was_created=False) 

716 

717 # not reached, for code linters only 

718 raise 

719 

720 echo = pool._should_log_debug() 

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

722 

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

724 # this could be swapping out an existing fairy on that ConnectionRecord 

725 rec.fairy_ref = weakref.ref(fairy) 

726 

727 if echo: 

728 pool.logger.debug( 

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

730 ) 

731 return fairy 

732 

733 def _checkin_failed( 

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

735 ) -> None: 

736 try: 

737 self.invalidate(e=err) 

738 finally: 

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

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

741 self.checkin( 

742 _fairy_was_created=_fairy_was_created, 

743 ) 

744 

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

746 if self.fairy_ref is None and _fairy_was_created: 

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

748 # meaning there was no _ConnectionFairy and we must unconditionally 

749 # do a checkin. 

750 # 

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

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

753 # a double checkin. 

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

755 return 

756 self.fairy_ref = None 

757 connection = self.dbapi_connection 

758 pool = self.__pool 

759 while self.finalize_callback: 

760 finalizer = self.finalize_callback.pop() 

761 if connection is not None: 

762 finalizer(connection) 

763 if pool.dispatch.checkin: 

764 pool.dispatch.checkin(connection, self) 

765 

766 pool._return_conn(self) 

767 

768 @property 

769 def in_use(self) -> bool: 

770 return self.fairy_ref is not None 

771 

772 @property 

773 def needs_gc(self) -> bool: 

774 ref = self.fairy_ref 

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

776 

777 @property 

778 def last_connect_time(self) -> float: 

779 return self.starttime 

780 

781 def close(self) -> None: 

782 if self.dbapi_connection is not None: 

783 self.__close() 

784 

785 def invalidate( 

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

787 ) -> None: 

788 # already invalidated 

789 if self.dbapi_connection is None: 

790 return 

791 if soft: 

792 self.__pool.dispatch.soft_invalidate( 

793 self.dbapi_connection, self, e 

794 ) 

795 self._log_invalidate(e, soft) 

796 self._soft_invalidate_time = time.time() 

797 return 

798 

799 try: 

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

801 self._log_invalidate(e, soft) 

802 self.__close(terminate=True) 

803 assert self.dbapi_connection is None 

804 finally: 

805 if self.dbapi_connection is not None: 

806 # the hard invalidation was interrupted before it could 

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

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

809 # interrupted has already discarded the connection; here 

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

811 # next checkout recycles it rather than use it 

812 self._soft_invalidate_time = time.time() 

813 

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

815 if e is not None: 

816 self.__pool.logger.info( 

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

818 "Soft " if soft else "", 

819 self.dbapi_connection, 

820 e.__class__.__name__, 

821 e, 

822 ) 

823 else: 

824 self.__pool.logger.info( 

825 "%sInvalidate connection %r", 

826 "Soft " if soft else "", 

827 self.dbapi_connection, 

828 ) 

829 

830 def get_connection(self) -> DBAPIConnection: 

831 recycle = False 

832 

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

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

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

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

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

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

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

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

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

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

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

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

845 

846 if self.dbapi_connection is None: 

847 self.info.clear() 

848 self.__connect() 

849 elif ( 

850 self.__pool._recycle > -1 

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

852 ): 

853 self.__pool.logger.info( 

854 "Connection %r exceeded timeout; recycling", 

855 self.dbapi_connection, 

856 ) 

857 recycle = True 

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

859 self.__pool.logger.info( 

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

861 + "recycling", 

862 self.dbapi_connection, 

863 ) 

864 recycle = True 

865 elif self._soft_invalidate_time > self.starttime: 

866 self.__pool.logger.info( 

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

868 + "recycling", 

869 self.dbapi_connection, 

870 ) 

871 recycle = True 

872 

873 if recycle: 

874 self.__close(terminate=True) 

875 self.info.clear() 

876 

877 self.__connect() 

878 

879 assert self.dbapi_connection is not None 

880 return self.dbapi_connection 

881 

882 def _is_hard_or_soft_invalidated(self) -> bool: 

883 return ( 

884 self.dbapi_connection is None 

885 or self.__pool._invalidate_time > self.starttime 

886 or (self._soft_invalidate_time > self.starttime) 

887 ) 

888 

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

890 self.finalize_callback.clear() 

891 if self.__pool.dispatch.close: 

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

893 assert self.dbapi_connection is not None 

894 try: 

895 self.__pool._close_connection( 

896 self.dbapi_connection, terminate=terminate 

897 ) 

898 finally: 

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

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

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

902 self.dbapi_connection = None 

903 

904 def __connect(self) -> None: 

905 pool = self.__pool 

906 

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

908 # creator fails, this attribute stays None 

909 self.dbapi_connection = None 

910 try: 

911 self.starttime = time.time() 

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

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

914 self.fresh = True 

915 except BaseException as e: 

916 with util.safe_reraise(): 

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

918 else: 

919 try: 

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

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

922 if pool.dispatch.first_connect: 

923 pool.dispatch.first_connect.for_modify( 

924 pool.dispatch 

925 ).exec_once_unless_exception(self.dbapi_connection, self) 

926 

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

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

929 pool.dispatch.connect.for_modify( 

930 pool.dispatch 

931 )._exec_w_sync_on_first_run(self.dbapi_connection, self) 

932 except BaseException: 

933 # the connection is established but the events that 

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

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

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

937 # opportunity, as the garbage collector cannot close a 

938 # connection that needs the event loop 

939 with util.safe_reraise(): 

940 try: 

941 pool._close_connection(connection, terminate=True) 

942 except Exception: 

943 pool.logger.exception( 

944 "Exception closing connection %r stranded by a " 

945 "failed connect event", 

946 connection, 

947 ) 

948 finally: 

949 # a cancellation reaching the await within an 

950 # asyncio terminate() propagates in place of the 

951 # original exception 

952 self.dbapi_connection = None 

953 

954 

955def _finalize_fairy( 

956 dbapi_connection: Optional[DBAPIConnection], 

957 connection_record: Optional[_ConnectionRecord], 

958 pool: Pool, 

959 echo: Optional[log._EchoFlagType], 

960 transaction_was_reset: bool = False, 

961 fairy: Optional[_ConnectionFairy] = None, 

962 is_gc_cleanup: bool = False, 

963) -> None: 

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

965 been garbage collected. 

966 

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

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

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

970 will only log a message and raise a warning. 

971 """ 

972 

973 if is_gc_cleanup: 

974 assert connection_record is not None 

975 

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

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

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

979 # (can happen with StaticPool) 

980 if not connection_record.needs_gc: 

981 return 

982 assert dbapi_connection is None 

983 dbapi_connection = connection_record.dbapi_connection 

984 

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

986 dont_restore_gced = pool._dialect.is_async 

987 

988 if dont_restore_gced: 

989 detach = connection_record is None or is_gc_cleanup 

990 can_manipulate_connection = not is_gc_cleanup 

991 can_close_or_terminate_connection = ( 

992 not pool._dialect.is_async or pool._dialect.has_terminate 

993 ) 

994 requires_terminate_for_close = ( 

995 pool._dialect.is_async and pool._dialect.has_terminate 

996 ) 

997 

998 else: 

999 detach = connection_record is None 

1000 can_manipulate_connection = can_close_or_terminate_connection = True 

1001 requires_terminate_for_close = False 

1002 

1003 if dbapi_connection is not None: 

1004 if connection_record and echo: 

1005 pool.logger.debug( 

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

1007 ) 

1008 

1009 try: 

1010 if not fairy: 

1011 assert connection_record is not None 

1012 fairy = _ConnectionFairy( 

1013 pool, 

1014 dbapi_connection, 

1015 connection_record, 

1016 echo, 

1017 ) 

1018 assert fairy.dbapi_connection is dbapi_connection 

1019 

1020 fairy._reset( 

1021 pool, 

1022 transaction_was_reset=transaction_was_reset, 

1023 terminate_only=detach, 

1024 asyncio_safe=can_manipulate_connection, 

1025 ) 

1026 

1027 if detach: 

1028 if connection_record: 

1029 fairy._pool = pool 

1030 fairy.detach() 

1031 

1032 if can_close_or_terminate_connection: 

1033 if pool.dispatch.close_detached: 

1034 pool.dispatch.close_detached(dbapi_connection) 

1035 

1036 pool._close_connection( 

1037 dbapi_connection, 

1038 terminate=requires_terminate_for_close, 

1039 ) 

1040 

1041 except BaseException as e: 

1042 pool.logger.error( 

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

1044 ) 

1045 if not isinstance(e, Exception): 

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

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

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

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

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

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

1052 if fairy is not None: 

1053 fairy._finalizer.detach() 

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

1055 fairy._connection_record = None 

1056 if connection_record: 

1057 connection_record._checkin_failed(e) 

1058 raise 

1059 if connection_record: 

1060 connection_record.invalidate(e=e) 

1061 finally: 

1062 if detach and is_gc_cleanup and dont_restore_gced: 

1063 message = ( 

1064 "The garbage collector is trying to clean up " 

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

1066 f"""which will be { 

1067 'dropped, as it cannot be safely terminated' 

1068 if not can_close_or_terminate_connection 

1069 else 'terminated' 

1070 }. """ 

1071 "Please ensure that SQLAlchemy pooled connections are " 

1072 "returned to " 

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

1074 "or by using appropriate context managers to manage " 

1075 "their lifecycle." 

1076 ) 

1077 pool.logger.error(message) 

1078 util.warn(message) 

1079 

1080 if connection_record and connection_record.in_use: 

1081 connection_record.checkin() 

1082 

1083 # give gc some help. See 

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

1085 # which actually started failing when pytest warnings plugin was 

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

1087 if fairy is not None: 

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

1089 fairy._finalizer.detach() 

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

1091 fairy._connection_record = None 

1092 del dbapi_connection 

1093 del connection_record 

1094 del fairy 

1095 

1096 

1097class PoolProxiedConnection(ManagesConnection): 

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

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

1100 

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

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

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

1104 

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

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

1107 

1108 """ 

1109 

1110 __slots__ = () 

1111 

1112 if typing.TYPE_CHECKING: 

1113 

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

1115 

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

1117 

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

1119 

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

1121 

1122 @property 

1123 def is_valid(self) -> bool: 

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

1125 to an active DBAPI connection.""" 

1126 

1127 raise NotImplementedError() 

1128 

1129 @property 

1130 def is_detached(self) -> bool: 

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

1132 from its pool.""" 

1133 

1134 raise NotImplementedError() 

1135 

1136 def detach(self) -> None: 

1137 """Separate this connection from its Pool. 

1138 

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

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

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

1142 DBAPI connection. 

1143 

1144 Note that any overall connection limiting constraints imposed by a 

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

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

1147 

1148 """ 

1149 

1150 raise NotImplementedError() 

1151 

1152 def close(self) -> None: 

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

1154 

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

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

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

1158 

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

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

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

1162 its configuration and current state. 

1163 

1164 """ 

1165 raise NotImplementedError() 

1166 

1167 def __enter__(self) -> Self: 

1168 return self 

1169 

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

1171 self.close() 

1172 return None 

1173 

1174 

1175class _AdhocProxiedConnection(PoolProxiedConnection): 

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

1177 the DBAPI connection is not actually proxied. 

1178 

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

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

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

1182 available. 

1183 

1184 """ 

1185 

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

1187 

1188 dbapi_connection: DBAPIConnection 

1189 _connection_record: ConnectionPoolEntry 

1190 

1191 def __init__( 

1192 self, 

1193 dbapi_connection: DBAPIConnection, 

1194 connection_record: ConnectionPoolEntry, 

1195 ): 

1196 self.dbapi_connection = dbapi_connection 

1197 self._connection_record = connection_record 

1198 self._is_valid = True 

1199 

1200 @property 

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

1202 return self._connection_record.driver_connection 

1203 

1204 @property 

1205 def connection(self) -> DBAPIConnection: 

1206 return self.dbapi_connection 

1207 

1208 @property 

1209 def is_valid(self) -> bool: 

1210 """Implement is_valid state attribute. 

1211 

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

1213 as there is no "invalidate" routine. 

1214 

1215 """ 

1216 return self._is_valid 

1217 

1218 def invalidate( 

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

1220 ) -> None: 

1221 self._is_valid = False 

1222 

1223 @util.ro_non_memoized_property 

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

1225 return self._connection_record.record_info 

1226 

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

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

1229 

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

1231 return getattr(self.dbapi_connection, key) 

1232 

1233 

1234class _ConnectionFairy(PoolProxiedConnection): 

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

1236 support. 

1237 

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

1239 to provide context management to a DBAPI connection delivered by 

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

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

1242 class for public API details. 

1243 

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

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

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

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

1248 invisible. 

1249 

1250 .. seealso:: 

1251 

1252 :class:`.PoolProxiedConnection` 

1253 

1254 :class:`.ConnectionPoolEntry` 

1255 

1256 

1257 """ 

1258 

1259 __slots__ = ( 

1260 "dbapi_connection", 

1261 "_connection_record", 

1262 "_finalizer", 

1263 "_echo", 

1264 "_pool", 

1265 "_counter", 

1266 "__weakref__", 

1267 "__dict__", 

1268 ) 

1269 

1270 pool: Pool 

1271 dbapi_connection: DBAPIConnection 

1272 _echo: log._EchoFlagType 

1273 

1274 def __init__( 

1275 self, 

1276 pool: Pool, 

1277 dbapi_connection: DBAPIConnection, 

1278 connection_record: _ConnectionRecord, 

1279 echo: log._EchoFlagType, 

1280 ): 

1281 self._pool = pool 

1282 self._counter = 0 

1283 self.dbapi_connection = dbapi_connection 

1284 self._connection_record = connection_record 

1285 self._echo = echo 

1286 

1287 # use weakref.finalize as a destructor 

1288 self._finalizer = weakref.finalize( 

1289 self, 

1290 _finalize_fairy, 

1291 None, 

1292 connection_record, 

1293 pool, 

1294 echo, 

1295 transaction_was_reset=False, 

1296 is_gc_cleanup=True, 

1297 ) 

1298 

1299 _connection_record: Optional[_ConnectionRecord] 

1300 

1301 @property 

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

1303 if self._connection_record is None: 

1304 return None 

1305 return self._connection_record.driver_connection 

1306 

1307 @property 

1308 @util.deprecated( 

1309 "2.0", 

1310 "The _ConnectionFairy.connection attribute is deprecated; " 

1311 "please use 'driver_connection'", 

1312 ) 

1313 def connection(self) -> DBAPIConnection: 

1314 return self.dbapi_connection 

1315 

1316 @classmethod 

1317 def _checkout( 

1318 cls, 

1319 pool: Pool, 

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

1321 fairy: Optional[_ConnectionFairy] = None, 

1322 ) -> _ConnectionFairy: 

1323 if not fairy: 

1324 fairy = _ConnectionRecord.checkout(pool) 

1325 

1326 if threadconns is not None: 

1327 threadconns.current = weakref.ref(fairy) 

1328 

1329 assert ( 

1330 fairy._connection_record is not None 

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

1332 assert ( 

1333 fairy.dbapi_connection is not None 

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

1335 

1336 fairy._counter += 1 

1337 if ( 

1338 not pool.dispatch.checkout and not pool._pre_ping 

1339 ) or fairy._counter != 1: 

1340 return fairy 

1341 

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

1343 # as the pre-pinger. 

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

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

1346 # here. 

1347 

1348 attempts = 2 

1349 

1350 while attempts > 0: 

1351 connection_is_fresh = fairy._connection_record.fresh 

1352 fairy._connection_record.fresh = False 

1353 try: 

1354 if pool._pre_ping: 

1355 if not connection_is_fresh: 

1356 if fairy._echo: 

1357 pool.logger.debug( 

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

1359 fairy.dbapi_connection, 

1360 ) 

1361 result = pool._dialect._do_ping_w_event( 

1362 fairy.dbapi_connection 

1363 ) 

1364 if not result: 

1365 if fairy._echo: 

1366 pool.logger.debug( 

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

1368 "will invalidate pool", 

1369 fairy.dbapi_connection, 

1370 ) 

1371 raise exc.InvalidatePoolError() 

1372 elif fairy._echo: 

1373 pool.logger.debug( 

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

1375 fairy.dbapi_connection, 

1376 ) 

1377 

1378 pool.dispatch.checkout( 

1379 fairy.dbapi_connection, fairy._connection_record, fairy 

1380 ) 

1381 return fairy 

1382 except exc.DisconnectionError as e: 

1383 if e.invalidate_pool: 

1384 pool.logger.info( 

1385 "Disconnection detected on checkout, " 

1386 "invalidating all pooled connections prior to " 

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

1388 e, 

1389 ) 

1390 fairy._connection_record.invalidate(e) 

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

1392 else: 

1393 pool.logger.info( 

1394 "Disconnection detected on checkout, " 

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

1396 fairy.dbapi_connection, 

1397 e, 

1398 ) 

1399 fairy._connection_record.invalidate(e) 

1400 try: 

1401 fairy.dbapi_connection = ( 

1402 fairy._connection_record.get_connection() 

1403 ) 

1404 except BaseException as err: 

1405 with util.safe_reraise(): 

1406 fairy._connection_record._checkin_failed( 

1407 err, 

1408 _fairy_was_created=True, 

1409 ) 

1410 

1411 # prevent _ConnectionFairy from being carried 

1412 # in the stack trace. Do this after the 

1413 # connection record has been checked in, so that 

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

1415 # try to checkin a second time. 

1416 del fairy 

1417 

1418 # never called, this is for code linters 

1419 raise 

1420 

1421 attempts -= 1 

1422 except BaseException as be_outer: 

1423 with util.safe_reraise(): 

1424 rec = fairy._connection_record 

1425 if rec is not None: 

1426 rec._checkin_failed( 

1427 be_outer, 

1428 _fairy_was_created=True, 

1429 ) 

1430 

1431 # prevent _ConnectionFairy from being carried 

1432 # in the stack trace, see above 

1433 del fairy 

1434 

1435 # never called, this is for code linters 

1436 raise 

1437 

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

1439 fairy.invalidate() 

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

1441 

1442 def _checkout_existing(self) -> _ConnectionFairy: 

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

1444 

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

1446 _finalize_fairy( 

1447 self.dbapi_connection, 

1448 self._connection_record, 

1449 self._pool, 

1450 self._echo, 

1451 transaction_was_reset=transaction_was_reset, 

1452 fairy=self, 

1453 ) 

1454 

1455 def _close(self) -> None: 

1456 self._checkin() 

1457 

1458 def _reset( 

1459 self, 

1460 pool: Pool, 

1461 transaction_was_reset: bool, 

1462 terminate_only: bool, 

1463 asyncio_safe: bool, 

1464 ) -> None: 

1465 if pool.dispatch.reset: 

1466 pool.dispatch.reset( 

1467 self.dbapi_connection, 

1468 self._connection_record, 

1469 PoolResetState( 

1470 transaction_was_reset=transaction_was_reset, 

1471 terminate_only=terminate_only, 

1472 asyncio_safe=asyncio_safe, 

1473 ), 

1474 ) 

1475 

1476 if not asyncio_safe: 

1477 return 

1478 

1479 if pool._reset_on_return is reset_rollback: 

1480 if transaction_was_reset: 

1481 if self._echo: 

1482 pool.logger.debug( 

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

1484 self.dbapi_connection, 

1485 ) 

1486 else: 

1487 if self._echo: 

1488 pool.logger.debug( 

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

1490 self.dbapi_connection, 

1491 ) 

1492 pool._dialect.do_rollback(self) 

1493 elif pool._reset_on_return is reset_commit: 

1494 if self._echo: 

1495 pool.logger.debug( 

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

1497 self.dbapi_connection, 

1498 ) 

1499 pool._dialect.do_commit(self) 

1500 

1501 @property 

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

1503 return self._pool.logger 

1504 

1505 @property 

1506 def is_valid(self) -> bool: 

1507 return self.dbapi_connection is not None 

1508 

1509 @property 

1510 def is_detached(self) -> bool: 

1511 return self._connection_record is None 

1512 

1513 @util.ro_memoized_property 

1514 def info(self) -> _InfoType: 

1515 if self._connection_record is None: 

1516 return {} 

1517 else: 

1518 return self._connection_record.info 

1519 

1520 @util.ro_non_memoized_property 

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

1522 if self._connection_record is None: 

1523 return None 

1524 else: 

1525 return self._connection_record.record_info 

1526 

1527 def invalidate( 

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

1529 ) -> None: 

1530 if self.dbapi_connection is None: 

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

1532 return 

1533 try: 

1534 if self._connection_record: 

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

1536 finally: 

1537 if not soft: 

1538 # prevent any rollback / reset actions etc. on 

1539 # the connection, including when the invalidation above 

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

1541 # returns the record to the pool 

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

1543 

1544 # finalize 

1545 self._checkin() 

1546 

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

1548 assert self.dbapi_connection is not None 

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

1550 

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

1552 return getattr(self.dbapi_connection, key) 

1553 

1554 def detach(self) -> None: 

1555 if self._connection_record is not None: 

1556 rec = self._connection_record 

1557 rec.fairy_ref = None 

1558 

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

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

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

1562 self._finalizer.detach() 

1563 

1564 rec.dbapi_connection = None 

1565 # TODO: should this be _return_conn? 

1566 self._pool._do_return_conn(self._connection_record) 

1567 

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

1569 # in pylance. mypy is OK w/ it 

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

1571 

1572 self._connection_record = None 

1573 

1574 if self._pool.dispatch.detach: 

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

1576 

1577 def close(self) -> None: 

1578 self._counter -= 1 

1579 if self._counter == 0: 

1580 self._checkin() 

1581 

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

1583 self._counter -= 1 

1584 if self._counter == 0: 

1585 self._checkin(transaction_was_reset=transaction_reset)