e6data-python-connector 3.0.1rc1__tar.gz → 3.0.1rc2__tar.gz
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-3.0.1rc1/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc2}/PKG-INFO +113 -63
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/README.md +112 -62
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection.py +47 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection_pool.py +2 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cursor.py +234 -30
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_manager.py +3 -1
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/connection_pool.py +11 -9
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6data_grpc.py +482 -63
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode.py +469 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode_worker.py +50 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_prefetch.py +421 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2/e6data_python_connector.egg-info}/PKG-INFO +113 -63
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/SOURCES.txt +3 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/setup.py +1 -1
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool.py +3 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/LICENSE +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/MANIFEST.in +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/aio.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cluster_manager.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_dialect.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_work.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/dialect.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_cache.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_common.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/result_batch.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/pyproject.toml +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/setup.cfg +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_binary.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/analyze_fields.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/conftest.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/debug_binary.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/final_test.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_sync_async.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/expiry_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/result_batch_benchmark_support.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/support.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_connection.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_pool.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_queries.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_resume.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_sqlalchemy.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_token_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_auth_boundary.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_oauth_continuation.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_readonly_matrix.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_shared_token_issuance.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/test_sync_oauth_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/integration/token_observation.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/move_tests.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/quick_test.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_dialect_columns.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_fix.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_known_case.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_mock_server.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_authentication.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_auto_resume.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_oauth_resume_integration.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/tests.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/tests_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/validate_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc1 → e6data_python_connector-3.0.1rc2}/test/verify_decimal_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1rc2
|
|
4
4
|
Summary: Client for the e6data distributed SQL Engine.
|
|
5
5
|
Home-page: https://github.com/e6x-labs/e6data-python-connector
|
|
6
6
|
Author: e6data, Inc.
|
|
@@ -471,85 +471,119 @@ cursor = conn.cursor(database, catalog_name)
|
|
|
471
471
|
|
|
472
472
|
### Opt in to multi-chunk result batches
|
|
473
473
|
|
|
474
|
-
`enable_result_batch_v2=True`
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
response size.
|
|
474
|
+
Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
|
|
475
|
+
and bounded parallel chunk decoding together. It defaults to `False`. There
|
|
476
|
+
are no separate public prefetch or decoding switches. `arraysize` and
|
|
477
|
+
`fetchmany(size)` only change the number of rows returned to the application.
|
|
479
478
|
|
|
480
479
|
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
481
|
-
`ENABLE_GET_NEXT_CHUNK_V2` controls
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
480
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
|
|
481
|
+
These flags do not extend the planner's query lifetime. Finishing a large result
|
|
482
|
+
within 900 seconds still needs a measurement on the target deployment.
|
|
483
|
+
|
|
484
|
+
The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
|
|
485
|
+
an import-safe application entry point. Put application startup inside
|
|
486
|
+
`if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
|
|
487
|
+
daemon and frozen applications are rejected before query submission. Two
|
|
488
|
+
workers use the explicit `spawn` method; the application's global multiprocessing
|
|
489
|
+
start method is unchanged. An unguarded main script can run its other import
|
|
490
|
+
side effects, so keep all application startup inside the guard.
|
|
491
|
+
|
|
492
|
+
Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
|
|
493
|
+
custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
|
|
494
|
+
flag-off path keeps its existing options. For async, set
|
|
495
|
+
`max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
|
|
496
|
+
Choose limits from measured envelope sizes and the client's memory budget.
|
|
497
|
+
|
|
498
|
+
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
491
499
|
|
|
492
500
|
```python
|
|
493
501
|
from e6data_python_connector import Connection
|
|
494
502
|
|
|
495
|
-
sync_options = {
|
|
496
|
-
**connection_options,
|
|
497
|
-
"enable_result_batch_v2": True,
|
|
498
|
-
"grpc_options": {
|
|
499
|
-
**connection_options.get("grpc_options", {}),
|
|
500
|
-
"max_receive_message_length": result_receive_limit_bytes,
|
|
501
|
-
},
|
|
502
|
-
}
|
|
503
|
-
with Connection(**sync_options) as connection:
|
|
504
|
-
with connection.cursor() as cursor:
|
|
505
|
-
cursor.execute(sql)
|
|
506
|
-
for rows in cursor.fetchall_buffer():
|
|
507
|
-
consume(rows)
|
|
508
|
-
```
|
|
509
503
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
504
|
+
def main():
|
|
505
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
506
|
+
with Connection(**options) as connection:
|
|
507
|
+
with connection.cursor() as cursor:
|
|
508
|
+
cursor.execute(sql)
|
|
509
|
+
for rows in cursor.fetchall_buffer():
|
|
510
|
+
consume(rows)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
main()
|
|
515
|
+
```
|
|
513
516
|
|
|
514
517
|
```python
|
|
518
|
+
import asyncio
|
|
515
519
|
from e6data_python_connector.aio import AsyncConnection
|
|
516
520
|
|
|
517
|
-
async_options = {
|
|
518
|
-
**connection_options,
|
|
519
|
-
"enable_result_batch_v2": True,
|
|
520
|
-
"max_receive_message_bytes": result_receive_limit_bytes,
|
|
521
|
-
}
|
|
522
|
-
async with AsyncConnection(**async_options) as connection:
|
|
523
|
-
async with connection.cursor() as cursor:
|
|
524
|
-
await cursor.execute(sql)
|
|
525
|
-
async for rows in cursor.fetchall_buffer():
|
|
526
|
-
consume(rows)
|
|
527
|
-
```
|
|
528
521
|
|
|
529
|
-
|
|
522
|
+
async def main():
|
|
523
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
524
|
+
async with AsyncConnection(**options) as connection:
|
|
525
|
+
async with connection.cursor() as cursor:
|
|
526
|
+
await cursor.execute(sql)
|
|
527
|
+
async for rows in cursor.fetchall_buffer():
|
|
528
|
+
consume(rows)
|
|
530
529
|
|
|
531
|
-
```python
|
|
532
|
-
from sqlalchemy import create_engine
|
|
533
|
-
from sqlalchemy.ext.asyncio import create_async_engine
|
|
534
530
|
|
|
535
|
-
|
|
536
|
-
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
asyncio.run(main())
|
|
537
533
|
```
|
|
538
534
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
envelope are decoded before any are exposed, so V2 may use more memory and delay
|
|
542
|
-
the first chunk compared with V1. `fetchall()` still retains the full result.
|
|
543
|
-
There is no prefetch or parallel fetching for a query.
|
|
535
|
+
SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
|
|
536
|
+
inside the guarded application entry point:
|
|
544
537
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
538
|
+
```python
|
|
539
|
+
connect_args = {**connection_options, "enable_result_batch_v2": True}
|
|
540
|
+
engine = create_engine("e6data://", connect_args=connect_args)
|
|
541
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
|
|
542
|
+
```
|
|
548
543
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
544
|
+
After the first V2 response, a download thread (sync) or task (async) keeps
|
|
545
|
+
fetching serialized batches until the engine returns end-of-stream. Decoding
|
|
546
|
+
runs concurrently and takes batches from the queue in order. Downloading does
|
|
547
|
+
not wait for decoding or application row processing. There is no queue-size
|
|
548
|
+
limit and no process-wide limit on active downloaders. Result RPCs for the same
|
|
549
|
+
query never overlap.
|
|
550
|
+
|
|
551
|
+
Each background RPC gets a fresh configured transport budget when it starts;
|
|
552
|
+
that deadline never extends after dispatch. An empty nonterminal response uses
|
|
553
|
+
backoff with a finite no-progress deadline. Completed responses can wait while
|
|
554
|
+
the application is idle. Public fetch deadlines still cover waiting and decoding,
|
|
555
|
+
and expiry or cancellation stops the downloader. Async `fetchall` keeps one
|
|
556
|
+
deadline for its entire operation.
|
|
557
|
+
|
|
558
|
+
Two shared worker processes can decode one multi-chunk envelope at a time.
|
|
559
|
+
Other envelopes use the sequential path. Workers receive chunk bytes and column
|
|
560
|
+
positions, not connections or credentials. Process startup and copying add cost,
|
|
561
|
+
so parallel decoding is not a promise of better performance for every result.
|
|
562
|
+
|
|
563
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
|
|
564
|
+
original decoded chunks. All chunks in an envelope must decode successfully
|
|
565
|
+
before any are exposed. The serialized queue can hold the entire result when
|
|
566
|
+
downloading is faster than decoding or application reads. Workers add Python
|
|
567
|
+
objects and process copies; the receive limit is not a limit on total memory.
|
|
568
|
+
`fetchall()` still retains the full result.
|
|
569
|
+
|
|
570
|
+
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
571
|
+
optimizations. Other RPC failures reach the caller as the original
|
|
572
|
+
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
573
|
+
its envelope is needed, after current valid chunks have been consumed. Failed
|
|
574
|
+
or cancelled queries are not replayed. Later fetches on an incomplete result
|
|
575
|
+
raise `IncompleteResultError`. Clear, cancel, close and pool return retire
|
|
576
|
+
pending work; unsafe connections must not be reused.
|
|
577
|
+
|
|
578
|
+
Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
|
|
579
|
+
Protobuf bytes are uncompressed payload size, not bytes sent over the network.
|
|
580
|
+
|
|
581
|
+
For rollback, close old cursors and connections, then omit
|
|
582
|
+
`enable_result_batch_v2` or set it to `False` on new connections. Closing the
|
|
583
|
+
last V2 connection also stops its shared workers. See the
|
|
584
|
+
[qualification instructions](test/README.md#result-batch-v2-qualification).
|
|
585
|
+
Local synthetic results do not prove customer performance or the 900-second
|
|
586
|
+
query requirement.
|
|
553
587
|
|
|
554
588
|
### Get Query Time Metrics
|
|
555
589
|
```python
|
|
@@ -996,11 +1030,27 @@ conn = Connection(
|
|
|
996
1030
|
```
|
|
997
1031
|
|
|
998
1032
|
When `debug=True`, the following features are enabled:
|
|
999
|
-
-
|
|
1033
|
+
- Connector logging at DEBUG level, for both sync and async connections. Existing
|
|
1034
|
+
application logging handlers and the root logger level are kept.
|
|
1000
1035
|
- Blue-green strategy transition logging
|
|
1001
1036
|
- Connection lifecycle logging
|
|
1002
1037
|
- Query execution detailed logging
|
|
1003
1038
|
|
|
1039
|
+
Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
|
|
1040
|
+
`decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
|
|
1041
|
+
are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
|
|
1042
|
+
response. `decode_seconds` measures client response processing. `serialized_bytes`
|
|
1043
|
+
is the protobuf response size, not the number of bytes on the network. A failed
|
|
1044
|
+
attempt reports zero response size and chunk count. The same values remain available
|
|
1045
|
+
as `result_batch_*` attributes for structured log handlers.
|
|
1046
|
+
|
|
1047
|
+
These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
|
|
1048
|
+
to logs. `debug=False` keeps the normal quiet default. Applications can also enable
|
|
1049
|
+
DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
|
|
1050
|
+
stream handler only when no existing handler in its logger path accepts DEBUG.
|
|
1051
|
+
As with normal Python logging, enabling the package logger also affects other open
|
|
1052
|
+
connector connections in that process.
|
|
1053
|
+
|
|
1004
1054
|
### gRPC Network Tracing
|
|
1005
1055
|
|
|
1006
1056
|
For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
|
|
@@ -425,85 +425,119 @@ cursor = conn.cursor(database, catalog_name)
|
|
|
425
425
|
|
|
426
426
|
### Opt in to multi-chunk result batches
|
|
427
427
|
|
|
428
|
-
`enable_result_batch_v2=True`
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
response size.
|
|
428
|
+
Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
|
|
429
|
+
and bounded parallel chunk decoding together. It defaults to `False`. There
|
|
430
|
+
are no separate public prefetch or decoding switches. `arraysize` and
|
|
431
|
+
`fetchmany(size)` only change the number of rows returned to the application.
|
|
433
432
|
|
|
434
433
|
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
435
|
-
`ENABLE_GET_NEXT_CHUNK_V2` controls
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
434
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
|
|
435
|
+
These flags do not extend the planner's query lifetime. Finishing a large result
|
|
436
|
+
within 900 seconds still needs a measurement on the target deployment.
|
|
437
|
+
|
|
438
|
+
The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
|
|
439
|
+
an import-safe application entry point. Put application startup inside
|
|
440
|
+
`if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
|
|
441
|
+
daemon and frozen applications are rejected before query submission. Two
|
|
442
|
+
workers use the explicit `spawn` method; the application's global multiprocessing
|
|
443
|
+
start method is unchanged. An unguarded main script can run its other import
|
|
444
|
+
side effects, so keep all application startup inside the guard.
|
|
445
|
+
|
|
446
|
+
Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
|
|
447
|
+
custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
|
|
448
|
+
flag-off path keeps its existing options. For async, set
|
|
449
|
+
`max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
|
|
450
|
+
Choose limits from measured envelope sizes and the client's memory budget.
|
|
451
|
+
|
|
452
|
+
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
445
453
|
|
|
446
454
|
```python
|
|
447
455
|
from e6data_python_connector import Connection
|
|
448
456
|
|
|
449
|
-
sync_options = {
|
|
450
|
-
**connection_options,
|
|
451
|
-
"enable_result_batch_v2": True,
|
|
452
|
-
"grpc_options": {
|
|
453
|
-
**connection_options.get("grpc_options", {}),
|
|
454
|
-
"max_receive_message_length": result_receive_limit_bytes,
|
|
455
|
-
},
|
|
456
|
-
}
|
|
457
|
-
with Connection(**sync_options) as connection:
|
|
458
|
-
with connection.cursor() as cursor:
|
|
459
|
-
cursor.execute(sql)
|
|
460
|
-
for rows in cursor.fetchall_buffer():
|
|
461
|
-
consume(rows)
|
|
462
|
-
```
|
|
463
457
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
458
|
+
def main():
|
|
459
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
460
|
+
with Connection(**options) as connection:
|
|
461
|
+
with connection.cursor() as cursor:
|
|
462
|
+
cursor.execute(sql)
|
|
463
|
+
for rows in cursor.fetchall_buffer():
|
|
464
|
+
consume(rows)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
if __name__ == "__main__":
|
|
468
|
+
main()
|
|
469
|
+
```
|
|
467
470
|
|
|
468
471
|
```python
|
|
472
|
+
import asyncio
|
|
469
473
|
from e6data_python_connector.aio import AsyncConnection
|
|
470
474
|
|
|
471
|
-
async_options = {
|
|
472
|
-
**connection_options,
|
|
473
|
-
"enable_result_batch_v2": True,
|
|
474
|
-
"max_receive_message_bytes": result_receive_limit_bytes,
|
|
475
|
-
}
|
|
476
|
-
async with AsyncConnection(**async_options) as connection:
|
|
477
|
-
async with connection.cursor() as cursor:
|
|
478
|
-
await cursor.execute(sql)
|
|
479
|
-
async for rows in cursor.fetchall_buffer():
|
|
480
|
-
consume(rows)
|
|
481
|
-
```
|
|
482
475
|
|
|
483
|
-
|
|
476
|
+
async def main():
|
|
477
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
478
|
+
async with AsyncConnection(**options) as connection:
|
|
479
|
+
async with connection.cursor() as cursor:
|
|
480
|
+
await cursor.execute(sql)
|
|
481
|
+
async for rows in cursor.fetchall_buffer():
|
|
482
|
+
consume(rows)
|
|
484
483
|
|
|
485
|
-
```python
|
|
486
|
-
from sqlalchemy import create_engine
|
|
487
|
-
from sqlalchemy.ext.asyncio import create_async_engine
|
|
488
484
|
|
|
489
|
-
|
|
490
|
-
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
asyncio.run(main())
|
|
491
487
|
```
|
|
492
488
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
envelope are decoded before any are exposed, so V2 may use more memory and delay
|
|
496
|
-
the first chunk compared with V1. `fetchall()` still retains the full result.
|
|
497
|
-
There is no prefetch or parallel fetching for a query.
|
|
489
|
+
SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
|
|
490
|
+
inside the guarded application entry point:
|
|
498
491
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
492
|
+
```python
|
|
493
|
+
connect_args = {**connection_options, "enable_result_batch_v2": True}
|
|
494
|
+
engine = create_engine("e6data://", connect_args=connect_args)
|
|
495
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
|
|
496
|
+
```
|
|
502
497
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
498
|
+
After the first V2 response, a download thread (sync) or task (async) keeps
|
|
499
|
+
fetching serialized batches until the engine returns end-of-stream. Decoding
|
|
500
|
+
runs concurrently and takes batches from the queue in order. Downloading does
|
|
501
|
+
not wait for decoding or application row processing. There is no queue-size
|
|
502
|
+
limit and no process-wide limit on active downloaders. Result RPCs for the same
|
|
503
|
+
query never overlap.
|
|
504
|
+
|
|
505
|
+
Each background RPC gets a fresh configured transport budget when it starts;
|
|
506
|
+
that deadline never extends after dispatch. An empty nonterminal response uses
|
|
507
|
+
backoff with a finite no-progress deadline. Completed responses can wait while
|
|
508
|
+
the application is idle. Public fetch deadlines still cover waiting and decoding,
|
|
509
|
+
and expiry or cancellation stops the downloader. Async `fetchall` keeps one
|
|
510
|
+
deadline for its entire operation.
|
|
511
|
+
|
|
512
|
+
Two shared worker processes can decode one multi-chunk envelope at a time.
|
|
513
|
+
Other envelopes use the sequential path. Workers receive chunk bytes and column
|
|
514
|
+
positions, not connections or credentials. Process startup and copying add cost,
|
|
515
|
+
so parallel decoding is not a promise of better performance for every result.
|
|
516
|
+
|
|
517
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
|
|
518
|
+
original decoded chunks. All chunks in an envelope must decode successfully
|
|
519
|
+
before any are exposed. The serialized queue can hold the entire result when
|
|
520
|
+
downloading is faster than decoding or application reads. Workers add Python
|
|
521
|
+
objects and process copies; the receive limit is not a limit on total memory.
|
|
522
|
+
`fetchall()` still retains the full result.
|
|
523
|
+
|
|
524
|
+
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
525
|
+
optimizations. Other RPC failures reach the caller as the original
|
|
526
|
+
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
527
|
+
its envelope is needed, after current valid chunks have been consumed. Failed
|
|
528
|
+
or cancelled queries are not replayed. Later fetches on an incomplete result
|
|
529
|
+
raise `IncompleteResultError`. Clear, cancel, close and pool return retire
|
|
530
|
+
pending work; unsafe connections must not be reused.
|
|
531
|
+
|
|
532
|
+
Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
|
|
533
|
+
Protobuf bytes are uncompressed payload size, not bytes sent over the network.
|
|
534
|
+
|
|
535
|
+
For rollback, close old cursors and connections, then omit
|
|
536
|
+
`enable_result_batch_v2` or set it to `False` on new connections. Closing the
|
|
537
|
+
last V2 connection also stops its shared workers. See the
|
|
538
|
+
[qualification instructions](test/README.md#result-batch-v2-qualification).
|
|
539
|
+
Local synthetic results do not prove customer performance or the 900-second
|
|
540
|
+
query requirement.
|
|
507
541
|
|
|
508
542
|
### Get Query Time Metrics
|
|
509
543
|
```python
|
|
@@ -950,11 +984,27 @@ conn = Connection(
|
|
|
950
984
|
```
|
|
951
985
|
|
|
952
986
|
When `debug=True`, the following features are enabled:
|
|
953
|
-
-
|
|
987
|
+
- Connector logging at DEBUG level, for both sync and async connections. Existing
|
|
988
|
+
application logging handlers and the root logger level are kept.
|
|
954
989
|
- Blue-green strategy transition logging
|
|
955
990
|
- Connection lifecycle logging
|
|
956
991
|
- Query execution detailed logging
|
|
957
992
|
|
|
993
|
+
Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
|
|
994
|
+
`decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
|
|
995
|
+
are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
|
|
996
|
+
response. `decode_seconds` measures client response processing. `serialized_bytes`
|
|
997
|
+
is the protobuf response size, not the number of bytes on the network. A failed
|
|
998
|
+
attempt reports zero response size and chunk count. The same values remain available
|
|
999
|
+
as `result_batch_*` attributes for structured log handlers.
|
|
1000
|
+
|
|
1001
|
+
These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
|
|
1002
|
+
to logs. `debug=False` keeps the normal quiet default. Applications can also enable
|
|
1003
|
+
DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
|
|
1004
|
+
stream handler only when no existing handler in its logger path accepts DEBUG.
|
|
1005
|
+
As with normal Python logging, enabling the package logger also affects other open
|
|
1006
|
+
connector connections in that process.
|
|
1007
|
+
|
|
958
1008
|
### gRPC Network Tracing
|
|
959
1009
|
|
|
960
1010
|
For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
|
|
@@ -52,6 +52,9 @@ class AsyncConnection:
|
|
|
52
52
|
oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
|
|
53
53
|
max_receive_message_bytes=64 * 1024 * 1024,
|
|
54
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.')
|
|
55
58
|
if sys.version_info < (3, 11):
|
|
56
59
|
raise RuntimeError('The async API requires Python 3.11 or newer.')
|
|
57
60
|
try:
|
|
@@ -81,6 +84,9 @@ class AsyncConnection:
|
|
|
81
84
|
raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
|
|
82
85
|
if not isinstance(enable_result_batch_v2, bool):
|
|
83
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()
|
|
84
90
|
settings = dict(host=host, port=port, username=username, password=password,
|
|
85
91
|
catalog=catalog, database=database, cluster_name=cluster_name,
|
|
86
92
|
secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
|
|
@@ -107,6 +113,9 @@ class AsyncConnection:
|
|
|
107
113
|
settings['grpc_options'] = MappingProxyType(options)
|
|
108
114
|
settings['max_receive_message_bytes'] = max_receive_message_bytes
|
|
109
115
|
self._config = MappingProxyType(settings)
|
|
116
|
+
if debug:
|
|
117
|
+
from .e6data_grpc import _configure_debug_logging
|
|
118
|
+
_configure_debug_logging()
|
|
110
119
|
self._oauth_enabled = not modes[0]
|
|
111
120
|
self._channel = self._client = self._token_provider = None
|
|
112
121
|
self._owns_token_provider = True
|
|
@@ -129,6 +138,11 @@ class AsyncConnection:
|
|
|
129
138
|
self._cleanup_error = None
|
|
130
139
|
self._cluster_manager = None
|
|
131
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()
|
|
132
146
|
|
|
133
147
|
def __getattr__(self, name):
|
|
134
148
|
config = self.__dict__.get('_config', {})
|
|
@@ -304,6 +318,24 @@ class AsyncConnection:
|
|
|
304
318
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
305
319
|
return metadata
|
|
306
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
|
+
|
|
307
339
|
async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
|
|
308
340
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
309
341
|
refreshed = switched = False
|
|
@@ -504,6 +536,15 @@ class AsyncConnection:
|
|
|
504
536
|
# Native channel close with no grace cancels transports without waiting.
|
|
505
537
|
if self._channel is not None:
|
|
506
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.')
|
|
507
548
|
for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
|
|
508
549
|
(self._token_provider, self._owns_token_provider)):
|
|
509
550
|
if owned and resource is not None:
|
|
@@ -542,6 +583,12 @@ class AsyncConnection:
|
|
|
542
583
|
if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
|
|
543
584
|
raise ProgrammingError('Connection belongs to another owner.')
|
|
544
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
|
|
545
592
|
self._state = 'new'
|
|
546
593
|
self._session_id = self._session_task = self._close_task = self._open_task = None
|
|
547
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):
|