e6data-python-connector 3.0.1rc2__tar.gz → 3.0.1rc4__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.1rc2/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc4}/PKG-INFO +19 -6
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/README.md +18 -5
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cluster_manager.py +9 -4
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection.py +30 -8
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection_pool.py +2 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cursor.py +3 -2
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_manager.py +58 -9
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6data_grpc.py +10 -6
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode.py +155 -65
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4/e6data_python_connector.egg-info}/PKG-INFO +19 -6
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/setup.py +1 -1
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_auto_resume.py +7 -2
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/LICENSE +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/MANIFEST.in +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/aio.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_dialect.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_work.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/dialect.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_cache.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_common.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_batch.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode_worker.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_prefetch.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/pyproject.toml +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/setup.cfg +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_binary.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_fields.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/conftest.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/debug_binary.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/final_test.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_sync_async.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/expiry_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/result_batch_benchmark_support.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/support.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_connection.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_pool.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_queries.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_resume.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_sqlalchemy.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_token_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_auth_boundary.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_oauth_continuation.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_readonly_matrix.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_shared_token_issuance.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_sync_oauth_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/token_observation.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/move_tests.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/quick_test.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_dialect_columns.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_fix.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_known_case.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_mock_server.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_authentication.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_resume_integration.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/tests.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/tests_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/validate_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/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.1rc4
|
|
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.
|
|
@@ -489,11 +489,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
|
|
|
489
489
|
start method is unchanged. An unguarded main script can run its other import
|
|
490
490
|
side effects, so keep all application startup inside the guard.
|
|
491
491
|
|
|
492
|
-
Both APIs default to
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
492
|
+
Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
|
|
493
|
+
Explicit positive byte limits are still honored. For sync, set
|
|
494
|
+
`max_receive_message_length` and `max_send_message_length` in `grpc_options`.
|
|
495
|
+
For async, set the receive limit with `max_receive_message_bytes` or
|
|
496
|
+
`max_receive_message_length` in `grpc_options`; if both are supplied, they must
|
|
497
|
+
agree. Set `max_send_message_length` in `grpc_options` for the send limit.
|
|
498
|
+
The receive limit applies to each message after transport decompression, not the
|
|
499
|
+
total query result. Unlimited receiving does not limit client memory use;
|
|
500
|
+
continuous downloading can retain serialized batches while decoding catches up.
|
|
497
501
|
|
|
498
502
|
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
499
503
|
|
|
@@ -567,6 +571,15 @@ downloading is faster than decoding or application reads. Workers add Python
|
|
|
567
571
|
objects and process copies; the receive limit is not a limit on total memory.
|
|
568
572
|
`fetchall()` still retains the full result.
|
|
569
573
|
|
|
574
|
+
If a decoder worker exits or its local pipe breaks, the connector stops the
|
|
575
|
+
failed workers and decodes that complete serialized envelope once in the parent
|
|
576
|
+
process. It uses the same deadline and makes no extra result RPC. Continuous
|
|
577
|
+
downloading stays active and uncapped during this recovery. Later envelopes use
|
|
578
|
+
sequential decoding until the decoder runtime is replaced through its normal
|
|
579
|
+
lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
|
|
580
|
+
reported while decoding data are still failures. Their original cause is kept
|
|
581
|
+
on the first error delivered to the caller.
|
|
582
|
+
|
|
570
583
|
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
571
584
|
optimizations. Other RPC failures reach the caller as the original
|
|
572
585
|
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
@@ -443,11 +443,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
|
|
|
443
443
|
start method is unchanged. An unguarded main script can run its other import
|
|
444
444
|
side effects, so keep all application startup inside the guard.
|
|
445
445
|
|
|
446
|
-
Both APIs default to
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
|
|
447
|
+
Explicit positive byte limits are still honored. For sync, set
|
|
448
|
+
`max_receive_message_length` and `max_send_message_length` in `grpc_options`.
|
|
449
|
+
For async, set the receive limit with `max_receive_message_bytes` or
|
|
450
|
+
`max_receive_message_length` in `grpc_options`; if both are supplied, they must
|
|
451
|
+
agree. Set `max_send_message_length` in `grpc_options` for the send limit.
|
|
452
|
+
The receive limit applies to each message after transport decompression, not the
|
|
453
|
+
total query result. Unlimited receiving does not limit client memory use;
|
|
454
|
+
continuous downloading can retain serialized batches while decoding catches up.
|
|
451
455
|
|
|
452
456
|
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
453
457
|
|
|
@@ -521,6 +525,15 @@ downloading is faster than decoding or application reads. Workers add Python
|
|
|
521
525
|
objects and process copies; the receive limit is not a limit on total memory.
|
|
522
526
|
`fetchall()` still retains the full result.
|
|
523
527
|
|
|
528
|
+
If a decoder worker exits or its local pipe breaks, the connector stops the
|
|
529
|
+
failed workers and decodes that complete serialized envelope once in the parent
|
|
530
|
+
process. It uses the same deadline and makes no extra result RPC. Continuous
|
|
531
|
+
downloading stays active and uncapped during this recovery. Later envelopes use
|
|
532
|
+
sequential decoding until the decoder runtime is replaced through its normal
|
|
533
|
+
lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
|
|
534
|
+
reported while decoding data are still failures. Their original cause is kept
|
|
535
|
+
on the first error delivered to the caller.
|
|
536
|
+
|
|
524
537
|
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
525
538
|
optimizations. Other RPC failures reach the caller as the original
|
|
526
539
|
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
@@ -123,10 +123,15 @@ class AsyncClusterManager:
|
|
|
123
123
|
options = dict(grpc_options or {})
|
|
124
124
|
if any(key.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for key in options):
|
|
125
125
|
raise ValueError('TLS authority overrides are unsupported.')
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
for name in ('max_receive_message_length', 'max_send_message_length'):
|
|
127
|
+
limits = [options.pop(key) for key in (name, 'grpc.' + name) if key in options]
|
|
128
|
+
for limit in limits:
|
|
129
|
+
if (isinstance(limit, bool) or not isinstance(limit, int)
|
|
130
|
+
or (limit != -1 and limit <= 0)):
|
|
131
|
+
raise ValueError(name + ' must be -1 (unlimited) or a positive integer.')
|
|
132
|
+
if len(limits) == 2 and limits[0] != limits[1]:
|
|
133
|
+
raise ValueError('Conflicting ' + name + ' options.')
|
|
134
|
+
options['grpc.' + name] = limits[0] if limits else -1
|
|
130
135
|
self._host, self._port = host, port
|
|
131
136
|
self._user, self._password = user, password
|
|
132
137
|
self._secure_channel, self._ssl_cert = secure_channel, ssl_cert
|
|
@@ -14,6 +14,9 @@ from .oauth_common import validate_positive_timeout, validate_token_endpoint
|
|
|
14
14
|
from .server import e6x_engine_pb2 as pb, e6x_engine_pb2_grpc as bindings
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
_UNSET_RECEIVE_MESSAGE_BYTES = object()
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
@dataclass(frozen=True)
|
|
18
21
|
class QueryRoute:
|
|
19
22
|
target: str
|
|
@@ -50,7 +53,7 @@ class AsyncConnection:
|
|
|
50
53
|
token_url=None, oauth_scope=None, access_token=None,
|
|
51
54
|
client_auth_method='basic', *, operation_timeout=600.0,
|
|
52
55
|
oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
|
|
53
|
-
max_receive_message_bytes=
|
|
56
|
+
max_receive_message_bytes=_UNSET_RECEIVE_MESSAGE_BYTES,
|
|
54
57
|
enable_result_batch_v2=False):
|
|
55
58
|
from .result_decode_worker import is_decode_worker
|
|
56
59
|
if is_decode_worker():
|
|
@@ -98,14 +101,31 @@ class AsyncConnection:
|
|
|
98
101
|
for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
|
|
99
102
|
('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
|
|
100
103
|
settings[name] = validate_positive_timeout(value, name)
|
|
101
|
-
if isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int) or max_receive_message_bytes <= 0:
|
|
102
|
-
raise ValueError('max_receive_message_bytes must be a positive integer.')
|
|
103
104
|
options = dict(grpc_options or {})
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
receive_names = ('max_receive_message_length', 'grpc.max_receive_message_length')
|
|
106
|
+
receive_values = [options.pop(name) for name in receive_names if name in options]
|
|
107
|
+
for value in receive_values:
|
|
108
|
+
if (isinstance(value, bool) or not isinstance(value, int)
|
|
109
|
+
or (value != -1 and value <= 0)):
|
|
110
|
+
raise ValueError('max_receive_message_length must be -1 (unlimited) or a positive integer.')
|
|
111
|
+
if len(receive_values) == 2 and receive_values[0] != receive_values[1]:
|
|
112
|
+
raise ValueError('Conflicting max_receive_message_length options.')
|
|
113
|
+
if max_receive_message_bytes is _UNSET_RECEIVE_MESSAGE_BYTES:
|
|
114
|
+
max_receive_message_bytes = receive_values[0] if receive_values else -1
|
|
115
|
+
if (isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int)
|
|
116
|
+
or (max_receive_message_bytes != -1 and max_receive_message_bytes <= 0)):
|
|
117
|
+
raise ValueError('max_receive_message_bytes must be -1 (unlimited) or a positive integer.')
|
|
118
|
+
if receive_values and receive_values[0] != max_receive_message_bytes:
|
|
119
|
+
raise ValueError('Set the receive limit using max_receive_message_bytes; supplied options must agree.')
|
|
120
|
+
send_names = ('max_send_message_length', 'grpc.max_send_message_length')
|
|
121
|
+
send_values = [options.pop(name) for name in send_names if name in options]
|
|
122
|
+
for value in send_values:
|
|
123
|
+
if (isinstance(value, bool) or not isinstance(value, int)
|
|
124
|
+
or (value != -1 and value <= 0)):
|
|
125
|
+
raise ValueError('max_send_message_length must be -1 (unlimited) or a positive integer.')
|
|
126
|
+
if len(send_values) == 2 and send_values[0] != send_values[1]:
|
|
127
|
+
raise ValueError('Conflicting max_send_message_length options.')
|
|
128
|
+
options['grpc.max_send_message_length'] = send_values[0] if send_values else -1
|
|
109
129
|
# Authority overrides can disable meaningful server identity validation.
|
|
110
130
|
if any(name.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for name in options):
|
|
111
131
|
raise ValueError('TLS authority overrides are unsupported.')
|
|
@@ -385,6 +405,8 @@ class AsyncConnection:
|
|
|
385
405
|
|
|
386
406
|
self._cluster_manager = AsyncClusterManager(
|
|
387
407
|
self.host, self.port, user=self.username or '', password=self.password or '',
|
|
408
|
+
grpc_options={**self.grpc_options,
|
|
409
|
+
'grpc.max_receive_message_length': self.max_receive_message_bytes},
|
|
388
410
|
secure_channel=self.secure, cluster_uuid=self.cluster_name,
|
|
389
411
|
ssl_cert=self.ssl_cert, metadata_provider=metadata_provider if self._oauth_enabled else None,
|
|
390
412
|
initial_strategy=self.strategy, auto_resume_timeout=self.auto_resume_timeout,
|
|
@@ -189,6 +189,8 @@ class AsyncConnectionPool:
|
|
|
189
189
|
from .async_cluster_manager import AsyncClusterManager
|
|
190
190
|
self._cluster_manager = AsyncClusterManager(
|
|
191
191
|
config.host, config.port, user=config.username or '', password=config.password or '',
|
|
192
|
+
grpc_options={**config.grpc_options,
|
|
193
|
+
'grpc.max_receive_message_length': config.max_receive_message_bytes},
|
|
192
194
|
secure_channel=config.secure, ssl_cert=config.ssl_cert,
|
|
193
195
|
cluster_uuid=config.cluster_name, initial_strategy='blue',
|
|
194
196
|
metadata_provider=self._resume_metadata if config._oauth_enabled else None,
|
|
@@ -129,7 +129,7 @@ class AsyncCursor:
|
|
|
129
129
|
if self._state == 'CLOSED':
|
|
130
130
|
raise ProgrammingError('Cursor is closed.')
|
|
131
131
|
if fetch and self._failure is not None:
|
|
132
|
-
raise self._failure
|
|
132
|
+
raise IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
|
|
133
133
|
if fetch and self._state in ('EMPTY', 'SUBMISSION_UNKNOWN'):
|
|
134
134
|
raise ProgrammingError('No complete query is available for fetching.')
|
|
135
135
|
|
|
@@ -172,12 +172,13 @@ class AsyncCursor:
|
|
|
172
172
|
self._retire_prefetch()
|
|
173
173
|
self._cancel_decode()
|
|
174
174
|
if self._failure is None:
|
|
175
|
+
# Never raise this template: its traceback would retain decoded inputs.
|
|
175
176
|
self._failure = IncompleteResultError(reason, query_id=self.query_id)
|
|
176
177
|
if self._state != 'CLOSED':
|
|
177
178
|
self._state = 'RESULT_FAILED'
|
|
178
179
|
self._rows.clear()
|
|
179
180
|
self._result_batches.clear()
|
|
180
|
-
return self._failure
|
|
181
|
+
return IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
|
|
181
182
|
|
|
182
183
|
def _accept_rows(self, rows):
|
|
183
184
|
self._rows.extend(rows)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import hashlib
|
|
1
2
|
import logging
|
|
3
|
+
import os
|
|
2
4
|
import threading
|
|
3
5
|
import time
|
|
4
6
|
import e6data_python_connector.cluster_server.cluster_pb2 as cluster_pb2
|
|
@@ -74,6 +76,7 @@ class _StatusLock:
|
|
|
74
76
|
self._status_multiprocessing_lock = (threading.Semaphore() if is_decode_worker()
|
|
75
77
|
else multiprocessing.Semaphore())
|
|
76
78
|
self._is_active = False
|
|
79
|
+
self._resume_completion = None
|
|
77
80
|
|
|
78
81
|
@property
|
|
79
82
|
def is_active(self) -> bool:
|
|
@@ -109,8 +112,14 @@ class _StatusLock:
|
|
|
109
112
|
_StatusLock: The current instance of the lock, used for context management.
|
|
110
113
|
"""
|
|
111
114
|
|
|
112
|
-
self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
|
|
113
|
-
|
|
115
|
+
if not self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT):
|
|
116
|
+
raise TimeoutError('Cluster recovery timed out waiting for status lock.')
|
|
117
|
+
try:
|
|
118
|
+
if not self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT):
|
|
119
|
+
raise TimeoutError('Cluster recovery timed out waiting for process lock.')
|
|
120
|
+
except BaseException:
|
|
121
|
+
self._status_thread_lock.release()
|
|
122
|
+
raise
|
|
114
123
|
return self
|
|
115
124
|
|
|
116
125
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
@@ -127,8 +136,8 @@ class _StatusLock:
|
|
|
127
136
|
exc_tb (Traceback): The traceback object of the exception (if any).
|
|
128
137
|
"""
|
|
129
138
|
|
|
130
|
-
self._status_thread_lock.release()
|
|
131
139
|
self._status_multiprocessing_lock.release()
|
|
140
|
+
self._status_thread_lock.release()
|
|
132
141
|
|
|
133
142
|
@contextmanager
|
|
134
143
|
def hold_until(self, deadline):
|
|
@@ -196,9 +205,10 @@ class ClusterManager:
|
|
|
196
205
|
self._timeout = time.time() + timeout
|
|
197
206
|
self._secure_channel = secure_channel
|
|
198
207
|
self.cluster_uuid = cluster_uuid
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
options = dict(grpc_options or ())
|
|
209
|
+
options.setdefault('grpc.max_receive_message_length', -1)
|
|
210
|
+
options.setdefault('grpc.max_send_message_length', -1)
|
|
211
|
+
self._grpc_options = list(options.items())
|
|
202
212
|
self._debug = debug
|
|
203
213
|
self._ssl_cert = ssl_cert
|
|
204
214
|
self._metadata_provider = metadata_provider
|
|
@@ -465,6 +475,34 @@ class ClusterManager:
|
|
|
465
475
|
logger.warning(f"Status check failed with error: {e}")
|
|
466
476
|
yield None
|
|
467
477
|
|
|
478
|
+
def _legacy_resume_identity(self):
|
|
479
|
+
"""Fingerprint exact connection inputs without retaining credentials.
|
|
480
|
+
|
|
481
|
+
Copy standard option containers. Unknown or mutable option values disable
|
|
482
|
+
sharing so their existing RPC behavior is preserved without guessing.
|
|
483
|
+
"""
|
|
484
|
+
values = (self._host, self._port, self.cluster_uuid, self._secure_channel,
|
|
485
|
+
self._ssl_cert, self._user, self._password)
|
|
486
|
+
scalar_types = (type(None), str, bytes, bool, int)
|
|
487
|
+
if any(type(value) not in scalar_types for value in values):
|
|
488
|
+
return None
|
|
489
|
+
options = self._grpc_options
|
|
490
|
+
if type(options) is dict:
|
|
491
|
+
pairs = tuple(options.items())
|
|
492
|
+
elif type(options) in (list, tuple):
|
|
493
|
+
pairs = tuple(options)
|
|
494
|
+
else:
|
|
495
|
+
return None
|
|
496
|
+
frozen_options = []
|
|
497
|
+
for pair in pairs:
|
|
498
|
+
if (type(pair) not in (tuple, list) or len(pair) != 2
|
|
499
|
+
or any(type(value) not in scalar_types for value in pair)):
|
|
500
|
+
return None
|
|
501
|
+
frozen_options.append(tuple((type(value).__name__, value) for value in pair))
|
|
502
|
+
identity = (tuple((type(value).__name__, value) for value in values),
|
|
503
|
+
type(options).__name__, tuple(frozen_options))
|
|
504
|
+
return hashlib.sha256(repr(identity).encode('utf-8')).digest()
|
|
505
|
+
|
|
468
506
|
def resume(self) -> bool:
|
|
469
507
|
"""
|
|
470
508
|
Resumes the cluster if it is currently suspended or not in the 'active' state.
|
|
@@ -494,14 +532,21 @@ class ClusterManager:
|
|
|
494
532
|
"""
|
|
495
533
|
if self._metadata_provider is not None:
|
|
496
534
|
return self._resume_oauth()
|
|
535
|
+
call_started = time.monotonic()
|
|
497
536
|
if self._debug:
|
|
498
537
|
logger.info(f"Starting auto-resume for cluster {self.cluster_uuid} at {self._host}:{self._port}")
|
|
499
538
|
|
|
500
539
|
with status_lock as lock:
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
540
|
+
identity = self._legacy_resume_identity()
|
|
541
|
+
process_id = os.getpid()
|
|
542
|
+
completion = lock._resume_completion
|
|
543
|
+
if (identity is not None and completion is not None
|
|
544
|
+
and completion[0] == process_id and completion[1] == identity
|
|
545
|
+
and completion[2] > call_started):
|
|
504
546
|
return True
|
|
547
|
+
# A fresh attempt can observe a new failure before older waiters run.
|
|
548
|
+
# Invalidate first, including when the attempt later raises an error.
|
|
549
|
+
lock._resume_completion = None
|
|
505
550
|
|
|
506
551
|
# Retrieve the current cluster status with strategy header
|
|
507
552
|
if self._debug:
|
|
@@ -530,6 +575,8 @@ class ClusterManager:
|
|
|
530
575
|
elif current_status.status == 'active':
|
|
531
576
|
if self._debug:
|
|
532
577
|
logger.info("Cluster is already active, no action needed")
|
|
578
|
+
if identity is not None:
|
|
579
|
+
lock._resume_completion = (process_id, identity, time.monotonic())
|
|
533
580
|
return True
|
|
534
581
|
elif current_status.status != 'resuming':
|
|
535
582
|
"""
|
|
@@ -548,6 +595,8 @@ class ClusterManager:
|
|
|
548
595
|
if status == 'active':
|
|
549
596
|
if self._debug:
|
|
550
597
|
logger.info(f"Cluster became active after {check_count} status checks")
|
|
598
|
+
if identity is not None:
|
|
599
|
+
lock._resume_completion = (process_id, identity, time.monotonic())
|
|
551
600
|
return True
|
|
552
601
|
elif status == 'failed':
|
|
553
602
|
if self._debug:
|
|
@@ -527,9 +527,9 @@ class Connection(object):
|
|
|
527
527
|
if key in normalized and normalized[key] != value:
|
|
528
528
|
raise ValueError('Conflicting gRPC options after prefix normalization.')
|
|
529
529
|
normalized[key] = value
|
|
530
|
-
limit = normalized.get('max_receive_message_length',
|
|
531
|
-
if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
|
|
532
|
-
raise ValueError('V2 receive limit must be a
|
|
530
|
+
limit = normalized.get('max_receive_message_length', -1)
|
|
531
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or (limit != -1 and limit <= 0):
|
|
532
|
+
raise ValueError('V2 receive limit must be a positive integer or -1 for unlimited.')
|
|
533
533
|
normalized['max_receive_message_length'] = limit
|
|
534
534
|
grpc_options = normalized
|
|
535
535
|
if enable_result_batch_v2 and grpc_options is not None and 'grpc_prepare_timeout' in grpc_options:
|
|
@@ -696,7 +696,7 @@ class Connection(object):
|
|
|
696
696
|
default_options = {
|
|
697
697
|
"keepalive_timeout_ms": 900000, # Time in milliseconds to keep the connection alive.
|
|
698
698
|
"max_receive_message_length": -1, # Maximum size of received messages.
|
|
699
|
-
"max_send_message_length":
|
|
699
|
+
"max_send_message_length": -1, # Unlimited sent message size.
|
|
700
700
|
"grpc_prepare_timeout": self.grpc_prepare_timeout, # Timeout for prepare statement API call.
|
|
701
701
|
"keepalive_time_ms": 30000, # Time in milliseconds between keep-alive pings.
|
|
702
702
|
"keepalive_permit_without_calls": 1, # Allow keep-alives with no active RPCs.
|
|
@@ -1032,6 +1032,7 @@ class Connection(object):
|
|
|
1032
1032
|
cluster_uuid=self.cluster_name,
|
|
1033
1033
|
timeout=self.grpc_auto_resume_timeout_seconds,
|
|
1034
1034
|
debug=self._debug,
|
|
1035
|
+
grpc_options=self._get_grpc_options,
|
|
1035
1036
|
ssl_cert=self._ssl_cert
|
|
1036
1037
|
).resume()
|
|
1037
1038
|
return status # Return boolean status directly
|
|
@@ -1709,7 +1710,9 @@ class Cursor(DBAPICursor):
|
|
|
1709
1710
|
self._result_failure = IncompleteResultError(reason, query_id=self._query_id)
|
|
1710
1711
|
self._data = None
|
|
1711
1712
|
self._result_batches.clear()
|
|
1712
|
-
|
|
1713
|
+
# Keep the cached template free of tracebacks and response frames.
|
|
1714
|
+
return IncompleteResultError(
|
|
1715
|
+
self._result_failure.reason, query_id=self._result_failure.query_id)
|
|
1713
1716
|
|
|
1714
1717
|
def _check_result(self):
|
|
1715
1718
|
if self._result_batch_v2_enabled and (self._result_pid != os.getpid() or
|
|
@@ -1720,7 +1723,8 @@ class Cursor(DBAPICursor):
|
|
|
1720
1723
|
if self._closed:
|
|
1721
1724
|
raise ProgrammingError('Cursor is closed.')
|
|
1722
1725
|
if self._result_failure is not None:
|
|
1723
|
-
raise
|
|
1726
|
+
raise IncompleteResultError(
|
|
1727
|
+
self._result_failure.reason, query_id=self._result_failure.query_id)
|
|
1724
1728
|
|
|
1725
1729
|
@property
|
|
1726
1730
|
def query_id(self):
|