diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 068be2e048..e50efcc659 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,12 @@ Features Others ------ +* Token-aware routing now caches the "no keyspace metadata" result per keyspace + instead of retrying the token map lookup on every query. This mainly benefits + ``schema_metadata_enabled=False``: the driver never fetches replication + strategies while schema metadata stays disabled, and cleanly falls back to + the child load-balancing policy (e.g. round robin) for that keyspace, as + documented for that setting. * The ``STARTUP`` options that describe the driver itself are no longer the application's to set. An ``ApplicationInfoBase.add_startup_options`` that sets ``DRIVER_NAME``, ``DRIVER_VERSION``, ``SESSION_ID`` or ``DRIVER_CONFIG`` now has that diff --git a/cassandra/metadata.py b/cassandra/metadata.py index 0cb17e1337..e175db95de 100644 --- a/cassandra/metadata.py +++ b/cassandra/metadata.py @@ -1852,6 +1852,12 @@ def rebuild_keyspace(self, keyspace, build_if_absent=False): if ks_meta: replica_map = self.replica_map_for_keyspace(self._metadata.keyspaces[keyspace]) self.tokens_to_hosts_by_ks[keyspace] = replica_map + elif build_if_absent: + # No keyspace metadata (e.g. schema_metadata_enabled=False): + # cache the empty result so token-aware routing cleanly + # falls back to the child load-balancing policy instead + # of retrying this lookup on every query. + self.tokens_to_hosts_by_ks[keyspace] = {} except Exception: # should not happen normally, but we don't want to blow up queries because of unexpected meta state # bypass until new map is generated diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index 2a1fced6cf..84cf7a97a0 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -447,6 +447,45 @@ def test_bytes_tokens(self): self._get_replicas(BytesToken) +class TokenMapNoKeyspaceMetadataTest(unittest.TestCase): + """ + Token-aware routing must fall back cleanly (no crash, empty replica list) + for a keyspace whose metadata is unavailable, e.g. because + ``schema_metadata_enabled=False``. + """ + + def _token_map(self): + token_klass = Murmur3Token + tokens = [token_klass(i) for i in range(0, (2 ** 127 - 1), 2 ** 125)] + hosts = [Host("ip%d" % i, SimpleConvictionPolicy, datacenter="dc1", rack="rack1", host_id=uuid.uuid4()) + for i in range(len(tokens))] + token_to_primary_replica = dict(zip(tokens, hosts)) + metadata = Mock(spec=Metadata, keyspaces={}) + return TokenMap(token_klass, token_to_primary_replica, tokens, metadata), tokens[0] + + def test_get_replicas_is_empty_without_keyspace_metadata(self): + token_map, token = self._token_map() + + assert token_map.get_replicas("ks", token) == [] + + def test_missing_keyspace_result_is_cached(self): + """rebuild_keyspace should not repeatedly try to build a map that + will never succeed without keyspace metadata.""" + token_map, token = self._token_map() + + token_map.get_replicas("ks", token) + assert token_map.tokens_to_hosts_by_ks.get("ks") == {} + + # a later call with build_if_absent=False (as done by schema-change + # handlers) should re-check for the keyspace, self-healing once + # metadata becomes available again + keyspace = KeyspaceMetadata("ks", True, "NetworkTopologyStrategy", {"dc1": "1"}) + token_map._metadata.keyspaces = {"ks": keyspace} + token_map.rebuild_keyspace("ks", build_if_absent=False) + + assert token_map.tokens_to_hosts_by_ks.get("ks") + + class DropTableMetadataTest(unittest.TestCase): """Metadata._drop_table should invalidate tablets for the dropped table.""" diff --git a/tests/unit/test_policies.py b/tests/unit/test_policies.py index 35c1a96f87..932c3dd112 100644 --- a/tests/unit/test_policies.py +++ b/tests/unit/test_policies.py @@ -605,6 +605,30 @@ def get_replicas(keyspace, packed_key): assert sorted(set(qplan)) == sorted(set(hosts)) + def test_falls_back_to_child_policy_without_keyspace_metadata(self): + """ + With schema_metadata_enabled=False the driver has no replica + information, so Metadata.get_replicas() returns []. TokenAwarePolicy + must fall back to the child policy's plan rather than raise or + return an incomplete/wrong plan. + """ + cluster = Mock(spec=Cluster) + cluster.metadata = Mock(spec=Metadata) + cluster.metadata._tablets = Mock(spec=Tablets) + cluster.metadata._tablets.get_tablet_for_key.return_value = None + cluster.metadata.get_replicas.return_value = [] + hosts = [Host(DefaultEndPoint(str(i)), SimpleConvictionPolicy, host_id=uuid.uuid4()) for i in range(4)] + for host in hosts: + host.set_up() + + policy = TokenAwarePolicy(RoundRobinPolicy()) + policy.populate(cluster, hosts) + + query = Statement(routing_key=struct.pack('>i', 0), keyspace='keyspace_name') + qplan = list(policy.make_query_plan(None, query)) + + assert sorted(qplan) == sorted(hosts) + def test_wrap_dc_aware(self): cluster = Mock(spec=Cluster) cluster.metadata = Mock(spec=Metadata)