Repository navigation
perf: share SSL contexts across connections to speed up connect() #1031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vuanhphung
wants to merge
7
commits into
main
Choose a base branch
from
perf/conn-setup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
aadb5e0
perf: share SSL contexts across connections
vuanhphung 165dbbd
perf: build SSL contexts under a per-key lock
vuanhphung 379e1b7
Document the shared-context invariant; make parity test host-independent
vuanhphung b8d5fb0
Rebuild cached SSL contexts after 10 minutes
vuanhphung c1fd8c6
Keep THttpClient's fail-fast check for client certificates
vuanhphung d4f71e8
Include ctime in the CA file signature
vuanhphung 78f1e14
Stop urllib3 from rewriting shared SSL contexts per connection
vuanhphung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| """Process-wide reuse of client SSL contexts. | ||
|
|
||
| Loading a CA bundle into a new context costs ~10-15 ms with OpenSSL 3 and does | ||
| not scale across threads, so building one per connection caps how fast a | ||
| process can open connections. A cached context is reused only by callers with | ||
| identical TLS options and unchanged CA files. Configurations with a client | ||
| certificate are never cached. Contexts are rebuilt after ``_TTL_SECONDS`` so | ||
| trust-store changes that no file signature covers (e.g. the Windows | ||
| certificate store) still apply without a restart. | ||
| """ | ||
|
|
||
| import os | ||
| import ssl | ||
| import threading | ||
| import time | ||
| from collections import OrderedDict | ||
| from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple | ||
|
|
||
| from urllib3.util.ssl_ import ALPN_PROTOCOLS | ||
|
|
||
| if TYPE_CHECKING: | ||
| from databricks.sql.types import SSLOptions | ||
|
|
||
| _MAX_ENTRIES = 32 | ||
| _TTL_SECONDS = 600 | ||
|
|
||
| _lock = threading.Lock() | ||
| _contexts: "OrderedDict[tuple, Tuple[ssl.SSLContext, float]]" = OrderedDict() | ||
| _build_locks: Dict[tuple, Any] = {} | ||
|
|
||
| _BASE_CONTEXT_CLASS = ssl.SSLContext | ||
|
|
||
|
|
||
| class _SharedSSLContext(_BASE_CONTEXT_CLASS): # type: ignore[valid-type,misc] | ||
| """A cached context that ignores same-value writes. | ||
|
|
||
| urllib3 re-applies ALPN and verify_mode to its context on every connection. | ||
| SSL_new() copies the context's ALPN buffer without the GIL, so replacing | ||
| that buffer while another thread connects can corrupt the ClientHello. | ||
| """ | ||
|
|
||
| def set_alpn_protocols(self, alpn_protocols): | ||
| protocols = list(alpn_protocols) | ||
| if protocols != getattr(self, "_shared_alpn_protocols", None): | ||
| super().set_alpn_protocols(protocols) | ||
| self._shared_alpn_protocols = protocols | ||
|
|
||
| @property | ||
| def verify_mode(self): | ||
| return super().verify_mode | ||
|
|
||
| @verify_mode.setter | ||
| def verify_mode(self, value): | ||
| if super().verify_mode != value: | ||
| _BASE_CONTEXT_CLASS.verify_mode.__set__(self, value) | ||
|
|
||
|
|
||
| def _make_shareable(context: ssl.SSLContext) -> bool: | ||
| # Only plain stdlib contexts are shared; others (e.g. pyOpenSSL) stay per caller. | ||
| if type(context) is not _BASE_CONTEXT_CLASS: | ||
| return False | ||
| context.__class__ = _SharedSSLContext | ||
| context.set_alpn_protocols(ALPN_PROTOCOLS) | ||
| return True | ||
|
|
||
|
|
||
| def _file_signature(path) -> Optional[Tuple]: | ||
| # Rewriting or replacing a CA file yields a new key, so rotation is picked up. | ||
| if not path: | ||
| return None | ||
| try: | ||
| st = os.stat(path) | ||
| except (OSError, TypeError, ValueError): | ||
| return (path, None) | ||
| # ctime too: in-place rewrites that preserve mtime (cp -p, rsync -t) still change it. | ||
| return (path, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns) | ||
|
|
||
|
|
||
| def _cache_key(kind: str, ssl_options: "SSLOptions") -> tuple: | ||
| defaults = ssl.get_default_verify_paths() # honors SSL_CERT_FILE / SSL_CERT_DIR | ||
| return ( | ||
| kind, | ||
| bool(ssl_options.tls_verify), | ||
| bool(ssl_options.tls_verify_hostname), | ||
| _file_signature(ssl_options.tls_trusted_ca_file), | ||
| _file_signature(defaults.cafile), | ||
| defaults.capath, | ||
| os.environ.get("SSLKEYLOGFILE"), | ||
| ) | ||
|
|
||
|
|
||
| def _fresh_context(key: tuple) -> Optional[ssl.SSLContext]: | ||
| # Caller holds _lock. | ||
| entry = _contexts.get(key) | ||
| if entry is None or time.monotonic() - entry[1] > _TTL_SECONDS: | ||
| return None | ||
| _contexts.move_to_end(key) | ||
| return entry[0] | ||
|
|
||
|
|
||
| def is_cacheable(ssl_options: "SSLOptions") -> bool: | ||
| return not ( | ||
| ssl_options.tls_client_cert_file or ssl_options.tls_client_cert_key_file | ||
| ) | ||
|
|
||
|
|
||
| def get_ssl_context( | ||
| kind: str, ssl_options: "SSLOptions", build: Callable[[], ssl.SSLContext] | ||
| ) -> ssl.SSLContext: | ||
| """Return the shared context for ``kind`` and ``ssl_options``, building it once. | ||
|
|
||
| ``kind`` separates builders that configure contexts differently. urllib3 | ||
| writes a pool's ``cert_reqs`` and ALPN protocols back onto its context on | ||
| every connection; the shared context skips those writes only when nothing | ||
| changes, so every pool sharing a context must use TLS pool kwargs derived | ||
| only from ``ssl_options`` (and no ``assert_hostname``). Do not otherwise | ||
| modify the returned context. Contexts other than plain ``ssl.SSLContext`` | ||
| are returned without caching. | ||
| """ | ||
| if not is_cacheable(ssl_options): | ||
| return build() | ||
|
|
||
| key = _cache_key(kind, ssl_options) | ||
| with _lock: | ||
| context = _fresh_context(key) | ||
| if context is not None: | ||
| return context | ||
| key_lock = _build_locks.setdefault(key, threading.Lock()) | ||
|
|
||
| # Build outside the shared lock so a slow CA file only delays its own key. | ||
| with key_lock: | ||
| with _lock: | ||
| context = _fresh_context(key) | ||
| if context is not None: | ||
| return context | ||
| try: | ||
| context = build() | ||
| shareable = _make_shareable(context) | ||
| except BaseException: | ||
| _forget_build_lock(key, key_lock) | ||
| raise | ||
| if not shareable: | ||
| _forget_build_lock(key, key_lock) | ||
| return context | ||
| with _lock: | ||
| _contexts[key] = (context, time.monotonic()) | ||
| _contexts.move_to_end(key) | ||
| if len(_contexts) > _MAX_ENTRIES: | ||
| _contexts.popitem(last=False) | ||
| _forget_build_lock(key, key_lock) | ||
| return context | ||
|
|
||
|
|
||
| def _forget_build_lock(key: tuple, key_lock: Any) -> None: | ||
| with _lock: | ||
| if _build_locks.get(key) is key_lock: | ||
| del _build_locks[key] | ||
|
|
||
|
|
||
| def clear() -> None: | ||
| with _lock: | ||
| _contexts.clear() | ||
|
|
||
|
|
||
| def _reset_after_fork() -> None: | ||
| # A fork during a build would leave the child's copies of the locks held. | ||
| global _lock | ||
| _lock = threading.Lock() | ||
| _build_locks.clear() | ||
|
|
||
|
|
||
| if hasattr(os, "register_at_fork"): | ||
| os.register_at_fork(after_in_child=_reset_after_fork) | ||
|
peco-review-bot[bot] marked this conversation as resolved.
Resolved
|
||
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.