Skip to content

Commit 106ca95

Browse files
Copilotdkropachev
andcommitted
Refactor TLS session caching to use abstractions and endpoint-based API
Changes requested by @dkropachev: 1. Changed set_session() to accept Endpoint objects instead of host/port 2. Added configuration option to cache by host only or by host+port 3. Created abstract base classes (TLSSessionCache, TLSSessionCacheOptions) 4. Moved implementations to cassandra/tls.py module 5. Updated Cluster to use TLSSessionCacheOptions for configuration Benefits: - More flexible caching strategies (by host or by host+port) - Cleaner separation of concerns with abstractions - Easier to extend with custom implementations - Single configuration object instead of multiple parameters Co-authored-by: dkropachev <40304587+dkropachev@users.noreply.github.com>
1 parent 5985be5 commit 106ca95

4 files changed

Lines changed: 441 additions & 200 deletions

File tree

cassandra/cluster.py

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,74 @@ def _connection_reduce_fn(val,import_fn):
200200
_DEFAULT_TLS_SESSION_CACHE_TTL = 3600 # 1 hour in seconds
201201

202202

203+
# Abstract base classes for TLS session caching - implementations in cassandra.tls
204+
from abc import ABC, abstractmethod
205+
206+
207+
class TLSSessionCache(ABC):
208+
"""
209+
Abstract base class for TLS session caching.
210+
211+
Implementations should provide thread-safe caching of TLS sessions
212+
to enable session resumption for faster reconnections.
213+
"""
214+
215+
@abstractmethod
216+
def get_session(self, endpoint):
217+
"""
218+
Get a cached TLS session for the given endpoint.
219+
220+
Args:
221+
endpoint: The EndPoint object representing the connection target
222+
223+
Returns:
224+
ssl.SSLSession object if a valid cached session exists, None otherwise
225+
"""
226+
pass
227+
228+
@abstractmethod
229+
def set_session(self, endpoint, session):
230+
"""
231+
Store a TLS session for the given endpoint.
232+
233+
Args:
234+
endpoint: The EndPoint object representing the connection target
235+
session: The ssl.SSLSession object to cache
236+
"""
237+
pass
238+
239+
@abstractmethod
240+
def clear_expired(self):
241+
"""Remove all expired sessions from the cache."""
242+
pass
243+
244+
@abstractmethod
245+
def clear(self):
246+
"""Clear all sessions from the cache."""
247+
pass
248+
249+
@abstractmethod
250+
def size(self):
251+
"""Return the current number of cached sessions."""
252+
pass
253+
254+
255+
class TLSSessionCacheOptions(ABC):
256+
"""
257+
Abstract base class for TLS session cache configuration options.
258+
"""
259+
260+
@abstractmethod
261+
def create_cache(self):
262+
"""
263+
Build and return a TLSSessionCache implementation.
264+
265+
Returns:
266+
TLSSessionCache: A configured session cache instance
267+
"""
268+
pass
269+
270+
203271
class NoHostAvailable(Exception):
204272
"""
205273
Raised when an operation is attempted but all connections are
@@ -879,33 +947,28 @@ def default_retry_policy(self, policy):
879947
.. versionadded:: 3.17.0
880948
"""
881949

882-
tls_session_cache_enabled = True
950+
tls_session_cache_options = None
883951
"""
884-
Enable TLS session caching for faster reconnections. When enabled, TLS sessions
885-
are cached per endpoint and reused for subsequent connections to the same server.
886-
This reduces handshake latency and CPU usage during reconnections.
952+
TLS session cache configuration. Set to an instance of :class:`~.TLSSessionCacheOptions`
953+
to configure session caching behavior. If None (default), a default configuration will
954+
be used when SSL/TLS is enabled.
887955
888-
Defaults to True when SSL/TLS is enabled. Set to False to disable session caching.
956+
The default configuration caches sessions by host and port, with a maximum of 100 sessions
957+
and a TTL of 3600 seconds (1 hour).
889958
890-
.. versionadded:: 3.30.0
891-
"""
892-
893-
tls_session_cache_size = _DEFAULT_TLS_SESSION_CACHE_SIZE
894-
"""
895-
Maximum number of TLS sessions to cache. When the cache is full, the least
896-
recently used session is evicted.
959+
To disable session caching entirely, set this to False.
897960
898-
Defaults to 100.
899-
900-
.. versionadded:: 3.30.0
901-
"""
902-
903-
tls_session_cache_ttl = _DEFAULT_TLS_SESSION_CACHE_TTL
904-
"""
905-
Time-to-live for cached TLS sessions in seconds. Sessions older than this
906-
are not reused and are removed from the cache.
961+
Example::
907962
908-
Defaults to 3600 seconds (1 hour).
963+
from cassandra.tls import DefaultTLSSessionCacheOptions
964+
965+
# Cache by host only (ignoring port)
966+
options = DefaultTLSSessionCacheOptions(
967+
max_size=200,
968+
ttl=7200,
969+
cache_by_host_only=True
970+
)
971+
cluster = Cluster(ssl_context=ssl_context, tls_session_cache_options=options)
909972
910973
.. versionadded:: 3.30.0
911974
"""
@@ -1239,9 +1302,7 @@ def __init__(self,
12391302
idle_heartbeat_timeout=30,
12401303
no_compact=False,
12411304
ssl_context=None,
1242-
tls_session_cache_enabled=True,
1243-
tls_session_cache_size=_DEFAULT_TLS_SESSION_CACHE_SIZE,
1244-
tls_session_cache_ttl=_DEFAULT_TLS_SESSION_CACHE_TTL,
1305+
tls_session_cache_options=None,
12451306
endpoint_factory=None,
12461307
application_name=None,
12471308
application_version=None,
@@ -1458,18 +1519,24 @@ def __init__(self,
14581519

14591520
self.ssl_options = ssl_options
14601521
self.ssl_context = ssl_context
1461-
self.tls_session_cache_enabled = tls_session_cache_enabled
1462-
self.tls_session_cache_size = tls_session_cache_size
1463-
self.tls_session_cache_ttl = tls_session_cache_ttl
1522+
self.tls_session_cache_options = tls_session_cache_options
14641523

14651524
# Initialize TLS session cache if SSL is enabled
14661525
self._tls_session_cache = None
1467-
if (ssl_context or ssl_options) and tls_session_cache_enabled:
1468-
from cassandra.connection import TLSSessionCache
1469-
self._tls_session_cache = TLSSessionCache(
1470-
max_size=tls_session_cache_size,
1471-
ttl=tls_session_cache_ttl
1472-
)
1526+
if (ssl_context or ssl_options) and tls_session_cache_options is not False:
1527+
from cassandra.tls import DefaultTLSSessionCacheOptions
1528+
1529+
# Use provided options or create default
1530+
if tls_session_cache_options is None:
1531+
cache_options = DefaultTLSSessionCacheOptions(
1532+
max_size=_DEFAULT_TLS_SESSION_CACHE_SIZE,
1533+
ttl=_DEFAULT_TLS_SESSION_CACHE_TTL,
1534+
cache_by_host_only=False
1535+
)
1536+
else:
1537+
cache_options = tls_session_cache_options
1538+
1539+
self._tls_session_cache = cache_options.create_cache()
14731540

14741541
self.sockopts = sockopts
14751542
self.cql_version = cql_version

cassandra/connection.py

Lines changed: 2 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -128,121 +128,6 @@ def decompress(byts):
128128
frame_header_v3 = struct.Struct('>BhBi')
129129

130130

131-
# Named tuple for TLS session cache entries
132-
_SessionCacheEntry = namedtuple('_SessionCacheEntry', ['session', 'timestamp'])
133-
134-
135-
class TLSSessionCache:
136-
"""
137-
Thread-safe cache for TLS sessions to enable session resumption.
138-
139-
This cache stores TLS sessions per endpoint (host:port) to allow
140-
quick TLS renegotiation when reconnecting to the same server.
141-
Sessions are automatically expired after a TTL and the cache has
142-
a maximum size with LRU eviction using OrderedDict.
143-
144-
TLS session resumption works with both TLS 1.2 and TLS 1.3:
145-
- TLS 1.2: Session IDs (RFC 5246) and optionally Session Tickets (RFC 5077)
146-
- TLS 1.3: Session Tickets (RFC 8446)
147-
148-
Python's ssl.SSLSession API handles both versions transparently, so no
149-
version-specific checks are needed.
150-
"""
151-
152-
def __init__(self, max_size=100, ttl=3600):
153-
"""
154-
Initialize the TLS session cache.
155-
156-
Args:
157-
max_size: Maximum number of sessions to cache (default: 100)
158-
ttl: Time-to-live for cached sessions in seconds (default: 3600)
159-
"""
160-
self._sessions = OrderedDict() # OrderedDict for O(1) LRU eviction
161-
self._lock = RLock()
162-
self._max_size = max_size
163-
self._ttl = ttl
164-
165-
def _make_key(self, host, port):
166-
"""Create a cache key from host and port."""
167-
return (host, port)
168-
169-
def get_session(self, host, port):
170-
"""
171-
Get a cached TLS session for the given endpoint.
172-
173-
Args:
174-
host: The hostname or IP address
175-
port: The port number
176-
177-
Returns:
178-
ssl.SSLSession object if a valid cached session exists, None otherwise
179-
"""
180-
key = self._make_key(host, port)
181-
with self._lock:
182-
if key not in self._sessions:
183-
return None
184-
185-
entry = self._sessions[key]
186-
187-
# Check if session has expired
188-
if time.time() - entry.timestamp > self._ttl:
189-
del self._sessions[key]
190-
return None
191-
192-
# Move to end to mark as recently used (LRU)
193-
self._sessions.move_to_end(key)
194-
return entry.session
195-
196-
def set_session(self, host, port, session):
197-
"""
198-
Store a TLS session for the given endpoint.
199-
200-
Args:
201-
host: The hostname or IP address
202-
port: The port number
203-
session: The ssl.SSLSession object to cache
204-
"""
205-
if session is None:
206-
return
207-
208-
key = self._make_key(host, port)
209-
current_time = time.time()
210-
211-
with self._lock:
212-
# If key already exists, just update it
213-
if key in self._sessions:
214-
self._sessions[key] = _SessionCacheEntry(session, current_time)
215-
self._sessions.move_to_end(key)
216-
return
217-
218-
# If cache is at max size, remove least recently used entry (first item)
219-
if len(self._sessions) >= self._max_size:
220-
self._sessions.popitem(last=False)
221-
222-
# Store session with creation time
223-
self._sessions[key] = _SessionCacheEntry(session, current_time)
224-
225-
def clear_expired(self):
226-
"""Remove all expired sessions from the cache."""
227-
current_time = time.time()
228-
with self._lock:
229-
expired_keys = [
230-
key for key, entry in self._sessions.items()
231-
if current_time - entry.timestamp > self._ttl
232-
]
233-
for key in expired_keys:
234-
del self._sessions[key]
235-
236-
def clear(self):
237-
"""Clear all sessions from the cache."""
238-
with self._lock:
239-
self._sessions.clear()
240-
241-
def size(self):
242-
"""Return the current number of cached sessions."""
243-
with self._lock:
244-
return len(self._sessions)
245-
246131

247132
class EndPoint(object):
248133
"""
@@ -1037,8 +922,7 @@ def _wrap_socket_from_context(self):
1037922
# Note: Session resumption works with both TLS 1.2 and TLS 1.3
1038923
# Python's ssl module handles both transparently via SSLSession objects
1039924
if self.tls_session_cache:
1040-
cached_session = self.tls_session_cache.get_session(
1041-
self.endpoint.address, self.endpoint.port)
925+
cached_session = self.tls_session_cache.get_session(self.endpoint)
1042926
if cached_session:
1043927
opts['session'] = cached_session
1044928
log.debug("Using cached TLS session for %s:%s",
@@ -1109,8 +993,7 @@ def _connect_socket(self):
1109993
# This ensures we only cache sessions for connections that actually succeeded
1110994
if self.tls_session_cache and self.ssl_context and hasattr(self._socket, 'session'):
1111995
if self._socket.session:
1112-
self.tls_session_cache.set_session(
1113-
self.endpoint.address, self.endpoint.port, self._socket.session)
996+
self.tls_session_cache.set_session(self.endpoint, self._socket.session)
1114997
# Track if the session was reused
1115998
self.session_reused = self._socket.session_reused
1116999
if self.session_reused:

0 commit comments

Comments
 (0)