e6data-python-connector 3.0.0__py3-none-any.whl → 3.0.1rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- e6data_python_connector/async_connection.py +53 -2
- e6data_python_connector/async_connection_pool.py +2 -0
- e6data_python_connector/async_cursor.py +379 -19
- e6data_python_connector/async_dialect.py +2 -1
- e6data_python_connector/cluster_manager.py +3 -1
- e6data_python_connector/connection_pool.py +11 -9
- e6data_python_connector/dialect.py +7 -2
- e6data_python_connector/e6data_grpc.py +715 -108
- e6data_python_connector/result_batch.py +49 -0
- e6data_python_connector/result_decode.py +469 -0
- e6data_python_connector/result_decode_worker.py +50 -0
- e6data_python_connector/result_prefetch.py +421 -0
- e6data_python_connector/server/e6x_engine_pb2.py +149 -147
- e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/METADATA +134 -2
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/RECORD +25 -18
- test/integration/benchmark_result_batch_v2.py +149 -0
- test/integration/result_batch_benchmark_support.py +196 -0
- test/integration/support.py +3 -0
- test/integration/test_result_batch_v2.py +120 -0
- test/test_connection_pool.py +3 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/WHEEL +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/entry_points.txt +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/licenses/LICENSE +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1rc2.dist-info}/top_level.txt +0 -0
|
@@ -50,7 +50,11 @@ class AsyncConnection:
|
|
|
50
50
|
token_url=None, oauth_scope=None, access_token=None,
|
|
51
51
|
client_auth_method='basic', *, operation_timeout=600.0,
|
|
52
52
|
oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
|
|
53
|
-
max_receive_message_bytes=64 * 1024 * 1024
|
|
53
|
+
max_receive_message_bytes=64 * 1024 * 1024,
|
|
54
|
+
enable_result_batch_v2=False):
|
|
55
|
+
from .result_decode_worker import is_decode_worker
|
|
56
|
+
if is_decode_worker():
|
|
57
|
+
raise RuntimeError('A result decode worker cannot create a connection.')
|
|
54
58
|
if sys.version_info < (3, 11):
|
|
55
59
|
raise RuntimeError('The async API requires Python 3.11 or newer.')
|
|
56
60
|
try:
|
|
@@ -78,13 +82,19 @@ class AsyncConnection:
|
|
|
78
82
|
raise ValueError('Async OAuth requires verified TLS (secure=True).')
|
|
79
83
|
if ssl_cert is not None and not isinstance(ssl_cert, (str, bytes)):
|
|
80
84
|
raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
|
|
85
|
+
if not isinstance(enable_result_batch_v2, bool):
|
|
86
|
+
raise ValueError('enable_result_batch_v2 must be a boolean.')
|
|
87
|
+
if enable_result_batch_v2:
|
|
88
|
+
from .result_decode import validate_decode_runtime
|
|
89
|
+
validate_decode_runtime()
|
|
81
90
|
settings = dict(host=host, port=port, username=username, password=password,
|
|
82
91
|
catalog=catalog, database=database, cluster_name=cluster_name,
|
|
83
92
|
secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
|
|
84
93
|
scheme=scheme, debug=debug, require_fastbinary=require_fastbinary,
|
|
85
94
|
client_id=client_id, client_secret=client_secret, token_url=token_url,
|
|
86
95
|
oauth_scope=oauth_scope, access_token=access_token,
|
|
87
|
-
client_auth_method=client_auth_method
|
|
96
|
+
client_auth_method=client_auth_method,
|
|
97
|
+
enable_result_batch_v2=enable_result_batch_v2)
|
|
88
98
|
for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
|
|
89
99
|
('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
|
|
90
100
|
settings[name] = validate_positive_timeout(value, name)
|
|
@@ -103,6 +113,9 @@ class AsyncConnection:
|
|
|
103
113
|
settings['grpc_options'] = MappingProxyType(options)
|
|
104
114
|
settings['max_receive_message_bytes'] = max_receive_message_bytes
|
|
105
115
|
self._config = MappingProxyType(settings)
|
|
116
|
+
if debug:
|
|
117
|
+
from .e6data_grpc import _configure_debug_logging
|
|
118
|
+
_configure_debug_logging()
|
|
106
119
|
self._oauth_enabled = not modes[0]
|
|
107
120
|
self._channel = self._client = self._token_provider = None
|
|
108
121
|
self._owns_token_provider = True
|
|
@@ -125,6 +138,11 @@ class AsyncConnection:
|
|
|
125
138
|
self._cleanup_error = None
|
|
126
139
|
self._cluster_manager = None
|
|
127
140
|
self._owns_cluster_manager = True
|
|
141
|
+
self._decoder_started = False
|
|
142
|
+
self._decoder_lease = None
|
|
143
|
+
if enable_result_batch_v2:
|
|
144
|
+
from .result_decode import DecoderLease
|
|
145
|
+
self._decoder_lease = DecoderLease()
|
|
128
146
|
|
|
129
147
|
def __getattr__(self, name):
|
|
130
148
|
config = self.__dict__.get('_config', {})
|
|
@@ -300,6 +318,24 @@ class AsyncConnection:
|
|
|
300
318
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
301
319
|
return metadata
|
|
302
320
|
|
|
321
|
+
async def _ensure_decoder(self, deadline):
|
|
322
|
+
self._check_owner()
|
|
323
|
+
if self._decoder_lease is not None and not self._decoder_started:
|
|
324
|
+
from .async_work import run_blocking
|
|
325
|
+
await run_blocking(self._decoder_lease.start, deadline, deadline=deadline)
|
|
326
|
+
self._check_owner()
|
|
327
|
+
self._decoder_started = True
|
|
328
|
+
|
|
329
|
+
async def _raw_result_rpc(self, method_name, request, metadata, deadline):
|
|
330
|
+
"""Transport only: the owning task already froze identity and metadata."""
|
|
331
|
+
call = getattr(self._client, method_name)(
|
|
332
|
+
request, metadata=metadata, timeout=self._remaining(deadline))
|
|
333
|
+
self._calls.add(call)
|
|
334
|
+
try:
|
|
335
|
+
return await call
|
|
336
|
+
finally:
|
|
337
|
+
self._calls.discard(call)
|
|
338
|
+
|
|
303
339
|
async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
|
|
304
340
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
305
341
|
refreshed = switched = False
|
|
@@ -500,6 +536,15 @@ class AsyncConnection:
|
|
|
500
536
|
# Native channel close with no grace cancels transports without waiting.
|
|
501
537
|
if self._channel is not None:
|
|
502
538
|
await self._channel.close()
|
|
539
|
+
if self._decoder_lease is not None:
|
|
540
|
+
# Retirement must survive loop shutdown and saturated work slots.
|
|
541
|
+
self._decoder_lease.retire(deadline)
|
|
542
|
+
try:
|
|
543
|
+
async with asyncio.timeout_at(deadline):
|
|
544
|
+
while self._decoder_lease.cleanup_pending:
|
|
545
|
+
await asyncio.sleep(min(0.005, self._remaining(deadline)))
|
|
546
|
+
except (Exception, asyncio.CancelledError):
|
|
547
|
+
self._cleanup_error = OperationalError('Result decoder cleanup remains pending.')
|
|
503
548
|
for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
|
|
504
549
|
(self._token_provider, self._owns_token_provider)):
|
|
505
550
|
if owned and resource is not None:
|
|
@@ -538,6 +583,12 @@ class AsyncConnection:
|
|
|
538
583
|
if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
|
|
539
584
|
raise ProgrammingError('Connection belongs to another owner.')
|
|
540
585
|
await self.close()
|
|
586
|
+
if self._decoder_lease is not None and self._decoder_lease.cleanup_pending:
|
|
587
|
+
raise ProgrammingError('Cannot reopen while result decoder cleanup remains pending.')
|
|
588
|
+
if self.enable_result_batch_v2:
|
|
589
|
+
from .result_decode import DecoderLease
|
|
590
|
+
self._decoder_lease = DecoderLease()
|
|
591
|
+
self._decoder_started = False
|
|
541
592
|
self._state = 'new'
|
|
542
593
|
self._session_id = self._session_task = self._close_task = self._open_task = None
|
|
543
594
|
if self._owns_token_provider:
|
|
@@ -316,6 +316,8 @@ class AsyncConnectionPool:
|
|
|
316
316
|
healthy = healthy and cursor._state != 'SUBMISSION_UNKNOWN'
|
|
317
317
|
await cursor._close_owned(deadline)
|
|
318
318
|
healthy = healthy and cursor.cleanup_error is None
|
|
319
|
+
healthy = (healthy and cursor._pending_result is None
|
|
320
|
+
and not cursor._retired_results)
|
|
319
321
|
healthy = (healthy and not connection._routes and not connection._ambiguous_submissions
|
|
320
322
|
and connection._state == 'open')
|
|
321
323
|
except (Exception, asyncio.CancelledError):
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Native asynchronous query lifecycle, with no replay of consumed results."""
|
|
2
2
|
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
3
5
|
from collections import deque
|
|
4
6
|
from contextlib import contextmanager
|
|
5
7
|
from io import BytesIO
|
|
@@ -11,6 +13,28 @@ from .e6data_grpc import _escaper
|
|
|
11
13
|
from .exceptions import (AmbiguousSubmissionError, IncompleteResultError,
|
|
12
14
|
NotSupportedError, OperationalError, ProgrammingError, OAuthError)
|
|
13
15
|
from .server import e6x_engine_pb2 as pb
|
|
16
|
+
from .result_batch import ResultBatchBuffer, decode_result_batches
|
|
17
|
+
from .result_prefetch import ContinuousResultStream, PendingFetch
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _log_result_batch(protocol, rpc_seconds, status, response=None, decode_seconds=0.0):
|
|
24
|
+
chunk_count = ((len(response.resultBatches) if protocol == 'v2'
|
|
25
|
+
else int(bool(response.resultBatch))) if response else 0)
|
|
26
|
+
serialized_bytes = response.ByteSize() if response else 0
|
|
27
|
+
logger.debug(
|
|
28
|
+
'Result batch fetch protocol=%s rpc_seconds=%.6f decode_seconds=%.6f '
|
|
29
|
+
'chunk_count=%d serialized_bytes=%d status=%s',
|
|
30
|
+
protocol, rpc_seconds, decode_seconds, chunk_count, serialized_bytes, status, extra={
|
|
31
|
+
'result_batch_protocol': protocol,
|
|
32
|
+
'result_batch_rpc_seconds': rpc_seconds,
|
|
33
|
+
'result_batch_chunk_count': chunk_count,
|
|
34
|
+
'result_batch_serialized_bytes': serialized_bytes,
|
|
35
|
+
'result_batch_decode_seconds': decode_seconds,
|
|
36
|
+
'result_batch_status': status,
|
|
37
|
+
})
|
|
14
38
|
|
|
15
39
|
|
|
16
40
|
def _metadata_columns(payload):
|
|
@@ -46,8 +70,20 @@ class AsyncCursor:
|
|
|
46
70
|
self._active_call = None
|
|
47
71
|
self._close_task = None
|
|
48
72
|
self._cleanup_error = None
|
|
73
|
+
self._result_batches = ResultBatchBuffer()
|
|
74
|
+
self._pending_result = None
|
|
75
|
+
self._result_available = None
|
|
76
|
+
self._retired_results = []
|
|
77
|
+
self._decode_owner = None
|
|
78
|
+
self._reset_result_protocol()
|
|
49
79
|
connection._cursors.add(self)
|
|
50
80
|
|
|
81
|
+
def _reset_result_protocol(self):
|
|
82
|
+
self._result_batch_v2 = self._connection.enable_result_batch_v2
|
|
83
|
+
self._result_protocol = 'v2' if self._result_batch_v2 else 'v1'
|
|
84
|
+
self._result_session_id = None
|
|
85
|
+
self._result_batches.clear()
|
|
86
|
+
|
|
51
87
|
@property
|
|
52
88
|
def connection(self):
|
|
53
89
|
return self._public_connection
|
|
@@ -133,10 +169,14 @@ class AsyncCursor:
|
|
|
133
169
|
self._active_call = None
|
|
134
170
|
|
|
135
171
|
def _fail_result(self, reason):
|
|
172
|
+
self._retire_prefetch()
|
|
173
|
+
self._cancel_decode()
|
|
136
174
|
if self._failure is None:
|
|
137
175
|
self._failure = IncompleteResultError(reason, query_id=self.query_id)
|
|
138
176
|
if self._state != 'CLOSED':
|
|
139
177
|
self._state = 'RESULT_FAILED'
|
|
178
|
+
self._rows.clear()
|
|
179
|
+
self._result_batches.clear()
|
|
140
180
|
return self._failure
|
|
141
181
|
|
|
142
182
|
def _accept_rows(self, rows):
|
|
@@ -147,10 +187,18 @@ class AsyncCursor:
|
|
|
147
187
|
self._rownumber += len(rows)
|
|
148
188
|
return rows
|
|
149
189
|
|
|
150
|
-
async def _query_request(self, cls, deadline):
|
|
190
|
+
async def _query_request(self, cls, deadline, *, owned=False):
|
|
151
191
|
if self._route is None:
|
|
152
192
|
raise ProgrammingError('No query handle is known.')
|
|
153
|
-
|
|
193
|
+
if self._result_batch_v2 and not self._connection._oauth_enabled and self._result_session_id:
|
|
194
|
+
session = self._result_session_id
|
|
195
|
+
elif owned:
|
|
196
|
+
# Owned cleanup cannot mint a replacement authentication session.
|
|
197
|
+
session = '' if self._connection._oauth_enabled else self._connection._session_id
|
|
198
|
+
if session is None:
|
|
199
|
+
raise OperationalError('Cleanup has no authenticated legacy session.')
|
|
200
|
+
else:
|
|
201
|
+
session = await self._connection.get_session_id(deadline=deadline)
|
|
154
202
|
return cls(sessionId=session, queryId=self.query_id, engineIP=self._route.engine_ip)
|
|
155
203
|
|
|
156
204
|
async def _refresh_metadata(self, deadline):
|
|
@@ -168,11 +216,13 @@ class AsyncCursor:
|
|
|
168
216
|
from .async_connection import QueryRoute
|
|
169
217
|
if self._state != 'EMPTY':
|
|
170
218
|
raise ProgrammingError('Clear the previous query before executing another.')
|
|
219
|
+
self._reset_result_protocol()
|
|
171
220
|
if not isinstance(operation, str) or not operation.strip():
|
|
172
221
|
raise ProgrammingError('operation must be a nonempty SQL string.')
|
|
173
222
|
sql = operation.strip().removesuffix(';')
|
|
174
223
|
if parameters is not None:
|
|
175
224
|
sql = sql % _escaper.escape_args(parameters)
|
|
225
|
+
await self._connection._ensure_decoder(deadline)
|
|
176
226
|
session = await self._connection.get_session_id(deadline=deadline)
|
|
177
227
|
v2 = bool(self._catalog)
|
|
178
228
|
request_type = pb.PrepareStatementV2Request if v2 else pb.PrepareStatementRequest
|
|
@@ -268,6 +318,8 @@ class AsyncCursor:
|
|
|
268
318
|
|
|
269
319
|
async def _next_batch(self, deadline):
|
|
270
320
|
from .async_work import reserve_work
|
|
321
|
+
if self._result_batch_v2:
|
|
322
|
+
return await self._next_v2_batch(deadline)
|
|
271
323
|
if self._state == 'EXHAUSTED':
|
|
272
324
|
return None
|
|
273
325
|
if self._columns is None:
|
|
@@ -279,17 +331,31 @@ class AsyncCursor:
|
|
|
279
331
|
def mark_dispatch():
|
|
280
332
|
nonlocal consumed
|
|
281
333
|
consumed = True
|
|
334
|
+
started = time.monotonic()
|
|
335
|
+
response = None
|
|
336
|
+
decode_seconds = 0.0
|
|
282
337
|
try:
|
|
283
|
-
|
|
338
|
+
try:
|
|
339
|
+
response = await self._call('getNextResultBatch', request, deadline, on_dispatch=mark_dispatch)
|
|
340
|
+
finally:
|
|
341
|
+
rpc_seconds = time.monotonic() - started
|
|
284
342
|
payload = response.resultBatch
|
|
285
343
|
if not payload:
|
|
286
344
|
self._state = 'EXHAUSTED'
|
|
287
345
|
return None
|
|
288
|
-
|
|
346
|
+
decode_started = time.monotonic()
|
|
347
|
+
try:
|
|
348
|
+
return await self._decode_batch(payload, reservation, revision)
|
|
349
|
+
finally:
|
|
350
|
+
decode_seconds = time.monotonic() - decode_started
|
|
289
351
|
except asyncio.CancelledError:
|
|
290
352
|
if consumed:
|
|
291
353
|
self._fail_result('ambiguous_result')
|
|
292
354
|
raise
|
|
355
|
+
except grpc.RpcError:
|
|
356
|
+
if consumed:
|
|
357
|
+
self._fail_result('ambiguous_result')
|
|
358
|
+
raise
|
|
293
359
|
except IncompleteResultError:
|
|
294
360
|
raise
|
|
295
361
|
except Exception as error:
|
|
@@ -298,6 +364,281 @@ class AsyncCursor:
|
|
|
298
364
|
raise
|
|
299
365
|
finally:
|
|
300
366
|
reservation.release()
|
|
367
|
+
if consumed:
|
|
368
|
+
status = 'ok' if response is not None and self._failure is None else 'error'
|
|
369
|
+
_log_result_batch('v1', rpc_seconds,
|
|
370
|
+
status, response if status == 'ok' else None, decode_seconds if status == 'ok' else 0.0)
|
|
371
|
+
|
|
372
|
+
def _retain_result_session(self, response, revision):
|
|
373
|
+
self._publish(revision)
|
|
374
|
+
if self._result_batch_v2 and not self._connection._oauth_enabled and response.sessionId:
|
|
375
|
+
pending = self._pending_result[0] if self._pending_result is not None else None
|
|
376
|
+
self._result_session_id = (pending.session_id if isinstance(pending, ContinuousResultStream)
|
|
377
|
+
and pending.session_id else response.sessionId)
|
|
378
|
+
|
|
379
|
+
async def _decode_v2_response(self, response, reservation, revision, deadline=None):
|
|
380
|
+
self._retain_result_session(response, revision)
|
|
381
|
+
owner = None
|
|
382
|
+
try:
|
|
383
|
+
if self._connection._decoder_started:
|
|
384
|
+
owner = self._connection._decoder_lease.new_owner()
|
|
385
|
+
self._decode_owner = owner
|
|
386
|
+
chunks = await reservation.run(
|
|
387
|
+
self._connection._decoder_lease.decode, tuple(self._columns),
|
|
388
|
+
tuple(response.resultBatches), deadline, owner)
|
|
389
|
+
else:
|
|
390
|
+
chunks = await reservation.run(decode_result_batches, self._columns, response.resultBatches)
|
|
391
|
+
except asyncio.CancelledError:
|
|
392
|
+
self._cancel_decode()
|
|
393
|
+
raise
|
|
394
|
+
except Exception as error:
|
|
395
|
+
raise self._fail_result('decode_failed') from error
|
|
396
|
+
finally:
|
|
397
|
+
if self._decode_owner is owner:
|
|
398
|
+
self._decode_owner = None
|
|
399
|
+
self._publish(revision)
|
|
400
|
+
self._result_batches.accept(chunks, response.endOfStream)
|
|
401
|
+
|
|
402
|
+
def _cancel_decode(self):
|
|
403
|
+
if self._decode_owner is not None:
|
|
404
|
+
self._connection._decoder_lease.cancel(self._decode_owner)
|
|
405
|
+
|
|
406
|
+
def _retire_prefetch(self):
|
|
407
|
+
if self._pending_result is not None:
|
|
408
|
+
pending, _, _ = self._pending_result
|
|
409
|
+
self._pending_result = None
|
|
410
|
+
self._result_available = None
|
|
411
|
+
pending.retire()
|
|
412
|
+
self._retired_results.append(pending)
|
|
413
|
+
|
|
414
|
+
async def _dispatch_result(self, deadline, permit=None):
|
|
415
|
+
"""Freeze identity in the owner; the background task only runs transport."""
|
|
416
|
+
revision, route = self._revision, self._route
|
|
417
|
+
request = await self._query_request(pb.GetNextResultBatchRequest, deadline)
|
|
418
|
+
metadata = await self._connection._metadata(deadline, route)
|
|
419
|
+
self._publish(revision)
|
|
420
|
+
self._connection._remaining(deadline)
|
|
421
|
+
method = 'getNextResultBatchV2' if self._result_protocol == 'v2' else 'getNextResultBatch'
|
|
422
|
+
task = asyncio.create_task(self._connection._raw_result_rpc(method, request, tuple(metadata), deadline))
|
|
423
|
+
pending = PendingFetch(task, permit, session_id=request.sessionId, deadline=deadline)
|
|
424
|
+
self._pending_result = (pending, revision, route)
|
|
425
|
+
|
|
426
|
+
async def _take_pending_result(self, deadline):
|
|
427
|
+
pending, revision, route = self._pending_result
|
|
428
|
+
if isinstance(pending, ContinuousResultStream):
|
|
429
|
+
event = self._result_available
|
|
430
|
+
while not pending.ready:
|
|
431
|
+
event.clear()
|
|
432
|
+
if pending.ready:
|
|
433
|
+
break
|
|
434
|
+
async with asyncio.timeout_at(deadline):
|
|
435
|
+
await event.wait()
|
|
436
|
+
else:
|
|
437
|
+
handle = pending.handle
|
|
438
|
+
if handle is not None and not pending.settled:
|
|
439
|
+
done, _ = await asyncio.wait({handle}, timeout=self._connection._remaining(deadline))
|
|
440
|
+
if not done:
|
|
441
|
+
raise TimeoutError('Result transport exceeded the operation deadline.')
|
|
442
|
+
self._publish(revision)
|
|
443
|
+
if route != self._route:
|
|
444
|
+
raise asyncio.CancelledError()
|
|
445
|
+
if not self._connection._oauth_enabled and pending.session_id:
|
|
446
|
+
self._result_session_id = pending.session_id
|
|
447
|
+
if not isinstance(pending, ContinuousResultStream):
|
|
448
|
+
self._pending_result = None
|
|
449
|
+
response = pending.take()
|
|
450
|
+
new_strategy = getattr(response, 'new_strategy', None)
|
|
451
|
+
if new_strategy in ('blue', 'green'):
|
|
452
|
+
self._connection._pending_strategy = new_strategy
|
|
453
|
+
self._connection._apply_pending_strategy()
|
|
454
|
+
return response, pending.elapsed
|
|
455
|
+
|
|
456
|
+
async def _start_prefetch(self, response, deadline):
|
|
457
|
+
if (self._result_protocol != 'v2' or response.endOfStream
|
|
458
|
+
or self._pending_result is not None):
|
|
459
|
+
return
|
|
460
|
+
revision, route = self._revision, self._route
|
|
461
|
+
try:
|
|
462
|
+
self._publish(revision)
|
|
463
|
+
self._connection._remaining(deadline)
|
|
464
|
+
except BaseException:
|
|
465
|
+
logger.debug('Continuous result download preparation failed.', extra={
|
|
466
|
+
'result_batch_download_status': 'preparation_failed'})
|
|
467
|
+
raise
|
|
468
|
+
event = asyncio.Event()
|
|
469
|
+
stream = ContinuousResultStream(session_id=self._result_session_id, notify=event.set)
|
|
470
|
+
producer = asyncio.create_task(self._download_results(stream, revision, route))
|
|
471
|
+
stream.set_handle(producer)
|
|
472
|
+
self._result_available = event
|
|
473
|
+
self._pending_result = (stream, revision, route)
|
|
474
|
+
logger.debug('Continuous result download started.', extra={'result_batch_download_status': 'started'})
|
|
475
|
+
|
|
476
|
+
async def _download_results(self, stream, revision, route):
|
|
477
|
+
"""Download ordered envelopes without using any decode/work reservation."""
|
|
478
|
+
budget = self._connection.operation_timeout
|
|
479
|
+
no_progress_deadline = time.monotonic() + budget
|
|
480
|
+
backoff = .01
|
|
481
|
+
rpc_started = None
|
|
482
|
+
try:
|
|
483
|
+
while not stream.retired:
|
|
484
|
+
deadline = min(time.monotonic() + budget, no_progress_deadline)
|
|
485
|
+
rpc_started = None
|
|
486
|
+
metadata = await self._connection._metadata(deadline, route)
|
|
487
|
+
if stream.retired or revision != self._revision or route != self._route:
|
|
488
|
+
stream.retire()
|
|
489
|
+
return
|
|
490
|
+
if self._lease_guard is not None:
|
|
491
|
+
self._lease_guard(internal=True)
|
|
492
|
+
self._connection._check_owner(_internal=True)
|
|
493
|
+
self._connection._remaining(deadline)
|
|
494
|
+
session = '' if self._connection._oauth_enabled else stream.session_id
|
|
495
|
+
if not self._connection._oauth_enabled and not session:
|
|
496
|
+
raise ProgrammingError('An active query session is required to download results.')
|
|
497
|
+
request = pb.GetNextResultBatchRequest(
|
|
498
|
+
sessionId=session, queryId=route.query_id, engineIP=route.engine_ip)
|
|
499
|
+
rpc_started = time.monotonic()
|
|
500
|
+
transport = asyncio.create_task(self._connection._raw_result_rpc(
|
|
501
|
+
'getNextResultBatchV2', request, tuple(metadata), deadline))
|
|
502
|
+
stream.set_transport(transport)
|
|
503
|
+
try:
|
|
504
|
+
response = await asyncio.shield(transport)
|
|
505
|
+
finally:
|
|
506
|
+
if transport.done():
|
|
507
|
+
stream.clear_transport(transport)
|
|
508
|
+
stream.push(response, time.monotonic() - rpc_started)
|
|
509
|
+
if stream.retired or response.endOfStream:
|
|
510
|
+
return
|
|
511
|
+
if any(response.resultBatches):
|
|
512
|
+
no_progress_deadline = time.monotonic() + budget
|
|
513
|
+
backoff = .01
|
|
514
|
+
else:
|
|
515
|
+
await asyncio.sleep(min(backoff, self._connection._remaining(no_progress_deadline)))
|
|
516
|
+
backoff = min(backoff * 2, .1)
|
|
517
|
+
except asyncio.CancelledError:
|
|
518
|
+
stream.retire()
|
|
519
|
+
raise
|
|
520
|
+
except Exception as error:
|
|
521
|
+
stream.fail(error, 0.0 if rpc_started is None else time.monotonic() - rpc_started)
|
|
522
|
+
finally:
|
|
523
|
+
stream.finish()
|
|
524
|
+
|
|
525
|
+
async def _next_v2_batch(self, deadline):
|
|
526
|
+
from .async_work import reserve_work
|
|
527
|
+
batch = self._result_batches.pop()
|
|
528
|
+
if batch is not None:
|
|
529
|
+
return batch
|
|
530
|
+
if self._result_batches.finished or self._state == 'EXHAUSTED':
|
|
531
|
+
self._state = 'EXHAUSTED'
|
|
532
|
+
return None
|
|
533
|
+
consumed = self._pending_result is not None
|
|
534
|
+
backoff = .01
|
|
535
|
+
try:
|
|
536
|
+
await self._connection._ensure_decoder(deadline)
|
|
537
|
+
if self._columns is None:
|
|
538
|
+
await self._refresh_metadata(deadline)
|
|
539
|
+
while True:
|
|
540
|
+
consumed = consumed or self._pending_result is not None
|
|
541
|
+
reservation = None
|
|
542
|
+
revision = self._revision
|
|
543
|
+
protocol = self._result_protocol
|
|
544
|
+
previously_consumed = consumed
|
|
545
|
+
dispatched = False
|
|
546
|
+
response = None
|
|
547
|
+
status = 'error'
|
|
548
|
+
rpc_seconds = decode_seconds = 0.0
|
|
549
|
+
started = time.monotonic()
|
|
550
|
+
prefetched = self._pending_result is not None
|
|
551
|
+
transport_record = None
|
|
552
|
+
transport_status = 'error'
|
|
553
|
+
|
|
554
|
+
def mark_dispatch():
|
|
555
|
+
nonlocal consumed, dispatched
|
|
556
|
+
consumed = dispatched = True
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
try:
|
|
560
|
+
if self._pending_result is None:
|
|
561
|
+
await self._dispatch_result(deadline)
|
|
562
|
+
transport_record = self._pending_result[0]
|
|
563
|
+
mark_dispatch()
|
|
564
|
+
response, rpc_seconds = await self._take_pending_result(deadline)
|
|
565
|
+
transport_status = 'ok'
|
|
566
|
+
except grpc.RpcError as error:
|
|
567
|
+
if protocol == 'v2' and error.code() == grpc.StatusCode.UNIMPLEMENTED:
|
|
568
|
+
consumed = previously_consumed
|
|
569
|
+
self._retire_prefetch()
|
|
570
|
+
# Settle V2 before V1 can advance the same query session.
|
|
571
|
+
async with asyncio.timeout_at(deadline):
|
|
572
|
+
await self._settle_retired_results()
|
|
573
|
+
self._publish(revision)
|
|
574
|
+
self._result_protocol = 'v1'
|
|
575
|
+
status = 'unimplemented'
|
|
576
|
+
logger.debug('Result batch compatibility fallback.',
|
|
577
|
+
extra={'result_batch_fallback': True})
|
|
578
|
+
continue
|
|
579
|
+
raise
|
|
580
|
+
finally:
|
|
581
|
+
wait_seconds = time.monotonic() - started
|
|
582
|
+
rpc_seconds = (transport_record.elapsed
|
|
583
|
+
if transport_record is not None and (
|
|
584
|
+
isinstance(transport_record, ContinuousResultStream)
|
|
585
|
+
or transport_record.settled)
|
|
586
|
+
else wait_seconds)
|
|
587
|
+
if prefetched:
|
|
588
|
+
logger.debug('Result prefetched RPC seconds=%.6f wait_seconds=%.6f status=%s',
|
|
589
|
+
rpc_seconds, wait_seconds, transport_status, extra={
|
|
590
|
+
'result_batch_prefetched_rpc_seconds': rpc_seconds,
|
|
591
|
+
'result_batch_prefetch_wait_seconds': wait_seconds,
|
|
592
|
+
'result_batch_prefetch_status': transport_status})
|
|
593
|
+
decode_started = time.monotonic()
|
|
594
|
+
try:
|
|
595
|
+
if protocol == 'v2':
|
|
596
|
+
await self._start_prefetch(response, deadline)
|
|
597
|
+
reservation = await reserve_work(deadline=deadline)
|
|
598
|
+
await self._decode_v2_response(response, reservation, revision, deadline)
|
|
599
|
+
else:
|
|
600
|
+
self._retain_result_session(response, revision)
|
|
601
|
+
if response.resultBatch:
|
|
602
|
+
reservation = await reserve_work(deadline=deadline)
|
|
603
|
+
batch = await self._decode_batch(response.resultBatch, reservation, revision)
|
|
604
|
+
else:
|
|
605
|
+
self._state = 'EXHAUSTED'
|
|
606
|
+
batch = None
|
|
607
|
+
finally:
|
|
608
|
+
decode_seconds = time.monotonic() - decode_started
|
|
609
|
+
status = 'ok'
|
|
610
|
+
if protocol == 'v1':
|
|
611
|
+
return batch
|
|
612
|
+
batch = self._result_batches.pop()
|
|
613
|
+
if batch is not None:
|
|
614
|
+
return batch
|
|
615
|
+
if self._result_batches.finished:
|
|
616
|
+
self._state = 'EXHAUSTED'
|
|
617
|
+
return None
|
|
618
|
+
finally:
|
|
619
|
+
if reservation is not None:
|
|
620
|
+
reservation.release()
|
|
621
|
+
if dispatched:
|
|
622
|
+
_log_result_batch(protocol, rpc_seconds, status,
|
|
623
|
+
response if status == 'ok' else None,
|
|
624
|
+
decode_seconds if status == 'ok' else 0.0)
|
|
625
|
+
if not isinstance(transport_record, ContinuousResultStream):
|
|
626
|
+
await asyncio.sleep(min(backoff, self._connection._remaining(deadline)))
|
|
627
|
+
backoff = min(backoff * 2, .1)
|
|
628
|
+
except asyncio.CancelledError:
|
|
629
|
+
if consumed:
|
|
630
|
+
self._fail_result('ambiguous_result')
|
|
631
|
+
raise
|
|
632
|
+
except grpc.RpcError:
|
|
633
|
+
if consumed:
|
|
634
|
+
self._fail_result('ambiguous_result')
|
|
635
|
+
raise
|
|
636
|
+
except IncompleteResultError:
|
|
637
|
+
raise
|
|
638
|
+
except Exception as error:
|
|
639
|
+
if consumed:
|
|
640
|
+
raise self._fail_result('ambiguous_result') from error
|
|
641
|
+
raise
|
|
301
642
|
|
|
302
643
|
async def _fetchmany(self, size, deadline):
|
|
303
644
|
while len(self._rows) < size and self._state != 'EXHAUSTED':
|
|
@@ -317,7 +658,7 @@ class AsyncCursor:
|
|
|
317
658
|
raise ProgrammingError('Fetch size must be a nonnegative integer.')
|
|
318
659
|
try:
|
|
319
660
|
return await self._fetchmany(size, self._connection._deadline(timeout))
|
|
320
|
-
except (asyncio.CancelledError, OperationalError, ProgrammingError, OAuthError):
|
|
661
|
+
except (asyncio.CancelledError, grpc.RpcError, OperationalError, ProgrammingError, OAuthError):
|
|
321
662
|
raise
|
|
322
663
|
except Exception as error:
|
|
323
664
|
raise _operational(error) from error
|
|
@@ -335,7 +676,7 @@ class AsyncCursor:
|
|
|
335
676
|
if rows is not None:
|
|
336
677
|
self._rownumber += len(rows)
|
|
337
678
|
return rows
|
|
338
|
-
except (asyncio.CancelledError, OperationalError, ProgrammingError, OAuthError):
|
|
679
|
+
except (asyncio.CancelledError, grpc.RpcError, OperationalError, ProgrammingError, OAuthError):
|
|
339
680
|
raise
|
|
340
681
|
except Exception as error:
|
|
341
682
|
raise _operational(error) from error
|
|
@@ -353,6 +694,8 @@ class AsyncCursor:
|
|
|
353
694
|
return rows
|
|
354
695
|
rows.extend(batch)
|
|
355
696
|
self._rownumber += len(batch)
|
|
697
|
+
except grpc.RpcError:
|
|
698
|
+
raise
|
|
356
699
|
except asyncio.CancelledError:
|
|
357
700
|
if rows:
|
|
358
701
|
self._fail_result('aggregation_failed')
|
|
@@ -440,38 +783,50 @@ class AsyncCursor:
|
|
|
440
783
|
return await self._connection.get_schema_names(self._catalog, timeout=timeout)
|
|
441
784
|
|
|
442
785
|
async def _stop_active(self):
|
|
786
|
+
self._cancel_decode()
|
|
787
|
+
self._retire_prefetch()
|
|
443
788
|
self._revision += 1
|
|
444
789
|
task = self._operation_task or self._active_call
|
|
445
790
|
if task is not None and task is not asyncio.current_task():
|
|
446
|
-
|
|
447
|
-
|
|
791
|
+
task.cancel()
|
|
792
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
793
|
+
await self._settle_retired_results()
|
|
794
|
+
|
|
795
|
+
async def _settle_retired_results(self):
|
|
796
|
+
for pending in tuple(self._retired_results):
|
|
797
|
+
handle = pending.handle
|
|
798
|
+
if handle is not None and not pending.settled:
|
|
799
|
+
await asyncio.gather(asyncio.shield(handle), return_exceptions=True)
|
|
800
|
+
while not pending.settled:
|
|
801
|
+
await asyncio.sleep(.005)
|
|
802
|
+
if not self._connection._oauth_enabled and pending.session_id:
|
|
803
|
+
self._result_session_id = pending.session_id
|
|
804
|
+
self._retired_results.remove(pending)
|
|
448
805
|
|
|
449
806
|
async def _clear(self, deadline, owned=False):
|
|
450
807
|
await self._stop_active()
|
|
451
808
|
if self._route is None:
|
|
452
809
|
if self._state == 'SUBMISSION_UNKNOWN' or self in self._connection._ambiguous_submissions:
|
|
453
810
|
raise AmbiguousSubmissionError()
|
|
811
|
+
self._reset_query_state()
|
|
454
812
|
return None
|
|
455
|
-
|
|
456
|
-
# Owned cleanup has no authority to mint a new legacy session silently.
|
|
457
|
-
session = '' if self._connection._oauth_enabled else self._connection._session_id
|
|
458
|
-
if session is None:
|
|
459
|
-
raise OperationalError('Cleanup has no authenticated legacy session.')
|
|
460
|
-
else:
|
|
461
|
-
session = await self._connection.get_session_id(deadline=deadline)
|
|
462
|
-
request = pb.ClearOrCancelQueryRequest(sessionId=session, queryId=self.query_id, engineIP=self._route.engine_ip)
|
|
813
|
+
request = await self._query_request(pb.ClearOrCancelQueryRequest, deadline, owned=owned)
|
|
463
814
|
response = await self._connection._rpc('clearOrCancelQuery', request, deadline=deadline,
|
|
464
815
|
route=self._route, _cleanup=owned)
|
|
465
816
|
self._connection._release_route(self.query_id)
|
|
466
817
|
self._connection._ambiguous_submissions.discard(self)
|
|
818
|
+
self._reset_query_state()
|
|
819
|
+
return response
|
|
820
|
+
|
|
821
|
+
def _reset_query_state(self):
|
|
467
822
|
self._route = None
|
|
468
823
|
self._rows.clear()
|
|
824
|
+
self._reset_result_protocol()
|
|
469
825
|
self._columns = self._description = None
|
|
470
826
|
self._rowcount, self._rownumber = -1, 0
|
|
471
827
|
self._failure = self._cleanup_error = None
|
|
472
828
|
if self._state != 'CLOSED':
|
|
473
829
|
self._state = 'EMPTY'
|
|
474
|
-
return response
|
|
475
830
|
|
|
476
831
|
async def clear(self, query_id=None, *, timeout=None):
|
|
477
832
|
self._check()
|
|
@@ -496,6 +851,8 @@ class AsyncCursor:
|
|
|
496
851
|
try:
|
|
497
852
|
async with asyncio.timeout_at(deadline):
|
|
498
853
|
await self._stop_active()
|
|
854
|
+
if self._result_batch_v2:
|
|
855
|
+
self._fail_result('cancelled_result')
|
|
499
856
|
request = await self._query_request(pb.CancelQueryRequest, deadline)
|
|
500
857
|
await self._connection._rpc('cancelQuery', request, deadline=deadline, route=self._route)
|
|
501
858
|
self._fail_result('cancelled_result')
|
|
@@ -505,7 +862,7 @@ class AsyncCursor:
|
|
|
505
862
|
raise _operational(error) from error
|
|
506
863
|
|
|
507
864
|
async def _close_owned(self, deadline):
|
|
508
|
-
if self._state == 'CLOSED':
|
|
865
|
+
if self._state == 'CLOSED' and self._route is None and not self._retired_results:
|
|
509
866
|
return
|
|
510
867
|
unknown_without_handle = self._state == 'SUBMISSION_UNKNOWN' and self._route is None
|
|
511
868
|
if unknown_without_handle:
|
|
@@ -524,7 +881,10 @@ class AsyncCursor:
|
|
|
524
881
|
self._cleanup_error = OperationalError('Query cleanup is unconfirmed.')
|
|
525
882
|
finally:
|
|
526
883
|
self._rows.clear()
|
|
527
|
-
self.
|
|
884
|
+
self._result_batches.clear()
|
|
885
|
+
if self._route is None and not self._retired_results:
|
|
886
|
+
self._result_session_id = None
|
|
887
|
+
self._connection._cursors.discard(self)
|
|
528
888
|
|
|
529
889
|
async def close(self):
|
|
530
890
|
if self._lease_guard is not None:
|
|
@@ -216,7 +216,8 @@ class E6dataAsyncDialect(E6dataDialect):
|
|
|
216
216
|
aliases = {'schema': 'database', 'cluster-name': 'cluster_name',
|
|
217
217
|
'cluster-uuid': 'cluster_name', 'auto-resume': 'auto_resume'}
|
|
218
218
|
strings = {'catalog', 'database', 'cluster_name', 'ssl_cert'}
|
|
219
|
-
booleans = {'secure', 'auto_resume', 'debug', 'require_fastbinary'
|
|
219
|
+
booleans = {'secure', 'auto_resume', 'debug', 'require_fastbinary',
|
|
220
|
+
'enable_result_batch_v2'}
|
|
220
221
|
floats = {'operation_timeout', 'cleanup_timeout', 'oauth_timeout', 'auto_resume_timeout'}
|
|
221
222
|
options = {}
|
|
222
223
|
if url.host is not None:
|