Skip to content

PYTHON-5272 Implement TLS session resumption for connections#2864

Draft
blink1073 wants to merge 16 commits into
mongodb:masterfrom
blink1073:PYTHON-5272
Draft

PYTHON-5272 Implement TLS session resumption for connections#2864
blink1073 wants to merge 16 commits into
mongodb:masterfrom
blink1073:PYTHON-5272

Conversation

@blink1073

@blink1073 blink1073 commented Jun 10, 2026

Copy link
Copy Markdown
Member

PYTHON-5272

Changes in this PR

Added TLS session resumption to the connection pool, avoiding a full handshake on each new connection to the same server. Session reuse is active on the sync path unconditionally, and on the async path on Python 3.11 or later.

Test Plan

Added unit and integration tests covering:

  • _SSLSessionCache get/set behavior
  • Pool creates a session cache when TLS is configured
  • A session is stored after a successful connection
  • The cached session is passed to wrap_socket on subsequent connections (sync, no server required)
  • The cached session is set on the SSLObject before the handshake on Python 3.11+ (async, no server required)

Checklist

Checklist for Author

  • Did you update the changelog (if necessary)?
  • Is there test coverage? — Yes
  • Is any followup work tracked in a JIRA ticket? If so, add link(s). — Async session reuse on Python < 3.11 deferred; no separate ticket yet

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

Add _SSLSessionCache to cache TLS sessions per pool, enabling session
resumption on subsequent connections to the same server. This avoids
full asymmetric-key handshakes on every new connection, addressing the
OpenSSL 3.0 performance overhead seen in BF-36991.
@blink1073 blink1073 requested a review from a team as a code owner June 10, 2026 11:54
@blink1073 blink1073 requested a review from sleepyStick June 10, 2026 11:54
@blink1073 blink1073 marked this pull request as draft June 10, 2026 11:56
Verify that a pre-populated _SSLSessionCache passes the cached session
as session= to wrap_socket on the next connection, using mocks so no
live server is required.
@codecov-commenter

codecov-commenter commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.17949% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pymongo/pool_shared.py 81.48% 3 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

On Python 3.11+, SSLProtocol.__init__ creates the ssl.SSLObject via
wrap_bio before the handshake starts in connection_made. We temporarily
replace asyncio.sslproto.SSLProtocol with a subclass that sets
sslobj.session to the cached session immediately after super().__init__,
then restore the original class. With a pre-connected sock= parameter,
_make_ssl_transport is called synchronously inside create_connection
before the first await, so the swap is race-free in a single-threaded
event loop.

After the handshake, the session is retrieved via
transport.get_extra_info('ssl_object').session and stored in the pool's
_SSLSessionCache for the next connection.
@blink1073 blink1073 removed the request for review from sleepyStick June 10, 2026 21:17
Module-level import of asyncio.sslproto on PyPy 3.11 changed GC timing
and surfaced a pre-existing unclosed-socket ResourceWarning as a test
error.  Use a module-level None sentinel instead and initialise it on
first use inside the function, which keeps the import lazy while still
capturing the true original SSLProtocol before any patching occurs.
…patching SSLProtocol

Replace the asyncio.sslproto.SSLProtocol monkey-patch with a
_SessionSSLContext wrapper that intercepts wrap_bio() and temporarily
sets sslobject_class to a per-connection ssl.SSLObject subclass that
assigns self.session after __init__. The set/use/restore cycle inside
wrap_bio() is atomic because wrap_bio() is synchronous, so asyncio
cannot yield to another coroutine mid-call.

This removes all global state (_ORIGINAL_SSL_PROTOCOL, the Python 3.11
version guard) and private attribute access (_sslobj), and works on all
supported Python versions without concurrency hazards.
@blink1073 blink1073 changed the title PYTHON-5272 Implement TLS session resumption for sync pool PYTHON-5272 Implement TLS session resumption for connections Jun 11, 2026
…s assignment

Drop the _SessionSSLContext wrapper entirely. On Python 3.11+
SSLProtocol.__init__ calls wrap_bio() synchronously before the first
event-loop yield, so setting ssl_context.sslobject_class directly is
race-free. A per-connection ssl.SSLObject subclass captures the cached
session in a closure and sets self.session after __init__. Session
injection is skipped silently on Python < 3.11 and for PyOpenSSL
contexts (which don't have sslobject_class).
A single-element list provides an atomic mutable slot under the GIL
without needing a custom class or a threading.Lock.  The pool
initialises it as [None]; readers use cache[0] and writers assign
cache[0] = session.
Add three unit tests for _configured_protocol_interface to cover the
previously-missing branches:

- test_async_configured_protocol_injects_session_via_sslobject_class:
  starts the cache with a pre-populated session so the if-session-is-not-None
  branch runs; exercises the _SessionSSLObject.__init__ body via wrap_bio
  with a patched ssl.SSLObject.session setter.
- test_async_configured_protocol_no_cache: passes ssl_session_cache=None to
  cover the False branches of both the injection guard and the save guard.
- test_async_configured_protocol_new_session_is_none: returns None from
  ssl_obj.session to cover the False branch of if-new_session-is-not-None.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants