Skip to content

Commit 042f2b9

Browse files
Add ClientRoutesEndPoint and ClientRoutesEndPointFactory
- ClientRoutesEndPointFactory: creates endpoints from system.peers rows by extracting host_id, deferring address translation and DNS resolution until connection time - ClientRoutesEndPoint: endpoint that resolves via _ClientRoutesHandler on each connection attempt, ensuring immediate reaction to route changes and CLIENT_ROUTES_CHANGE events
1 parent a54b35a commit 042f2b9

1 file changed

Lines changed: 118 additions & 2 deletions

File tree

cassandra/connection.py

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
from threading import Thread, Event, RLock, Condition
2626
import time
2727
import ssl
28+
import uuid
2829
import weakref
2930
import random
3031
import itertools
31-
from typing import Optional, Union
32+
from typing import Any, Dict, Optional, Tuple, Union
3233

3334
from cassandra.application_info import ApplicationInfoBase
35+
from cassandra.client_routes import _ClientRoutesHandler
3436
from cassandra.protocol_features import ProtocolFeatures
3537

3638
if 'gevent.monkey' in sys.modules:
@@ -230,7 +232,7 @@ class DefaultEndPointFactory(EndPointFactory):
230232
port = None
231233
"""
232234
If no port is discovered in the row, this is the default port
233-
used for endpoint creation.
235+
used for endpoint creation.
234236
"""
235237

236238
def __init__(self, port=None):
@@ -328,6 +330,50 @@ def create_from_sni(self, sni):
328330
return SniEndPoint(self._proxy_address, sni, self._port)
329331

330332

333+
class ClientRoutesEndPointFactory(EndPointFactory):
334+
"""
335+
EndPointFactory for Client Routes (Private Link) support.
336+
337+
Creates ClientRoutesEndPoint instances that defer both address translation
338+
(host_id -> hostname lookup) and DNS resolution until connection time.
339+
This ensures immediate reaction to infrastructure changes.
340+
"""
341+
342+
client_routes_handler: _ClientRoutesHandler
343+
default_port: Optional[int]
344+
345+
def __init__(self, client_routes_handler: _ClientRoutesHandler, port: Optional[int] = None) -> None:
346+
"""
347+
:param client_routes_handler: _ClientRoutesHandler instance to lookup routes
348+
:param port: Default port if none found in row
349+
"""
350+
self.client_routes_handler = client_routes_handler
351+
self.default_port = port
352+
353+
def create(self, row: Dict[str, Any]) -> 'ClientRoutesEndPoint':
354+
"""
355+
Create a ClientRoutesEndPoint from a system.peers row.
356+
357+
Stores only the host_id and handler reference. Both translation
358+
(route lookup) and DNS resolution happen later in resolve().
359+
"""
360+
from cassandra.metadata import _NodeInfo
361+
host_id = row.get("host_id")
362+
363+
if host_id is None:
364+
raise ValueError("No host_id to create ClientRoutesEndPoint")
365+
366+
addr = _NodeInfo.get_broadcast_rpc_address(row)
367+
port = _NodeInfo.get_broadcast_rpc_port(row) or _NodeInfo.get_broadcast_port(row) or self.default_port
368+
369+
return ClientRoutesEndPoint(
370+
host_id=host_id,
371+
handler=self.client_routes_handler,
372+
original_address=addr,
373+
original_port=port,
374+
)
375+
376+
331377
@total_ordering
332378
class UnixSocketEndPoint(EndPoint):
333379
"""
@@ -369,6 +415,76 @@ def __repr__(self):
369415
return "<%s: %s>" % (self.__class__.__name__, self._unix_socket_path)
370416

371417

418+
@total_ordering
419+
class ClientRoutesEndPoint(EndPoint):
420+
"""
421+
Client Routes (Private Link) EndPoint implementation.
422+
423+
Defers both address translation (route lookup) and DNS resolution
424+
until resolve() is called at connection time. This ensures immediate
425+
reaction to infrastructure changes and CLIENT_ROUTES_CHANGE events.
426+
"""
427+
428+
_host_id: uuid.UUID
429+
_handler: _ClientRoutesHandler
430+
_original_address: str
431+
_original_port: Optional[int]
432+
433+
def __init__(self, host_id: uuid.UUID, handler: _ClientRoutesHandler, original_address: str, original_port: Optional[int] = None) -> None:
434+
"""
435+
:param host_id: Host UUID for route lookup
436+
:param handler: _ClientRoutesHandler instance
437+
:param original_address: Original address from system.peers (for identification)
438+
:param original_port: Original port if route doesn't specify one
439+
"""
440+
self._host_id = host_id
441+
self._handler = handler
442+
self._original_address = original_address
443+
self._original_port = original_port
444+
445+
@property
446+
def address(self) -> str:
447+
"""Returns the original address (updated by resolve())."""
448+
return self._original_address
449+
450+
@property
451+
def port(self) -> Optional[int]:
452+
return self._original_port
453+
454+
@property
455+
def host_id(self) -> uuid.UUID:
456+
return self._host_id
457+
458+
def resolve(self) -> Tuple[str, int]:
459+
"""
460+
Resolve endpoint by delegating to the handler.
461+
Falls back to original address/port if no route mapping is available.
462+
"""
463+
result = self._handler.resolve_host(self._host_id)
464+
if result is None:
465+
return self._original_address, self._original_port
466+
return result
467+
468+
def __eq__(self, other):
469+
return (isinstance(other, ClientRoutesEndPoint) and
470+
self._host_id == other._host_id and
471+
self._original_address == other._original_address)
472+
473+
def __hash__(self):
474+
return hash((self._host_id, self._original_address))
475+
476+
def __lt__(self, other):
477+
return ((self._host_id, self._original_address) <
478+
(other._host_id, other._original_address))
479+
480+
def __str__(self):
481+
return str("%s (host_id=%s)" % (self._original_address, self._host_id))
482+
483+
def __repr__(self):
484+
return "<%s: host_id=%s, original_addr=%s>" % (
485+
self.__class__.__name__, self._host_id, self._original_address)
486+
487+
372488
class _Frame(object):
373489
def __init__(self, version, flags, stream, opcode, body_offset, end_pos):
374490
self.version = version

0 commit comments

Comments
 (0)