PYTHON-5272 Implement TLS session resumption for connections#2864
Draft
blink1073 wants to merge 16 commits into
Draft
PYTHON-5272 Implement TLS session resumption for connections#2864blink1073 wants to merge 16 commits into
blink1073 wants to merge 16 commits into
Conversation
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.
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 Report❌ Patch coverage is
📢 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.
Capture _ORIGINAL_SSL_PROTOCOL once at module load time and always restore to it unconditionally, so that concurrent connections from the same pool cannot leave a stale SSLProtocol subclass active in asyncio.sslproto.
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.
…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).
…nnecessary isinstance guard
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
_SSLSessionCacheget/set behaviorwrap_socketon subsequent connections (sync, no server required)SSLObjectbefore the handshake on Python 3.11+ (async, no server required)Checklist
Checklist for Author
Checklist for Reviewer