Skip to content

Commit 180e0ed

Browse files
Integrate client routes handler into Cluster and ControlConnection
Cluster: - Add client_routes_config parameter with mutual exclusivity check against endpoint_factory - Create _ClientRoutesHandler and ClientRoutesEndPointFactory when client_routes_config is provided ControlConnection: - Register CLIENT_ROUTES_CHANGE event watcher when handler is present - Forward events to handler via _handle_client_routes_change - Trigger full route re-read on control connection reconnection
1 parent 042f2b9 commit 180e0ed

1 file changed

Lines changed: 92 additions & 5 deletions

File tree

cassandra/cluster.py

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from itertools import groupby, count, chain
3030
import json
3131
import logging
32-
from typing import Optional, Union
32+
from typing import Any, Dict, Optional, Union
3333
from warnings import warn
3434
from random import random
3535
import re
@@ -48,7 +48,8 @@
4848
SchemaTargetType, DriverException, ProtocolVersion,
4949
UnresolvableContactPoints, DependencyException)
5050
from cassandra.auth import _proxy_execute_key, PlainTextAuthProvider
51-
from cassandra.connection import (ConnectionException, ConnectionShutdown,
51+
from cassandra.client_routes import ClientRoutesChangeType, ClientRoutesConfig, _ClientRoutesHandler
52+
from cassandra.connection import (ClientRoutesEndPointFactory, ConnectionException, ConnectionShutdown,
5253
ConnectionHeartbeat, ProtocolVersionUnsupported,
5354
EndPoint, DefaultEndPoint, DefaultEndPointFactory,
5455
SniEndPointFactory, ConnectionBusy, locally_supported_compressions)
@@ -1215,7 +1216,8 @@ def __init__(self,
12151216
shard_aware_options=None,
12161217
metadata_request_timeout: Optional[float] = None,
12171218
column_encryption_policy=None,
1218-
application_info:Optional[ApplicationInfoBase]=None
1219+
application_info:Optional[ApplicationInfoBase]=None,
1220+
client_routes_config:Optional[ClientRoutesConfig]=None
12191221
):
12201222
"""
12211223
``executor_threads`` defines the number of threads in a pool for handling asynchronous tasks such as
@@ -1280,6 +1282,45 @@ def __init__(self,
12801282
if column_encryption_policy is not None:
12811283
self.column_encryption_policy = column_encryption_policy
12821284

1285+
if client_routes_config is not None and endpoint_factory is not None:
1286+
raise ValueError("client_routes_config and endpoint_factory are mutually exclusive")
1287+
1288+
self._client_routes_handler = None
1289+
if client_routes_config is not None:
1290+
if not isinstance(client_routes_config, ClientRoutesConfig):
1291+
raise TypeError("client_routes_config must be a ClientRoutesConfig instance")
1292+
1293+
# SSL hostname verification is incompatible with client routes:
1294+
# connections go through NLB proxies whose addresses won't match
1295+
# server certificates.
1296+
_check_hostname_enabled = False
1297+
if ssl_context is not None and ssl_context.check_hostname:
1298+
_check_hostname_enabled = True
1299+
if ssl_options is not None and ssl_options.get('check_hostname', False):
1300+
_check_hostname_enabled = True
1301+
if _check_hostname_enabled:
1302+
raise ValueError(
1303+
"SSL hostname verification (check_hostname=True) is incompatible "
1304+
"with client_routes_config. When using client routes, connections "
1305+
"go through NLB proxies whose addresses won't match server "
1306+
"certificates. Disable hostname verification by setting "
1307+
"ssl_context.check_hostname = False."
1308+
)
1309+
1310+
ssl_enabled = ssl_context is not None or ssl_options is not None
1311+
self._client_routes_handler = _ClientRoutesHandler(client_routes_config, ssl_enabled=ssl_enabled)
1312+
1313+
if contact_points is _NOT_SET or not self._contact_points_explicit:
1314+
seed_addrs = [dep.connection_addr for dep in client_routes_config.proxies
1315+
if dep.connection_addr]
1316+
if seed_addrs:
1317+
self.contact_points = seed_addrs
1318+
self._contact_points_explicit = True
1319+
log.info("[client routes] Using %d deployment connection addresses as contact points",
1320+
len(seed_addrs))
1321+
1322+
if self._client_routes_handler is not None:
1323+
endpoint_factory = ClientRoutesEndPointFactory(self._client_routes_handler, self.port)
12831324
self.endpoint_factory = endpoint_factory or DefaultEndPointFactory(port=self.port)
12841325
self.endpoint_factory.configure(self)
12851326

@@ -1715,6 +1756,13 @@ def connect(self, keyspace=None, wait_for_all_pools=False):
17151756
self.control_connection.connect()
17161757
self._populate_hosts()
17171758

1759+
if self._client_routes_handler is not None:
1760+
try:
1761+
self._client_routes_handler.initialize(self.control_connection)
1762+
except Exception as e:
1763+
log.error("[control connection] Failed to initialize client routes handler: %s", e, exc_info=True)
1764+
raise
1765+
17181766
log.debug("Control connection created")
17191767
except Exception:
17201768
log.exception("Control connection failed to connect, "
@@ -3612,11 +3660,16 @@ def _try_connect(self, endpoint):
36123660
# this object (after a dereferencing a weakref)
36133661
self_weakref = weakref.ref(self, partial(_clear_watcher, weakref.proxy(connection)))
36143662
try:
3615-
connection.register_watchers({
3663+
watchers = {
36163664
"TOPOLOGY_CHANGE": partial(_watch_callback, self_weakref, '_handle_topology_change'),
36173665
"STATUS_CHANGE": partial(_watch_callback, self_weakref, '_handle_status_change'),
36183666
"SCHEMA_CHANGE": partial(_watch_callback, self_weakref, '_handle_schema_change')
3619-
}, register_timeout=self._timeout)
3667+
}
3668+
3669+
if self._cluster._client_routes_handler is not None:
3670+
watchers["CLIENT_ROUTES_CHANGE"] = partial(_watch_callback, self_weakref, '_handle_client_routes_change')
3671+
3672+
connection.register_watchers(watchers, register_timeout=self._timeout)
36203673

36213674
sel_peers = self._get_peers_query(self.PeersQueryType.PEERS, connection)
36223675
sel_local = self._SELECT_LOCAL if self._token_meta_enabled else self._SELECT_LOCAL_NO_TOKENS
@@ -3658,6 +3711,12 @@ def _reconnect(self):
36583711
log.debug("[control connection] Attempting to reconnect")
36593712
try:
36603713
self._set_new_connection(self._reconnect_internal())
3714+
3715+
if self._cluster._client_routes_handler is not None:
3716+
try:
3717+
self._cluster._client_routes_handler.handle_control_connection_reconnect(self)
3718+
except Exception as e:
3719+
log.warning("[control connection] Failed to notify client routes handler of reconnection: %s", e)
36613720
except NoHostAvailable:
36623721
# make a retry schedule (which includes backoff)
36633722
schedule = self._cluster.reconnection_policy.new_schedule()
@@ -3979,6 +4038,34 @@ def _handle_status_change(self, event):
39794038
# this will be run by the scheduler
39804039
self._cluster.on_down(host, is_host_addition=False)
39814040

4041+
def _handle_client_routes_change(self, event: Dict[str, Any]) -> None:
4042+
"""
4043+
Handle CLIENT_ROUTES_CHANGE event from the server.
4044+
4045+
This event indicates that the system.client_routes table has been updated
4046+
and we need to refresh our route mappings.
4047+
"""
4048+
if self._cluster._client_routes_handler is None:
4049+
log.warning("[control connection] Received CLIENT_ROUTES_CHANGE but no handler configured")
4050+
return
4051+
4052+
raw_change_type = event.get("change_type")
4053+
try:
4054+
change_type = ClientRoutesChangeType(raw_change_type)
4055+
except ValueError:
4056+
log.warning("[control connection] Unknown CLIENT_ROUTES_CHANGE type: %s", raw_change_type)
4057+
return
4058+
4059+
connection_ids = tuple(event.get("connection_ids", []))
4060+
host_ids = tuple(event.get("host_ids", []))
4061+
4062+
# Handle the event asynchronously
4063+
self._cluster.scheduler.schedule_unique(
4064+
0,
4065+
self._cluster._client_routes_handler.handle_client_routes_change,
4066+
self, change_type, connection_ids, host_ids
4067+
)
4068+
39824069
def _handle_schema_change(self, event):
39834070
if self._schema_event_refresh_window < 0:
39844071
return

0 commit comments

Comments
 (0)