Exponential trace fetch backoff, now configurable, test
This commit is contained in:
		| @@ -911,6 +911,17 @@ class Session(object): | |||||||
|     timeout, neither the registered callback or errback will be called. |     timeout, neither the registered callback or errback will be called. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     max_trace_wait = 2.0 | ||||||
|  |     """ | ||||||
|  |     The maximum amount of time (in seconds) the driver will wait for trace | ||||||
|  |     details to be populated server-side for a query before giving up. | ||||||
|  |     If the `trace` parameter for :meth:`~.execute()` or :meth:`~.execute_async()` | ||||||
|  |     is :const:`True`, the driver will repeatedly attempt to fetch trace | ||||||
|  |     details for the query (using exponential backoff) until this limit is | ||||||
|  |     hit.  If the limit is passed, :exc:`cassandra.query.TraceUnavailable` | ||||||
|  |     will be raised. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     _lock = None |     _lock = None | ||||||
|     _pools = None |     _pools = None | ||||||
|     _load_balancer = None |     _load_balancer = None | ||||||
| @@ -977,7 +988,7 @@ class Session(object): | |||||||
|         finally: |         finally: | ||||||
|             if trace: |             if trace: | ||||||
|                 try: |                 try: | ||||||
|                     query.trace = future.get_query_trace() |                     query.trace = future.get_query_trace(self.max_trace_wait) | ||||||
|                 except Exception: |                 except Exception: | ||||||
|                     log.exception("Unable to fetch query trace:") |                     log.exception("Unable to fetch query trace:") | ||||||
|  |  | ||||||
| @@ -2162,17 +2173,19 @@ class ResponseFuture(object): | |||||||
|             else: |             else: | ||||||
|                 raise OperationTimedOut() |                 raise OperationTimedOut() | ||||||
|  |  | ||||||
|     def get_query_trace(self): |     def get_query_trace(self, max_wait=None): | ||||||
|         """ |         """ | ||||||
|         Returns the :class:`~.query.QueryTrace` instance representing a trace |         Returns the :class:`~.query.QueryTrace` instance representing a trace | ||||||
|         of the last attempt for this operation, or :const:`None` if tracing was |         of the last attempt for this operation, or :const:`None` if tracing was | ||||||
|         not enabled for this query.  Note that this may raise an exception if |         not enabled for this query.  Note that this may raise an exception if | ||||||
|         there are problems retrieving the trace details from Cassandra. |         there are problems retrieving the trace details from Cassandra. If the | ||||||
|  |         trace is not available after `max_wait` seconds, | ||||||
|  |         :exc:`cassandra.query.TraceUnavailable` will be raised. | ||||||
|         """ |         """ | ||||||
|         if not self._query_trace: |         if not self._query_trace: | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|         self._query_trace.populate() |         self._query_trace.populate(max_wait) | ||||||
|         return self._query_trace |         return self._query_trace | ||||||
|  |  | ||||||
|     def add_callback(self, fn, *args, **kwargs): |     def add_callback(self, fn, *args, **kwargs): | ||||||
|   | |||||||
| @@ -13,6 +13,9 @@ from cassandra.cqltypes import unix_time_from_uuid1 | |||||||
| from cassandra.decoder import (cql_encoders, cql_encode_object, | from cassandra.decoder import (cql_encoders, cql_encode_object, | ||||||
|                                cql_encode_sequence) |                                cql_encode_sequence) | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | log = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Statement(object): | class Statement(object): | ||||||
|     """ |     """ | ||||||
| @@ -386,27 +389,29 @@ class QueryTrace(object): | |||||||
|     _SELECT_SESSIONS_FORMAT = "SELECT * FROM system_traces.sessions WHERE session_id = %s" |     _SELECT_SESSIONS_FORMAT = "SELECT * FROM system_traces.sessions WHERE session_id = %s" | ||||||
|     _SELECT_EVENTS_FORMAT = "SELECT * FROM system_traces.events WHERE session_id = %s" |     _SELECT_EVENTS_FORMAT = "SELECT * FROM system_traces.events WHERE session_id = %s" | ||||||
|     _BASE_RETRY_SLEEP = 0.003 |     _BASE_RETRY_SLEEP = 0.003 | ||||||
|     _MAX_ATTEMPTS = 5 |  | ||||||
|  |  | ||||||
|     def __init__(self, trace_id, session): |     def __init__(self, trace_id, session): | ||||||
|         self.trace_id = trace_id |         self.trace_id = trace_id | ||||||
|         self._session = session |         self._session = session | ||||||
|  |  | ||||||
|     def populate(self): |     def populate(self, max_wait=2.0): | ||||||
|         """ |         """ | ||||||
|         Retrieves the actual tracing details from Cassandra and populates the |         Retrieves the actual tracing details from Cassandra and populates the | ||||||
|         attributes of this instance.  Because tracing details are stored |         attributes of this instance.  Because tracing details are stored | ||||||
|         asynchronously by Cassandra, this may need to retry the session |         asynchronously by Cassandra, this may need to retry the session | ||||||
|         detail fetch up to five times before raising :exc:`.TraceUnavailable`. |         detail fetch.  If the trace is still not available after `max_wait` | ||||||
|  |         seconds, :exc:`.TraceUnavailable` will be raised; if `max_wait` is | ||||||
|         Currently intended for internal use only. |         :const:`None`, this will retry forever. | ||||||
|         """ |         """ | ||||||
|         attempt = 0 |         attempt = 0 | ||||||
|         while attempt <= self._MAX_ATTEMPTS: |         start = time.time() | ||||||
|             attempt += 1 |         while True: | ||||||
|  |             if max_wait is not None and time.time() - start >= max_wait: | ||||||
|  |                 raise TraceUnavailable("Trace information was not available within %f seconds" % (max_wait,)) | ||||||
|             session_results = self._session.execute(self._SELECT_SESSIONS_FORMAT, (self.trace_id,)) |             session_results = self._session.execute(self._SELECT_SESSIONS_FORMAT, (self.trace_id,)) | ||||||
|             if not session_results or session_results[0].duration is None: |             if not session_results or session_results[0].duration is None: | ||||||
|                 time.sleep(self._BASE_RETRY_SLEEP * attempt) |                 time.sleep(self._BASE_RETRY_SLEEP * (2 ** attempt)) | ||||||
|  |                 attempt += 1 | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             session_row = session_results[0] |             session_row = session_results[0] | ||||||
| @@ -419,6 +424,7 @@ class QueryTrace(object): | |||||||
|             event_results = self._session.execute(self._SELECT_EVENTS_FORMAT, (self.trace_id,)) |             event_results = self._session.execute(self._SELECT_EVENTS_FORMAT, (self.trace_id,)) | ||||||
|             self.events = tuple(TraceEvent(r.activity, r.event_id, r.source, r.source_elapsed, r.thread) |             self.events = tuple(TraceEvent(r.activity, r.event_id, r.source, r.source_elapsed, r.thread) | ||||||
|                                 for r in event_results) |                                 for r in event_results) | ||||||
|  |             break | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return "%s [%s] coordinator: %s, started at: %s, duration: %s, parameters: %s" \ |         return "%s [%s] coordinator: %s, started at: %s, duration: %s, parameters: %s" \ | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ except ImportError: | |||||||
|     import unittest # noqa |     import unittest # noqa | ||||||
|  |  | ||||||
| import cassandra | import cassandra | ||||||
| from cassandra.query import SimpleStatement | from cassandra.query import SimpleStatement, TraceUnavailable | ||||||
| from cassandra.io.asyncorereactor import AsyncoreConnection | from cassandra.io.asyncorereactor import AsyncoreConnection | ||||||
| from cassandra.policies import RoundRobinPolicy, ExponentialReconnectionPolicy, RetryPolicy, SimpleConvictionPolicy, HostDistance | from cassandra.policies import RoundRobinPolicy, ExponentialReconnectionPolicy, RetryPolicy, SimpleConvictionPolicy, HostDistance | ||||||
|  |  | ||||||
| @@ -214,6 +214,16 @@ class ClusterTests(unittest.TestCase): | |||||||
|         future.result() |         future.result() | ||||||
|         self.assertEqual(None, future.get_query_trace()) |         self.assertEqual(None, future.get_query_trace()) | ||||||
|  |  | ||||||
|  |     def test_trace_timeout(self): | ||||||
|  |         cluster = Cluster() | ||||||
|  |         session = cluster.connect() | ||||||
|  |  | ||||||
|  |         query = "SELECT * FROM system.local" | ||||||
|  |         statement = SimpleStatement(query) | ||||||
|  |         future = session.execute_async(statement, trace=True) | ||||||
|  |         future.result() | ||||||
|  |         self.assertRaises(TraceUnavailable, future.get_query_trace, -1.0) | ||||||
|  |  | ||||||
|     def test_string_coverage(self): |     def test_string_coverage(self): | ||||||
|         """ |         """ | ||||||
|         Ensure str(future) returns without error |         Ensure str(future) returns without error | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from cassandra.cluster import Cluster | |||||||
|  |  | ||||||
|  |  | ||||||
| class QueryTest(unittest.TestCase): | class QueryTest(unittest.TestCase): | ||||||
|  |  | ||||||
|     def test_query(self): |     def test_query(self): | ||||||
|         cluster = Cluster() |         cluster = Cluster() | ||||||
|         session = cluster.connect() |         session = cluster.connect() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Tyler Hobbs
					Tyler Hobbs