@@ -128,6 +128,108 @@ def decompress(byts):
128128frame_header_v3 = struct .Struct ('>BhBi' )
129129
130130
131+ class TLSSessionCache :
132+ """
133+ Thread-safe cache for TLS sessions to enable session resumption.
134+
135+ This cache stores TLS sessions per endpoint (host:port) to allow
136+ quick TLS renegotiation when reconnecting to the same server.
137+ Sessions are automatically expired after a TTL and the cache has
138+ a maximum size with LRU eviction.
139+ """
140+
141+ def __init__ (self , max_size = 100 , ttl = 3600 ):
142+ """
143+ Initialize the TLS session cache.
144+
145+ Args:
146+ max_size: Maximum number of sessions to cache (default: 100)
147+ ttl: Time-to-live for cached sessions in seconds (default: 3600)
148+ """
149+ self ._sessions = {} # {endpoint_key: (session, timestamp, access_time)}
150+ self ._lock = RLock ()
151+ self ._max_size = max_size
152+ self ._ttl = ttl
153+
154+ def _make_key (self , host , port ):
155+ """Create a cache key from host and port."""
156+ return (host , port )
157+
158+ def get_session (self , host , port ):
159+ """
160+ Get a cached TLS session for the given endpoint.
161+
162+ Args:
163+ host: The hostname or IP address
164+ port: The port number
165+
166+ Returns:
167+ ssl.SSLSession object if a valid cached session exists, None otherwise
168+ """
169+ key = self ._make_key (host , port )
170+ with self ._lock :
171+ if key not in self ._sessions :
172+ return None
173+
174+ session , timestamp , _ = self ._sessions [key ]
175+
176+ # Check if session has expired
177+ if time .time () - timestamp > self ._ttl :
178+ del self ._sessions [key ]
179+ return None
180+
181+ # Update access time for LRU
182+ self ._sessions [key ] = (session , timestamp , time .time ())
183+ return session
184+
185+ def set_session (self , host , port , session ):
186+ """
187+ Store a TLS session for the given endpoint.
188+
189+ Args:
190+ host: The hostname or IP address
191+ port: The port number
192+ session: The ssl.SSLSession object to cache
193+ """
194+ if session is None :
195+ return
196+
197+ key = self ._make_key (host , port )
198+ current_time = time .time ()
199+
200+ with self ._lock :
201+ # If cache is at max size, remove least recently used entry
202+ if len (self ._sessions ) >= self ._max_size and key not in self ._sessions :
203+ # Find entry with oldest access time
204+ oldest_key = min (self ._sessions .keys (),
205+ key = lambda k : self ._sessions [k ][2 ])
206+ del self ._sessions [oldest_key ]
207+
208+ # Store session with creation time and access time
209+ self ._sessions [key ] = (session , current_time , current_time )
210+
211+ def clear_expired (self ):
212+ """Remove all expired sessions from the cache."""
213+ current_time = time .time ()
214+ with self ._lock :
215+ expired_keys = [
216+ key for key , (_ , timestamp , _ ) in self ._sessions .items ()
217+ if current_time - timestamp > self ._ttl
218+ ]
219+ for key in expired_keys :
220+ del self ._sessions [key ]
221+
222+ def clear (self ):
223+ """Clear all sessions from the cache."""
224+ with self ._lock :
225+ self ._sessions .clear ()
226+
227+ def size (self ):
228+ """Return the current number of cached sessions."""
229+ with self ._lock :
230+ return len (self ._sessions )
231+
232+
131233class EndPoint (object ):
132234 """
133235 Represents the information to connect to a cassandra node.
@@ -687,6 +789,8 @@ class Connection(object):
687789 endpoint = None
688790 ssl_options = None
689791 ssl_context = None
792+ tls_session_cache = None
793+ session_reused = False
690794 last_error = None
691795
692796 # The current number of operations that are in flight. More precisely,
@@ -763,14 +867,16 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
763867 ssl_options = None , sockopts = None , compression : Union [bool , str ] = True ,
764868 cql_version = None , protocol_version = ProtocolVersion .MAX_SUPPORTED , is_control_connection = False ,
765869 user_type_map = None , connect_timeout = None , allow_beta_protocol_version = False , no_compact = False ,
766- ssl_context = None , owning_pool = None , shard_id = None , total_shards = None ,
870+ ssl_context = None , tls_session_cache = None , owning_pool = None , shard_id = None , total_shards = None ,
767871 on_orphaned_stream_released = None , application_info : Optional [ApplicationInfoBase ] = None ):
768872 # TODO next major rename host to endpoint and remove port kwarg.
769873 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
770874
771875 self .authenticator = authenticator
772876 self .ssl_options = ssl_options .copy () if ssl_options else {}
773877 self .ssl_context = ssl_context
878+ self .tls_session_cache = tls_session_cache
879+ self .session_reused = False
774880 self .sockopts = sockopts
775881 self .compression = compression
776882 self .cql_version = cql_version
@@ -913,7 +1019,28 @@ def _wrap_socket_from_context(self):
9131019 server_hostname = self .endpoint .address
9141020 opts ['server_hostname' ] = server_hostname
9151021
916- return self .ssl_context .wrap_socket (self ._socket , ** opts )
1022+ # Try to get a cached TLS session for resumption
1023+ if self .tls_session_cache :
1024+ cached_session = self .tls_session_cache .get_session (
1025+ self .endpoint .address , self .endpoint .port )
1026+ if cached_session :
1027+ opts ['session' ] = cached_session
1028+ log .debug ("Using cached TLS session for %s:%s" ,
1029+ self .endpoint .address , self .endpoint .port )
1030+
1031+ ssl_socket = self .ssl_context .wrap_socket (self ._socket , ** opts )
1032+
1033+ # Store the session for future reuse
1034+ if self .tls_session_cache and ssl_socket .session :
1035+ self .tls_session_cache .set_session (
1036+ self .endpoint .address , self .endpoint .port , ssl_socket .session )
1037+ # Track if the session was reused
1038+ self .session_reused = ssl_socket .session_reused
1039+ if self .session_reused :
1040+ log .debug ("TLS session was reused for %s:%s" ,
1041+ self .endpoint .address , self .endpoint .port )
1042+
1043+ return ssl_socket
9171044
9181045 def _initiate_connection (self , sockaddr ):
9191046 if self .features .shard_id is not None :
0 commit comments