|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 | import unittest |
| 15 | +from concurrent.futures import Future |
15 | 16 |
|
16 | 17 | import logging |
17 | 18 | import socket |
|
23 | 24 | InvalidRequest, Unauthorized, AuthenticationFailed, OperationTimedOut, UnsupportedOperation, RequestValidationException, ConfigurationException, ProtocolVersion |
24 | 25 | from cassandra.cluster import _Scheduler, Session, Cluster, default_lbp_factory, \ |
25 | 26 | ExecutionProfile, _ConfigMode, EXEC_PROFILE_DEFAULT |
| 27 | +from cassandra.connection import ConnectionBusy |
26 | 28 | from cassandra.pool import Host |
27 | 29 | from cassandra.policies import HostDistance, RetryPolicy, RoundRobinPolicy, DowngradingConsistencyRetryPolicy, SimpleConvictionPolicy |
28 | 30 | from cassandra.query import SimpleStatement, named_tuple_factory, tuple_factory |
@@ -247,11 +249,65 @@ def test_event_delay_timing(self, *_): |
247 | 249 |
|
248 | 250 |
|
249 | 251 | class SessionTest(unittest.TestCase): |
| 252 | + class FakeTime(object): |
| 253 | + |
| 254 | + def __init__(self): |
| 255 | + self.clock = 0 |
| 256 | + |
| 257 | + def time(self): |
| 258 | + return self.clock |
| 259 | + |
| 260 | + def sleep(self, amount): |
| 261 | + self.clock += amount |
| 262 | + |
| 263 | + class MockPool(object): |
| 264 | + |
| 265 | + def __init__(self, host, connection): |
| 266 | + self.host = host |
| 267 | + self.host_distance = HostDistance.LOCAL |
| 268 | + self.is_shutdown = False |
| 269 | + self.connection = connection |
| 270 | + |
| 271 | + def _get_connection_for_routing_key(self): |
| 272 | + return self.connection |
| 273 | + |
250 | 274 | def setUp(self): |
251 | 275 | if connection_class is None: |
252 | 276 | raise unittest.SkipTest('libev does not appear to be installed correctly') |
253 | 277 | connection_class.initialize_reactor() |
254 | 278 |
|
| 279 | + def _mock_schema_response(self, schema_version): |
| 280 | + response = Mock() |
| 281 | + response.column_names = ["schema_version"] |
| 282 | + response.parsed_rows = [[schema_version]] |
| 283 | + return response |
| 284 | + |
| 285 | + def _new_schema_agreement_session(self, schema_versions, distances=None): |
| 286 | + hosts = [] |
| 287 | + distance_map = {} |
| 288 | + if distances is None: |
| 289 | + distances = [HostDistance.LOCAL] * len(schema_versions) |
| 290 | + |
| 291 | + for index, schema_version in enumerate(schema_versions): |
| 292 | + host = Host("127.0.0.%d" % (index + 1), SimpleConvictionPolicy, host_id=uuid.uuid4()) |
| 293 | + host.set_up() |
| 294 | + hosts.append(host) |
| 295 | + distance_map[host] = distances[index] |
| 296 | + |
| 297 | + cluster = Cluster(protocol_version=4) |
| 298 | + for host in hosts: |
| 299 | + cluster.metadata.add_or_return_host(host) |
| 300 | + |
| 301 | + session = Session(cluster, hosts) |
| 302 | + session._profile_manager.distance = Mock(side_effect=lambda host: distance_map.get(host, HostDistance.LOCAL)) |
| 303 | + session._pools = {} |
| 304 | + for host, schema_version in zip(hosts, schema_versions): |
| 305 | + connection = Mock(endpoint=host.endpoint) |
| 306 | + connection.wait_for_response.return_value = self._mock_schema_response(schema_version) |
| 307 | + session._pools[host] = self.MockPool(host, connection) |
| 308 | + |
| 309 | + return session, hosts |
| 310 | + |
255 | 311 | # TODO: this suite could be expanded; for now just adding a test covering a PR |
256 | 312 | @mock_session_pools |
257 | 313 | def test_default_serial_consistency_level_ep(self, *_): |
@@ -339,6 +395,106 @@ def test_set_keyspace_escapes_quotes(self, *_): |
339 | 395 | assert query == 'USE simple_ks', ( |
340 | 396 | "Simple keyspace names should not be quoted, got: %r" % query) |
341 | 397 |
|
| 398 | + @mock_session_pools |
| 399 | + def test_wait_for_schema_agreement_queries_all_local_hosts(self, *_): |
| 400 | + session, hosts = self._new_schema_agreement_session(["a", "a"]) |
| 401 | + |
| 402 | + assert session.wait_for_schema_agreement(wait_time=1) |
| 403 | + |
| 404 | + for host in hosts: |
| 405 | + connection = session._pools[host].connection |
| 406 | + connection.wait_for_response.assert_called_once() |
| 407 | + |
| 408 | + @mock_session_pools |
| 409 | + def test_wait_for_schema_agreement_retries_until_local_hosts_match(self, *_): |
| 410 | + session, hosts = self._new_schema_agreement_session(["a", "b"]) |
| 411 | + clock = self.FakeTime() |
| 412 | + second_connection = session._pools[hosts[1]].connection |
| 413 | + second_connection.wait_for_response.side_effect = [ |
| 414 | + self._mock_schema_response("b"), |
| 415 | + self._mock_schema_response("a")] |
| 416 | + |
| 417 | + with patch('cassandra.cluster.time', new=clock): |
| 418 | + assert session.wait_for_schema_agreement(wait_time=1) |
| 419 | + assert second_connection.wait_for_response.call_count == 2 |
| 420 | + assert clock.clock == 0.2 |
| 421 | + |
| 422 | + @mock_session_pools |
| 423 | + def test_wait_for_schema_agreement_retries_when_local_connection_is_busy(self, *_): |
| 424 | + session, hosts = self._new_schema_agreement_session(["a", "a"]) |
| 425 | + clock = self.FakeTime() |
| 426 | + busy_connection = session._pools[hosts[1]].connection |
| 427 | + busy_connection.wait_for_response.side_effect = [ |
| 428 | + ConnectionBusy("connection overloaded"), |
| 429 | + self._mock_schema_response("a")] |
| 430 | + |
| 431 | + with patch('cassandra.cluster.time', new=clock): |
| 432 | + assert session.wait_for_schema_agreement(wait_time=1) |
| 433 | + assert busy_connection.wait_for_response.call_count == 2 |
| 434 | + assert clock.clock == 0.2 |
| 435 | + |
| 436 | + @mock_session_pools |
| 437 | + def test_wait_for_schema_agreement_ignores_local_hosts_without_session_pool(self, *_): |
| 438 | + session, hosts = self._new_schema_agreement_session(["a"]) |
| 439 | + |
| 440 | + unconnected_host = Host("127.0.0.2", SimpleConvictionPolicy, host_id=uuid.uuid4()) |
| 441 | + unconnected_host.set_up() |
| 442 | + session.cluster.metadata.add_or_return_host(unconnected_host) |
| 443 | + |
| 444 | + assert session.wait_for_schema_agreement(wait_time=1) |
| 445 | + session._pools[hosts[0]].connection.wait_for_response.assert_called() |
| 446 | + |
| 447 | + @mock_session_pools |
| 448 | + @patch('cassandra.cluster.wait_futures') |
| 449 | + def test_wait_for_schema_agreement_limits_parallel_queries_to_default(self, mocked_wait_futures, *_): |
| 450 | + session, _ = self._new_schema_agreement_session(["a"] * 11) |
| 451 | + batch_sizes = [] |
| 452 | + |
| 453 | + def submit(fn, host, query, deadline): |
| 454 | + future = Future() |
| 455 | + future.set_result(fn(host, query, deadline)) |
| 456 | + return future |
| 457 | + |
| 458 | + def fake_wait(futures, timeout=None, return_when=None): |
| 459 | + batch_sizes.append(len(futures)) |
| 460 | + return set(futures), set() |
| 461 | + |
| 462 | + session.submit = Mock(side_effect=submit) |
| 463 | + mocked_wait_futures.side_effect = fake_wait |
| 464 | + |
| 465 | + assert session.wait_for_schema_agreement(wait_time=1) |
| 466 | + assert batch_sizes == [10, 1] |
| 467 | + |
| 468 | + @mock_session_pools |
| 469 | + def test_wait_for_schema_agreement_rack_scope_only_queries_local_rack_connections(self, *_): |
| 470 | + session, hosts = self._new_schema_agreement_session( |
| 471 | + ["a", "a", "a"], |
| 472 | + distances=[HostDistance.LOCAL_RACK, HostDistance.LOCAL, HostDistance.REMOTE]) |
| 473 | + |
| 474 | + assert session.wait_for_schema_agreement(wait_time=1, scope='rack') |
| 475 | + |
| 476 | + session._pools[hosts[0]].connection.wait_for_response.assert_called_once() |
| 477 | + session._pools[hosts[1]].connection.wait_for_response.assert_not_called() |
| 478 | + session._pools[hosts[2]].connection.wait_for_response.assert_not_called() |
| 479 | + |
| 480 | + @mock_session_pools |
| 481 | + def test_wait_for_schema_agreement_cluster_scope_queries_all_connected_hosts(self, *_): |
| 482 | + session, hosts = self._new_schema_agreement_session( |
| 483 | + ["a", "a", "a"], |
| 484 | + distances=[HostDistance.LOCAL_RACK, HostDistance.LOCAL, HostDistance.REMOTE]) |
| 485 | + |
| 486 | + assert session.wait_for_schema_agreement(wait_time=1, scope='cluster') |
| 487 | + |
| 488 | + for host in hosts: |
| 489 | + session._pools[host].connection.wait_for_response.assert_called_once() |
| 490 | + |
| 491 | + @mock_session_pools |
| 492 | + def test_wait_for_schema_agreement_rejects_unknown_scope(self, *_): |
| 493 | + session, _ = self._new_schema_agreement_session(["a"]) |
| 494 | + |
| 495 | + with pytest.raises(ValueError): |
| 496 | + session.wait_for_schema_agreement(wait_time=1, scope='planet') |
| 497 | + |
342 | 498 | class ProtocolVersionTests(unittest.TestCase): |
343 | 499 |
|
344 | 500 | def test_protocol_downgrade_test(self): |
|
0 commit comments