diff --git a/cassandra/cluster.py b/cassandra/cluster.py index 57fcf46331..d858f5835e 100644 --- a/cassandra/cluster.py +++ b/cassandra/cluster.py @@ -51,7 +51,8 @@ from cassandra.connection import (ClientRoutesEndPointFactory, ConnectionException, ConnectionShutdown, ConnectionHeartbeat, ProtocolVersionUnsupported, EndPoint, DefaultEndPoint, DefaultEndPointFactory, - SniEndPointFactory, ConnectionBusy, locally_supported_compressions) + SniEndPointFactory, UnixSocketEndPoint, + ConnectionBusy, locally_supported_compressions) from cassandra.cqltypes import UserType import cassandra.cqltypes as types from cassandra.encoder import Encoder @@ -2225,8 +2226,7 @@ def get_control_connection_host(self): Returns the control connection host metadata. """ connection = self.control_connection._connection - endpoint = connection.endpoint if connection else None - return self.metadata.get_host(endpoint) if endpoint else None + return self.control_connection._get_host_for_connection(connection) def refresh_schema_metadata(self, max_schema_agreement_wait=None): """ @@ -4131,6 +4131,7 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, found_host_ids = set() found_endpoints = set() + local_row = None if local_result.parsed_rows: local_rows = dict_factory(local_result.column_names, local_result.parsed_rows) local_row = local_rows[0] @@ -4150,11 +4151,13 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, if not self._is_valid_peer(row): continue - endpoint = self._cluster.endpoint_factory.create(row) + factory_endpoint = self._cluster.endpoint_factory.create(row) host_id = row.get("host_id") - if endpoint in found_endpoints: - log.warning("Found multiple hosts with the same endpoint(%s). Excluding peer %s - %s", endpoint, row.get("peer"), host_id) + # Use the factory endpoint for duplicate detection even when a Unix + # socket is retained as the route to the local host. + if factory_endpoint in found_endpoints: + log.warning("Found multiple hosts with the same endpoint(%s). Excluding peer %s - %s", factory_endpoint, row.get("peer"), host_id) continue if host_id in found_host_ids: @@ -4162,13 +4165,28 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, continue found_host_ids.add(host_id) - found_endpoints.add(endpoint) + found_endpoints.add(factory_endpoint) + existing_host = self._cluster.metadata.get_host_by_host_id(host_id) + + # Host hashes depend on their endpoint, so never replace the route + # of an existing Host with or from a Unix socket. A newly discovered + # local Host keeps the socket which actually reached the node. + if (existing_host is not None and + isinstance(existing_host.endpoint, UnixSocketEndPoint)): + endpoint = existing_host.endpoint + elif (existing_host is None and row is local_row and + isinstance(connection.original_endpoint, + UnixSocketEndPoint)): + endpoint = connection.original_endpoint + else: + endpoint = factory_endpoint + host = self._cluster.metadata.get_host(endpoint) datacenter = row.get("data_center") rack = row.get("rack") if host is None: - host = self._cluster.metadata.get_host_by_host_id(host_id) + host = existing_host if host and host.endpoint != endpoint: log.debug("[control connection] Updating host ip from %s to %s for (%s)", host.endpoint, endpoint, host_id) reconnector = host.get_and_set_reconnection_handler(None) @@ -4198,6 +4216,9 @@ def _refresh_node_list_and_token_map(self, connection, preloaded_results=None, host.dse_workload = row.get("workload") host.dse_workloads = row.get("workloads") + if row is local_row: + connection._control_connection_host_id = host_id + tokens = row.get("tokens", None) if partitioner and tokens and self._token_meta_enabled: token_map[host] = tokens @@ -4465,8 +4486,15 @@ def _get_schema_mismatches(self, peers_result, local_result, local_address): continue endpoint = self._cluster.endpoint_factory.create(row) peer = self._cluster.metadata.get_host(endpoint) + if peer is None: + peer_by_host_id = self._cluster.metadata.get_host_by_host_id( + row.get('host_id')) + if (peer_by_host_id is not None and + isinstance(peer_by_host_id.endpoint, + UnixSocketEndPoint)): + peer = peer_by_host_id if peer and peer.is_up is not False: - versions[schema_ver].add(endpoint) + versions[schema_ver].add(peer.endpoint) if len(versions) == 1: log.debug("[control connection] Schemas match") @@ -4474,6 +4502,34 @@ def _get_schema_mismatches(self, peers_result, local_result, local_address): return dict((version, list(nodes)) for version, nodes in versions.items()) + def _get_host_for_connection(self, connection): + if connection is None: + return None + + host_id = getattr(connection, '_control_connection_host_id', None) + if host_id is not None: + host = self._cluster.metadata.get_host_by_host_id(host_id) + if host is not None: + return host + + original_endpoint = getattr(connection, 'original_endpoint', None) + if original_endpoint is not None: + host = self._cluster.metadata.get_host(original_endpoint) + if host is not None: + return host + + return self._cluster.metadata.get_host(connection.endpoint) + + def _connection_matches_host(self, connection, host): + if connection is None: + return False + + host_id = getattr(connection, '_control_connection_host_id', None) + if host_id is not None and host_id == host.host_id: + return True + + return self._get_host_for_connection(connection) is host + def _get_peers_query(self, peers_query_type, connection=None): """ Determine the peers query to use. @@ -4504,9 +4560,10 @@ def _get_peers_query(self, peers_query_type, connection=None): query_template = (self._SELECT_SCHEMA_PEERS_TEMPLATE if peers_query_type == self.PeersQueryType.PEERS_SCHEMA else self._SELECT_PEERS_NO_TOKENS_TEMPLATE) - original_endpoint_host = self._cluster.metadata.get_host(connection.original_endpoint) - host_release_version = None if original_endpoint_host is None else original_endpoint_host.release_version - host_dse_version = None if original_endpoint_host is None else original_endpoint_host.dse_version + connection_host = self._get_host_for_connection( + connection) + host_release_version = None if connection_host is None else connection_host.release_version + host_dse_version = None if connection_host is None else connection_host.dse_version uses_native_address_query = ( host_dse_version and Version(host_dse_version) >= self._MINIMUM_NATIVE_ADDRESS_DSE_VERSION) @@ -4527,13 +4584,45 @@ def _signal_error(self): # try just signaling the cluster, as this will trigger a reconnect # as part of marking the host down if self._connection and self._connection.is_defunct: - host = self._cluster.metadata.get_host(self._connection.endpoint) + connection = self._connection + host = self._get_host_for_connection(connection) # host may be None if it's already been removed, but that indicates # that errors have already been reported, so we're fine if host: - self._cluster.signal_connection_failure( - host, self._connection.last_error, is_host_addition=False) - return + original_endpoint = getattr( + connection, 'original_endpoint', None) + unix_backed = ( + isinstance(host.endpoint, UnixSocketEndPoint) or + isinstance(connection.endpoint, UnixSocketEndPoint) or + isinstance(original_endpoint, UnixSocketEndPoint)) + route_mismatch = connection.endpoint != host.endpoint + # Keep ordinary endpoint-equal TCP connections on the + # legacy signal-only path. General suppressed-DOWN recovery + # and its reconnection cadence are outside this change. + if not unix_backed and not route_mismatch: + self._cluster.signal_connection_failure( + host, connection.last_error, + is_host_addition=False) + return + + # A newly resolvable Unix Host or alternate connection + # route still needs the direct reconnect fallback when + # host-state handling suppresses its DOWN notification. A + # fresh DOWN transition guarantees that on_down() will + # enqueue the reconnect instead. + with host.lock: + host_was_up = host.is_up is True + host_was_reconnecting = ( + host.is_currently_reconnecting()) + self._cluster.signal_connection_failure( + host, connection.last_error, + is_host_addition=False) + down_notification_queued = ( + host_was_up and not host_was_reconnecting and + host.is_up is False) + + if down_notification_queued: + return # if the connection is not defunct or the host already left, reconnect # manually @@ -4545,7 +4634,7 @@ def on_up(self, host): def on_down(self, host): conn = self._connection - if conn and conn.endpoint == host.endpoint and \ + if self._connection_matches_host(conn, host) and \ self._reconnection_handler is None: log.debug("[control connection] Control connection host (%s) is " "considered down, starting reconnection", host) @@ -4558,7 +4647,7 @@ def on_add(self, host, refresh_nodes=True): def on_remove(self, host): c = self._connection - if c and c.endpoint == host.endpoint: + if self._connection_matches_host(c, host): log.debug("[control connection] Control connection host (%s) is being removed. Reconnecting", host) # refresh will be done on reconnect self.reconnect() diff --git a/cassandra/connection.py b/cassandra/connection.py index b4ea59b23c..d0a75818b2 100644 --- a/cassandra/connection.py +++ b/cassandra/connection.py @@ -899,6 +899,8 @@ def orphaned_threshold_for(max_in_flight): is_unsupported_proto_version = False is_control_connection = False + # Stable identity learned from system.local for control connections. + _control_connection_host_id = None signaled_error = False # used for flagging at the pool level allow_beta_protocol_version = False diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 0cb17e1337..25d1ceb7d5 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -140,7 +140,10 @@ def export_schema_as_string(self): def refresh(self, connection, timeout, target_type=None, change_type=None, fetch_size=None, metadata_request_timeout=None, **kwargs): - host = self.get_host(connection.original_endpoint) + host_id = getattr(connection, '_control_connection_host_id', None) + host = self.get_host_by_host_id(host_id) if host_id is not None else None + if host is None: + host = self.get_host(connection.original_endpoint) server_version = host.release_version if host else None dse_version = host.dse_version if host else None parser = get_schema_parser(connection, server_version, dse_version, timeout, metadata_request_timeout, fetch_size) diff --git a/cassandra/pool.py b/cassandra/pool.py index 1d90e3233f..2cd376d293 100644 --- a/cassandra/pool.py +++ b/cassandra/pool.py @@ -29,7 +29,8 @@ from cassandra.util import WeakSet # NOQA from cassandra import AuthenticationFailed -from cassandra.connection import ConnectionException, EndPoint, DefaultEndPoint +from cassandra.connection import (ConnectionException, EndPoint, + DefaultEndPoint, UnixSocketEndPoint) from cassandra.policies import HostDistance log = logging.getLogger(__name__) @@ -241,6 +242,12 @@ def __hash__(self): return hash(self.endpoint) def __lt__(self, other): + self_is_unix = isinstance(self.endpoint, UnixSocketEndPoint) + other_is_unix = isinstance(other.endpoint, UnixSocketEndPoint) + if self_is_unix != other_is_unix: + # Endpoint comparators assume same-kind operands, so partition + # Unix and network Hosts before delegating their ordering. + return self_is_unix return self.endpoint < other.endpoint def __str__(self): @@ -694,7 +701,11 @@ def _get_shard_aware_endpoint(self): shard_aware_port_ssl; if it is absent, return None so the pool opens a regular SSL connection instead of falling back to the plaintext port. Explicit ssl_options={}, like ssl_context, marks the cluster SSL-enabled. + Unix sockets bypass advertised TCP ports and source-port shard targeting. """ + if isinstance(self.host.endpoint, UnixSocketEndPoint): + return None + if (self.advanced_shardaware_block_until and self.advanced_shardaware_block_until > time.time()) or \ self._session.cluster.shard_aware_options.disable_shardaware_port: return None diff --git a/tests/unit/test_control_connection.py b/tests/unit/test_control_connection.py index fd62323f33..dec61dacdc 100644 --- a/tests/unit/test_control_connection.py +++ b/tests/unit/test_control_connection.py @@ -19,9 +19,12 @@ from cassandra import OperationTimedOut, SchemaTargetType, SchemaChangeType from cassandra.protocol import ResultMessage, RESULT_KIND_ROWS -from cassandra.cluster import ControlConnection, _Scheduler, ProfileManager, EXEC_PROFILE_DEFAULT, ExecutionProfile +from cassandra.cluster import (Cluster, ControlConnection, _Scheduler, + ProfileManager, EXEC_PROFILE_DEFAULT, + ExecutionProfile) from cassandra.pool import Host -from cassandra.connection import EndPoint, DefaultEndPoint, DefaultEndPointFactory +from cassandra.connection import (ConnectionException, EndPoint, DefaultEndPoint, + DefaultEndPointFactory, UnixSocketEndPoint) from cassandra.policies import (SimpleConvictionPolicy, RoundRobinPolicy, ConstantReconnectionPolicy, IdentityTranslator) @@ -80,8 +83,8 @@ def add_or_return_host(self, host): def update_host(self, host, old_endpoint): host, created = self.add_or_return_host(host) - self._host_id_by_endpoint[host.endpoint] = host.host_id self._host_id_by_endpoint.pop(old_endpoint, False) + self._host_id_by_endpoint[host.endpoint] = host.host_id def all_hosts_items(self): return list(self.hosts.items()) @@ -205,6 +208,27 @@ def setUp(self): self.control_connection = ControlConnection(self.cluster, 1, 0, 0, 0) self.control_connection._connection = self.connection self.control_connection._time = self.time + self.cluster.control_connection = self.control_connection + + def _forget_local_host(self): + endpoint = DefaultEndPoint('192.168.1.0') + self.cluster.metadata._host_id_by_endpoint.pop(endpoint) + self.cluster.metadata.hosts.pop('uuid1') + + def _discover_local_host_over_unix(self): + maintenance_endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + self._forget_local_host() + self.connection.endpoint = maintenance_endpoint + self.connection.original_endpoint = maintenance_endpoint + self.control_connection.refresh_node_list_and_token_map() + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + local_host.set_up() + return maintenance_endpoint, local_host + + def _refresh_control_connection_over_network(self): + self.connection.endpoint = DefaultEndPoint('192.168.1.0') + self.connection.original_endpoint = self.connection.endpoint + self.control_connection.refresh_node_list_and_token_map() def test_wait_for_schema_agreement(self): """ @@ -330,6 +354,280 @@ def test_refresh_nodes_and_tokens(self): assert self.connection.wait_for_responses.call_count == 1 + def test_refresh_uses_control_endpoint_for_local_unix_host(self): + maintenance_endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + self._forget_local_host() + self.connection.endpoint = maintenance_endpoint + self.connection.original_endpoint = maintenance_endpoint + + self.control_connection.refresh_node_list_and_token_map() + + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + assert local_host.endpoint == maintenance_endpoint + assert local_host.broadcast_rpc_address == '192.168.1.0' + peer_host = self.cluster.metadata.get_host_by_host_id('uuid2') + assert peer_host.endpoint == DefaultEndPoint('192.168.1.1') + assert sorted([local_host, peer_host]) == \ + sorted([peer_host, local_host]) + + def test_refresh_checks_unix_local_advertised_endpoint_for_duplicates(self): + self._forget_local_host() + self.connection.endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + self.connection.original_endpoint = \ + UnixSocketEndPoint('/tmp/maintenance.sock') + self.connection.peer_results[1].append([ + '192.168.1.0', '10.0.0.4', 'a', 'dc1', 'rack1', + ['4', '104', '204'], 'uuid4']) + + self.control_connection.refresh_node_list_and_token_map() + + assert self.cluster.metadata.get_host_by_host_id('uuid4') is None + + def test_refresh_preserves_known_unix_endpoint_when_host_becomes_peer(self): + maintenance_endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + self._forget_local_host() + self.connection.endpoint = maintenance_endpoint + self.connection.original_endpoint = maintenance_endpoint + self.control_connection.refresh_node_list_and_token_map() + + local_results = ( + self.connection.local_results[0], + [['192.168.1.1', 'a', 'foocluster', 'dc1', 'rack1', + 'Murmur3Partitioner', '2.2.0', ['1', '101', '201'], + 'uuid2']]) + peer_results = ( + self.connection.peer_results[0], + [['192.168.1.0', '10.0.0.1', 'a', 'dc1', 'rack1', + ['0', '100', '200'], 'uuid1'], + ['192.168.1.2', '10.0.0.2', 'a', 'dc1', 'rack1', + ['2', '102', '202'], 'uuid3']]) + self.connection.endpoint = DefaultEndPoint('192.168.1.1') + self.connection.original_endpoint = self.connection.endpoint + + self.control_connection._refresh_node_list_and_token_map( + self.connection, + preloaded_results=_node_meta_results(local_results, peer_results)) + + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + assert local_host.endpoint == maintenance_endpoint + + peer_results[1][0][2] = 'b' + peers_response, local_response = _node_meta_results( + local_results, peer_results) + mismatches = self.control_connection._get_schema_mismatches( + peers_response, local_response, self.connection.endpoint) + assert maintenance_endpoint in mismatches['b'] + + def test_refresh_uses_factory_for_local_network_host(self): + self.connection.original_endpoint = DefaultEndPoint('proxy', 9999) + + self.control_connection.refresh_node_list_and_token_map() + + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + assert local_host.endpoint == DefaultEndPoint('192.168.1.0') + + def test_schema_query_uses_shard_aware_connection_original_endpoint(self): + host = self.cluster.metadata.get_host_by_host_id('uuid1') + self.connection.endpoint = DefaultEndPoint('192.168.1.0', 19042) + self.connection.original_endpoint = host.endpoint + self.control_connection._uses_peers_v2 = False + + query = self.control_connection._get_peers_query( + self.control_connection.PeersQueryType.PEERS_SCHEMA, + self.connection) + + assert query == self.control_connection._SELECT_SCHEMA_PEERS_TEMPLATE \ + .format(nt_col_name='rpc_address') + + def test_refresh_network_local_preserves_known_unix_endpoint(self): + maintenance_endpoint, local_host = \ + self._discover_local_host_over_unix() + host_index = {local_host: object()} + + self._refresh_control_connection_over_network() + + assert self.cluster.metadata.get_host_by_host_id('uuid1') is local_host + assert local_host.endpoint == maintenance_endpoint + assert host_index[local_host] is not None + assert Cluster.get_control_connection_host(self.cluster) is local_host + + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + # Model a conviction whose DOWN transition is discounted because a + # usable session pool remains: no control on_down callback is queued. + self.cluster.signal_connection_failure = Mock(return_value=True) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_unix_signal_error_reconnects_if_down_notification_suppressed(self): + _, local_host = self._discover_local_host_over_unix() + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + self.cluster.signal_connection_failure = Mock(return_value=True) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_tcp_route_mismatch_reconnects_if_down_notification_suppressed(self): + self.control_connection.refresh_node_list_and_token_map() + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + self.connection.endpoint = DefaultEndPoint('192.168.1.0', 19042) + self.connection.original_endpoint = local_host.endpoint + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + self.cluster.signal_connection_failure = Mock(return_value=True) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_route_mismatch_signal_error_waits_for_queued_down_reconnect(self): + _, local_host = self._discover_local_host_over_unix() + self._refresh_control_connection_over_network() + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + down_notifications = [] + + def transition_host_down(host, *_args, **_kwargs): + host.set_down() + down_notifications.append(host) + return True + + self.cluster.signal_connection_failure = Mock( + side_effect=transition_host_down) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_not_called() + + self.control_connection.on_down(down_notifications.pop()) + + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_route_mismatch_signal_error_reconnects_if_host_already_down(self): + _, local_host = self._discover_local_host_over_unix() + self._refresh_control_connection_over_network() + local_host.set_down() + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + self.cluster.signal_connection_failure = Mock(return_value=True) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_route_mismatch_signal_error_reconnects_if_host_reconnecting(self): + _, local_host = self._discover_local_host_over_unix() + self._refresh_control_connection_over_network() + local_host.get_and_set_reconnection_handler(Mock()) + connection_error = ConnectionException('control connection failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + + def transition_without_notification(host, *_args, **_kwargs): + host.set_down() + return True + + self.cluster.signal_connection_failure = Mock( + side_effect=transition_without_notification) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + local_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_remove_matches_control_connection_by_host_id(self): + maintenance_endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + self._forget_local_host() + self.connection.endpoint = maintenance_endpoint + self.connection.original_endpoint = maintenance_endpoint + self.control_connection.refresh_node_list_and_token_map() + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + + self.connection.endpoint = DefaultEndPoint('192.168.1.0') + self.cluster.metadata.hosts.pop('uuid1') + self.cluster.metadata._host_id_by_endpoint.pop(maintenance_endpoint) + self.cluster.executor.reset_mock() + + self.control_connection.on_remove(local_host) + + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_down_matches_replacement_at_stale_control_endpoint(self): + self.control_connection.refresh_node_list_and_token_map() + old_host = self.cluster.metadata.get_host_by_host_id('uuid1') + endpoint = old_host.endpoint + self.cluster.metadata.hosts.pop('uuid1') + + replacement_host = Host( + endpoint, SimpleConvictionPolicy, host_id='replacement-id') + replacement_host.set_up() + self.cluster.metadata.hosts['replacement-id'] = replacement_host + self.cluster.metadata._host_id_by_endpoint[endpoint] = \ + 'replacement-id' + + connection_error = ConnectionException('old control failed') + self.connection.is_defunct = True + self.connection.last_error = connection_error + self.cluster.signal_connection_failure = Mock(return_value=True) + self.cluster.executor.reset_mock() + + self.control_connection._signal_error() + + self.cluster.signal_connection_failure.assert_called_once_with( + replacement_host, connection_error, is_host_addition=False) + self.cluster.executor.submit.assert_not_called() + + self.control_connection.on_down(replacement_host) + + self.cluster.executor.submit.assert_called_once_with( + self.control_connection._reconnect) + + def test_refresh_unix_local_preserves_known_network_endpoint(self): + maintenance_endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + local_host = self.cluster.metadata.get_host_by_host_id('uuid1') + host_index = {local_host: object()} + self.connection.endpoint = maintenance_endpoint + self.connection.original_endpoint = maintenance_endpoint + + self.control_connection.refresh_node_list_and_token_map() + + assert self.cluster.metadata.get_host_by_host_id('uuid1') is local_host + assert local_host.endpoint == DefaultEndPoint('192.168.1.0') + assert host_index[local_host] is not None + def test_refresh_nodes_and_tokens_with_invalid_peers(self): def refresh_and_validate_added_hosts(): self.connection.wait_for_responses = Mock(return_value=_node_meta_results( diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 2a1fced6cf..ced388414f 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -15,11 +15,12 @@ from binascii import unhexlify import logging -from unittest.mock import Mock +from unittest.mock import Mock, patch import os import uuid import cassandra +from cassandra.connection import DefaultEndPoint, UnixSocketEndPoint from cassandra.cqltypes import strip_frozen from cassandra.marshal import uint16_unpack, uint16_pack from cassandra.metadata import (Murmur3Token, MD5Token, @@ -829,6 +830,30 @@ def test_build_index_as_cql(self): class SchemaParserLookupTests(unittest.TestCase): + def test_refresh_uses_control_connection_host_id_for_versions(self): + metadata = Metadata() + host_id = uuid.uuid4() + host = Host( + UnixSocketEndPoint('/tmp/maintenance.sock'), + SimpleConvictionPolicy, + host_id=host_id) + host.release_version = '3.11.0' + metadata.add_or_return_host(host) + + connection = Mock() + connection.endpoint = DefaultEndPoint('192.168.1.0') + connection.original_endpoint = connection.endpoint + connection._control_connection_host_id = host_id + parser = Mock() + parser.get_all_keyspaces.return_value = () + + with patch('cassandra.metadata.get_schema_parser', + return_value=parser) as get_parser: + metadata.refresh(connection, 0.1) + + get_parser.assert_called_once_with( + connection, '3.11.0', None, 0.1, None, None) + def test_reads_versions_from_system_local_when_missing(self): connection = Mock() diff --git a/tests/unit/test_shard_aware.py b/tests/unit/test_shard_aware.py index af27a84011..5c0b06c25d 100644 --- a/tests/unit/test_shard_aware.py +++ b/tests/unit/test_shard_aware.py @@ -21,7 +21,8 @@ from cassandra.cluster import ShardAwareOptions from cassandra.pool import HostConnection, HostDistance -from cassandra.connection import ShardingInfo, DefaultEndPoint +from cassandra.connection import (ShardingInfo, DefaultEndPoint, + UnixSocketEndPoint) from cassandra.metadata import Murmur3Token from cassandra.protocol_features import ProtocolFeatures from cassandra.shard_info import _ShardingInfo @@ -167,6 +168,40 @@ def test_advanced_shard_aware_port(self): finally: session.cluster.executor.shutdown(wait=True) + def test_unix_socket_bypasses_advanced_shard_aware_port(self): + endpoint = UnixSocketEndPoint('/tmp/maintenance.sock') + host = MagicMock() + host.endpoint = endpoint + session = MockSession() + pending = [] + + def submit(fn, *args, **kwargs): + pending.append((fn, args, kwargs)) + + session.submit = submit + connection_factory = MagicMock( + side_effect=session.mock_connection_factory) + session.cluster.connection_factory = connection_factory + + try: + pool = HostConnection( + host=host, host_distance=HostDistance.REMOTE, + session=session) + while pending: + fn, args, kwargs = pending.pop(0) + fn(*args, **kwargs) + + assert pool._get_shard_aware_endpoint() is None + assert set(pool._connections) == {0, 1, 2, 3} + assert connection_factory.call_count == 4 + for factory_call in connection_factory.call_args_list: + args, kwargs = factory_call + assert args[0] is endpoint + assert 'shard_id' not in kwargs + assert 'total_shards' not in kwargs + finally: + session.cluster.executor.shutdown(wait=True) + def test_ssl_advanced_shard_aware_port_requires_ssl_port(self): """ Test that SSL connections do not fall back to the plaintext