|
25 | 25 | from threading import Thread, Event, RLock, Condition |
26 | 26 | import time |
27 | 27 | import ssl |
| 28 | +import uuid |
28 | 29 | import weakref |
29 | 30 | import random |
30 | 31 | import itertools |
31 | | -from typing import Optional, Union |
| 32 | +from typing import Any, Dict, Optional, Tuple, Union |
32 | 33 |
|
33 | 34 | from cassandra.application_info import ApplicationInfoBase |
| 35 | +from cassandra.client_routes import _ClientRoutesHandler |
34 | 36 | from cassandra.protocol_features import ProtocolFeatures |
35 | 37 |
|
36 | 38 | if 'gevent.monkey' in sys.modules: |
@@ -230,7 +232,7 @@ class DefaultEndPointFactory(EndPointFactory): |
230 | 232 | port = None |
231 | 233 | """ |
232 | 234 | If no port is discovered in the row, this is the default port |
233 | | - used for endpoint creation. |
| 235 | + used for endpoint creation. |
234 | 236 | """ |
235 | 237 |
|
236 | 238 | def __init__(self, port=None): |
@@ -328,6 +330,50 @@ def create_from_sni(self, sni): |
328 | 330 | return SniEndPoint(self._proxy_address, sni, self._port) |
329 | 331 |
|
330 | 332 |
|
| 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 | + |
331 | 377 | @total_ordering |
332 | 378 | class UnixSocketEndPoint(EndPoint): |
333 | 379 | """ |
@@ -369,6 +415,76 @@ def __repr__(self): |
369 | 415 | return "<%s: %s>" % (self.__class__.__name__, self._unix_socket_path) |
370 | 416 |
|
371 | 417 |
|
| 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 | + |
372 | 488 | class _Frame(object): |
373 | 489 | def __init__(self, version, flags, stream, opcode, body_offset, end_pos): |
374 | 490 | self.version = version |
|
0 commit comments