e6data-python-connector 3.0.1rc3__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.1rc3/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc4}/PKG-INFO +19 -6
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/README.md +18 -5
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cluster_manager.py +9 -4
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection.py +30 -8
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection_pool.py +2 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cursor.py +3 -2
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_manager.py +4 -3
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6data_grpc.py +10 -6
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode.py +155 -65
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4/e6data_python_connector.egg-info}/PKG-INFO +19 -6
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/setup.py +1 -1
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_auto_resume.py +7 -2
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/LICENSE +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/MANIFEST.in +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/aio.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_dialect.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_work.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/dialect.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_cache.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_common.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_batch.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode_worker.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_prefetch.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/pyproject.toml +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/setup.cfg +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_binary.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_fields.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/conftest.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/debug_binary.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/final_test.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/__init__.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_sync_async.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/expiry_preflight.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/result_batch_benchmark_support.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/support.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_connection.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_pool.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_queries.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_resume.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_sqlalchemy.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_token_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_auth_boundary.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_oauth_continuation.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_readonly_matrix.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_result_batch_v2.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_shared_token_issuance.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_sync_oauth_lifecycle.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/token_observation.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/move_tests.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/quick_test.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_38_nines.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_oauth.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_dialect_columns.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_fix.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_known_case.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_mock_server.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_authentication.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_resume_integration.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/tests.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/tests_grpc.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/validate_implementation.py +0 -0
- {e6data_python_connector-3.0.1rc3 → 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)
|
|
@@ -205,9 +205,10 @@ class ClusterManager:
|
|
|
205
205
|
self._timeout = time.time() + timeout
|
|
206
206
|
self._secure_channel = secure_channel
|
|
207
207
|
self.cluster_uuid = cluster_uuid
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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())
|
|
211
212
|
self._debug = debug
|
|
212
213
|
self._ssl_cert = ssl_cert
|
|
213
214
|
self._metadata_provider = metadata_provider
|
|
@@ -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):
|
|
@@ -32,6 +32,32 @@ class ResultDecodeError(ValueError):
|
|
|
32
32
|
"""A worker failed before a complete envelope could be published."""
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
class _WorkerTransportError(ResultDecodeError):
|
|
36
|
+
"""Only worker exit and pipe I/O failures permit a local sequential retry."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, stage, index, pid, exitcode=None, exception_type=None):
|
|
39
|
+
self.stage = stage
|
|
40
|
+
self.index = index
|
|
41
|
+
self.pid = pid
|
|
42
|
+
self.exitcode = exitcode
|
|
43
|
+
self.exception_type = exception_type
|
|
44
|
+
super().__init__(
|
|
45
|
+
'Decode worker unavailable: stage=%s worker=%s pid=%s exitcode=%s error_type=%s.'
|
|
46
|
+
% (stage, index, pid, exitcode, exception_type))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _detached_error(error):
|
|
50
|
+
"""Keep safe failure metadata without retaining result-bearing tracebacks."""
|
|
51
|
+
if isinstance(error, _WorkerTransportError):
|
|
52
|
+
return _WorkerTransportError(error.stage, error.index, error.pid,
|
|
53
|
+
error.exitcode, error.exception_type)
|
|
54
|
+
if isinstance(error, TimeoutError):
|
|
55
|
+
return TimeoutError('Result decoding deadline exceeded.')
|
|
56
|
+
if isinstance(error, ResultDecodeError):
|
|
57
|
+
return ResultDecodeError(str(error))
|
|
58
|
+
return ResultDecodeError('Result decoder failed (%s).' % type(error).__name__)
|
|
59
|
+
|
|
60
|
+
|
|
35
61
|
class _DecodeOwner:
|
|
36
62
|
"""One lease's cancellation latch for one envelope, with identity semantics."""
|
|
37
63
|
|
|
@@ -81,6 +107,7 @@ class _Slot:
|
|
|
81
107
|
self.error = None
|
|
82
108
|
|
|
83
109
|
def io(self):
|
|
110
|
+
stage = 'handshake'
|
|
84
111
|
try:
|
|
85
112
|
ready = self.channel.recv()
|
|
86
113
|
if not isinstance(ready, tuple) or len(ready) != 2 or ready[0] != 'ready':
|
|
@@ -94,16 +121,22 @@ class _Slot:
|
|
|
94
121
|
# so it cannot wait forever on the now-empty queue.
|
|
95
122
|
while self.runtime.state in ('starting', 'ready'):
|
|
96
123
|
job = self.jobs.get()
|
|
124
|
+
stage = 'send'
|
|
97
125
|
self.channel.send(job)
|
|
98
126
|
if job is None:
|
|
99
127
|
return
|
|
128
|
+
stage = 'receive'
|
|
100
129
|
result = self.channel.recv()
|
|
101
130
|
with self.runtime.condition:
|
|
102
131
|
self.result = result
|
|
103
132
|
self.runtime.condition.notify_all()
|
|
104
133
|
except (Exception, SystemExit) as error:
|
|
105
134
|
with self.runtime.condition:
|
|
106
|
-
|
|
135
|
+
if isinstance(error, (EOFError, OSError)):
|
|
136
|
+
self.error = _WorkerTransportError(
|
|
137
|
+
stage, self.index, self.pid, exception_type=type(error).__name__)
|
|
138
|
+
else:
|
|
139
|
+
self.error = _detached_error(error)
|
|
107
140
|
self.runtime.condition.notify_all()
|
|
108
141
|
|
|
109
142
|
|
|
@@ -115,6 +148,7 @@ class _Runtime:
|
|
|
115
148
|
self.state = 'starting'
|
|
116
149
|
self.error = None
|
|
117
150
|
self._active = None
|
|
151
|
+
self._active_cancelled = False
|
|
118
152
|
self._sequence = 0
|
|
119
153
|
self.cleanup_thread = None
|
|
120
154
|
self.stopped = threading.Event()
|
|
@@ -125,17 +159,23 @@ class _Runtime:
|
|
|
125
159
|
def pids(self):
|
|
126
160
|
return tuple(slot.pid for slot in self.slots if slot.pid is not None)
|
|
127
161
|
|
|
128
|
-
def
|
|
162
|
+
def _worker_failure(self):
|
|
163
|
+
# A protocol failure remains fatal even if another worker exits.
|
|
164
|
+
for slot in self.slots:
|
|
165
|
+
if slot.error is not None and not isinstance(slot.error, _WorkerTransportError):
|
|
166
|
+
return _detached_error(slot.error)
|
|
129
167
|
for slot in self.slots:
|
|
130
|
-
if slot.error is not None:
|
|
131
|
-
return True
|
|
132
168
|
if slot.process is not None and slot.pid is not None:
|
|
133
169
|
try:
|
|
134
170
|
if wait([slot.process.sentinel], timeout=0):
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
171
|
+
return _WorkerTransportError('exit', slot.index, slot.pid,
|
|
172
|
+
exitcode=slot.process.exitcode)
|
|
173
|
+
except (ValueError, OSError) as error:
|
|
174
|
+
return ResultDecodeError('Decode worker health check failed (%s).'
|
|
175
|
+
% type(error).__name__)
|
|
176
|
+
if slot.error is not None:
|
|
177
|
+
return _detached_error(slot.error)
|
|
178
|
+
return None
|
|
139
179
|
|
|
140
180
|
def start(self, deadline):
|
|
141
181
|
try:
|
|
@@ -144,7 +184,7 @@ class _Runtime:
|
|
|
144
184
|
_remaining(deadline)
|
|
145
185
|
with self.condition:
|
|
146
186
|
if self.error is not None:
|
|
147
|
-
raise self.error
|
|
187
|
+
raise _detached_error(self.error)
|
|
148
188
|
if self.state != 'starting':
|
|
149
189
|
raise ResultDecodeError('Decoder closed during worker startup.')
|
|
150
190
|
slot = _Slot(self, index)
|
|
@@ -164,20 +204,21 @@ class _Runtime:
|
|
|
164
204
|
with self.condition:
|
|
165
205
|
while not all(slot.ready for slot in self.slots):
|
|
166
206
|
if self.error is not None:
|
|
167
|
-
raise self.error
|
|
168
|
-
|
|
169
|
-
|
|
207
|
+
raise _detached_error(self.error)
|
|
208
|
+
failure = self._worker_failure()
|
|
209
|
+
if failure is not None:
|
|
210
|
+
raise failure
|
|
170
211
|
self.condition.wait(min(.02, _remaining(deadline)))
|
|
171
212
|
_remaining(deadline)
|
|
172
213
|
if self.error is not None:
|
|
173
|
-
raise self.error
|
|
214
|
+
raise _detached_error(self.error)
|
|
174
215
|
if self.state != 'starting':
|
|
175
216
|
raise ResultDecodeError('Decoder closed during worker startup.')
|
|
176
217
|
self.state = 'ready'
|
|
177
218
|
self.startup_done.set()
|
|
178
219
|
self.condition.notify_all()
|
|
179
220
|
except BaseException as error:
|
|
180
|
-
self.startup_error = error
|
|
221
|
+
self.startup_error = _detached_error(error)
|
|
181
222
|
self.startup_done.set()
|
|
182
223
|
self.quarantine(error, deadline)
|
|
183
224
|
self.stopped.wait(max(0, deadline - time.monotonic()))
|
|
@@ -188,13 +229,13 @@ class _Runtime:
|
|
|
188
229
|
while not self.startup_done.is_set():
|
|
189
230
|
self.condition.wait(min(.02, _remaining(deadline)))
|
|
190
231
|
if self.startup_error is not None:
|
|
191
|
-
raise self.startup_error
|
|
232
|
+
raise _detached_error(self.startup_error)
|
|
192
233
|
# A runtime that failed after startup is usable only sequentially.
|
|
193
234
|
|
|
194
235
|
def quarantine(self, error, deadline=None):
|
|
195
236
|
with self.condition:
|
|
196
237
|
if self.error is None:
|
|
197
|
-
self.error = error
|
|
238
|
+
self.error = _detached_error(error)
|
|
198
239
|
self.state = 'failed'
|
|
199
240
|
self.condition.notify_all()
|
|
200
241
|
self._request_cleanup(deadline if deadline is not None else time.monotonic() + .5)
|
|
@@ -244,6 +285,13 @@ class _Runtime:
|
|
|
244
285
|
slot.thread.join()
|
|
245
286
|
if slot.process is not None:
|
|
246
287
|
slot.process.close()
|
|
288
|
+
# No I/O thread can add another result or retain a queued job now.
|
|
289
|
+
slot.result = None
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
slot.jobs.get_nowait()
|
|
293
|
+
except queue.Empty:
|
|
294
|
+
break
|
|
247
295
|
with self.condition:
|
|
248
296
|
self.state = 'closed'
|
|
249
297
|
self.stopped.set()
|
|
@@ -258,6 +306,7 @@ class _Runtime:
|
|
|
258
306
|
owner.cancelled = True
|
|
259
307
|
active = self._active
|
|
260
308
|
if active is not None and active[0] is lease and (owner is None or active[1] is owner):
|
|
309
|
+
self._active_cancelled = True
|
|
261
310
|
self.quarantine(ResultDecodeError('Result decoding was cancelled.'))
|
|
262
311
|
|
|
263
312
|
def release(self, lease, deadline, wait=True):
|
|
@@ -276,20 +325,26 @@ class _Runtime:
|
|
|
276
325
|
_logger.debug('result_decode cleanup=%s workers=%d',
|
|
277
326
|
'stopped' if self.stopped.is_set() else 'pending', len(self.pids))
|
|
278
327
|
|
|
328
|
+
def _check_decode(self, lease, owner, deadline):
|
|
329
|
+
_remaining(deadline)
|
|
330
|
+
if lease._closed:
|
|
331
|
+
raise ValueError('Decoder lease is closed.')
|
|
332
|
+
if isinstance(owner, _DecodeOwner):
|
|
333
|
+
if owner.lease is not lease:
|
|
334
|
+
raise ValueError('Decode owner belongs to another lease.')
|
|
335
|
+
if owner.cancelled:
|
|
336
|
+
raise ResultDecodeError('Result decoding was cancelled.')
|
|
337
|
+
if (self._active is not None and self._active[0] is lease
|
|
338
|
+
and self._active[1] is owner and self._active_cancelled):
|
|
339
|
+
raise ResultDecodeError('Result decoding was cancelled.')
|
|
340
|
+
|
|
279
341
|
def decode(self, lease, columns, payloads, deadline, owner):
|
|
280
342
|
started = time.monotonic()
|
|
281
343
|
mode, fallback = 'sequential', 'single_chunk'
|
|
282
344
|
admitted = False
|
|
283
345
|
try:
|
|
284
346
|
with self.condition:
|
|
285
|
-
|
|
286
|
-
if lease._closed:
|
|
287
|
-
raise ValueError('Decoder lease is closed.')
|
|
288
|
-
if isinstance(owner, _DecodeOwner):
|
|
289
|
-
if owner.lease is not lease:
|
|
290
|
-
raise ValueError('Decode owner belongs to another lease.')
|
|
291
|
-
if owner.cancelled:
|
|
292
|
-
raise ResultDecodeError('Result decoding was cancelled.')
|
|
347
|
+
self._check_decode(lease, owner, deadline)
|
|
293
348
|
if len(payloads) > 1:
|
|
294
349
|
if self.state != 'ready':
|
|
295
350
|
fallback = 'unavailable'
|
|
@@ -299,29 +354,52 @@ class _Runtime:
|
|
|
299
354
|
fallback = 'decimal_context'
|
|
300
355
|
else:
|
|
301
356
|
self._active = (lease, owner)
|
|
357
|
+
self._active_cancelled = False
|
|
302
358
|
self._sequence += 1
|
|
303
359
|
token = self._sequence
|
|
304
360
|
admitted = True
|
|
305
361
|
mode, fallback = 'parallel', 'none'
|
|
306
362
|
if not admitted:
|
|
307
363
|
chunks = decode_result_batches(columns, payloads)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
else:
|
|
365
|
+
original_flags = dict(decimal.getcontext().flags)
|
|
366
|
+
try:
|
|
367
|
+
chunks = self._parallel(columns, payloads, deadline, token, lease, owner)
|
|
368
|
+
except _WorkerTransportError as error:
|
|
369
|
+
# Retire both processes and I/O threads before decoding the
|
|
370
|
+
# complete already-downloaded envelope in the caller.
|
|
371
|
+
self.quarantine(error, deadline)
|
|
372
|
+
_logger.warning('result_decode recovery=started %s', error)
|
|
373
|
+
error.__traceback__ = None
|
|
374
|
+
while not self.stopped.is_set():
|
|
375
|
+
with self.condition:
|
|
376
|
+
self._check_decode(lease, owner, deadline)
|
|
377
|
+
self.stopped.wait(min(.02, _remaining(deadline)))
|
|
378
|
+
with self.condition:
|
|
379
|
+
self._check_decode(lease, owner, deadline)
|
|
380
|
+
decimal.getcontext().flags = original_flags
|
|
381
|
+
mode, fallback = 'sequential', 'worker_transport'
|
|
382
|
+
chunks = decode_result_batches(columns, payloads)
|
|
383
|
+
with self.condition:
|
|
384
|
+
self._check_decode(lease, owner, deadline)
|
|
385
|
+
_logger.debug('result_decode recovery=complete chunks=%d', len(payloads))
|
|
386
|
+
except BaseException as error:
|
|
387
|
+
self.quarantine(error, deadline)
|
|
388
|
+
raise
|
|
389
|
+
with self.condition:
|
|
390
|
+
self._check_decode(lease, owner, deadline)
|
|
391
|
+
return chunks
|
|
315
392
|
finally:
|
|
316
393
|
if admitted:
|
|
317
394
|
with self.condition:
|
|
318
395
|
self._active = None
|
|
396
|
+
self._active_cancelled = False
|
|
319
397
|
self.condition.notify_all()
|
|
320
398
|
_logger.debug('result_decode mode=%s wall_ms=%.3f chunks=%d workers=%d fallback=%s cleanup=%s',
|
|
321
399
|
mode, (time.monotonic() - started) * 1000, len(payloads),
|
|
322
400
|
2 if admitted else 0, fallback, self.state)
|
|
323
401
|
|
|
324
|
-
def _parallel(self, columns, payloads, deadline, token):
|
|
402
|
+
def _parallel(self, columns, payloads, deadline, token, lease, owner):
|
|
325
403
|
# The existing vector decoder uses only the number of columns. Plain
|
|
326
404
|
# positions preserve that contract without serializing FieldInfo objects.
|
|
327
405
|
metadata = tuple(range(len(columns)))
|
|
@@ -330,39 +408,51 @@ class _Runtime:
|
|
|
330
408
|
output = [None] * len(payloads)
|
|
331
409
|
pending = {}
|
|
332
410
|
next_index = 0
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
index
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
411
|
+
result = None
|
|
412
|
+
try:
|
|
413
|
+
with self.condition:
|
|
414
|
+
while pending or next_index < len(payloads):
|
|
415
|
+
self._check_decode(lease, owner, deadline)
|
|
416
|
+
if self.error is not None:
|
|
417
|
+
raise _detached_error(self.error)
|
|
418
|
+
# Validate available replies before considering recovery, so
|
|
419
|
+
# a worker/protocol error is never hidden by a second exit.
|
|
420
|
+
for slot in self.slots:
|
|
421
|
+
if slot.index in pending and slot.result is not None:
|
|
422
|
+
result, slot.result = slot.result, None
|
|
423
|
+
index = pending.pop(slot.index)
|
|
424
|
+
if (not isinstance(result, tuple) or len(result) != 5 or
|
|
425
|
+
result[1] != token or result[2] != index):
|
|
426
|
+
raise ResultDecodeError('Decode worker returned an invalid job result.')
|
|
427
|
+
flags = result[4]
|
|
428
|
+
if not isinstance(flags, tuple) or any(name not in decimal_signals for name in flags):
|
|
429
|
+
raise ResultDecodeError('Decode worker returned invalid decimal flags.')
|
|
430
|
+
for name in flags:
|
|
431
|
+
context.flags[decimal_signals[name]] = True
|
|
432
|
+
if result[0] == 'error':
|
|
433
|
+
raise ResultDecodeError('Result chunk decoding failed in worker (%s).' % result[3])
|
|
434
|
+
if result[0] != 'result':
|
|
435
|
+
raise ResultDecodeError('Decode worker returned an invalid result kind.')
|
|
436
|
+
output[index] = result[3]
|
|
437
|
+
failure = self._worker_failure()
|
|
438
|
+
if failure is not None:
|
|
439
|
+
raise failure
|
|
440
|
+
for slot in self.slots:
|
|
441
|
+
if slot.index not in pending and next_index < len(payloads):
|
|
442
|
+
index = next_index
|
|
443
|
+
next_index += 1
|
|
444
|
+
pending[slot.index] = index
|
|
445
|
+
slot.jobs.put_nowait((token, index, metadata, payloads[index]))
|
|
446
|
+
if pending:
|
|
447
|
+
self.condition.wait(min(.02, _remaining(deadline)))
|
|
448
|
+
self._check_decode(lease, owner, deadline)
|
|
449
|
+
return [rows for rows in output if rows]
|
|
450
|
+
finally:
|
|
451
|
+
# Fatal errors may keep their traceback for the caller. Do not let
|
|
452
|
+
# that traceback also retain partial decoded output or pending jobs.
|
|
453
|
+
output.clear()
|
|
454
|
+
pending.clear()
|
|
455
|
+
result = None
|
|
366
456
|
|
|
367
457
|
|
|
368
458
|
class DecoderLease:
|
|
@@ -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
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_auto_resume.py
RENAMED
|
@@ -349,7 +349,9 @@ def test_legacy_suspend_resume_baseline(cached, active):
|
|
|
349
349
|
connector._set_active_strategy(cached)
|
|
350
350
|
state = ['suspended']
|
|
351
351
|
seen = []
|
|
352
|
-
connection = make_connection(access_token=None, username='fixture-user', password='fixture-password'
|
|
352
|
+
connection = make_connection(access_token=None, username='fixture-user', password='fixture-password',
|
|
353
|
+
grpc_options={'max_receive_message_length': 4096,
|
|
354
|
+
'max_send_message_length': 8192})
|
|
353
355
|
def authenticate(request, metadata):
|
|
354
356
|
assert request.user == 'fixture-user'
|
|
355
357
|
assert request.password == 'fixture-password'
|
|
@@ -361,7 +363,10 @@ def test_legacy_suspend_resume_baseline(cached, active):
|
|
|
361
363
|
if headers['strategy'] != active:
|
|
362
364
|
raise rpc_error(grpc.StatusCode.UNKNOWN, 'status: 456')
|
|
363
365
|
return engine_pb.AuthenticateResponse(sessionId='legacy-session')
|
|
364
|
-
def resume(
|
|
366
|
+
def resume(manager):
|
|
367
|
+
options = dict(manager._grpc_options)
|
|
368
|
+
assert options['grpc.max_receive_message_length'] == 4096
|
|
369
|
+
assert options['grpc.max_send_message_length'] == 8192
|
|
365
370
|
state[0] = 'active'
|
|
366
371
|
return True
|
|
367
372
|
connection._client = SimpleNamespace(authenticate=authenticate)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/aio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_38_nines.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_all_cases.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_binary.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_correct_value.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_fields.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/check_decimal_errors.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/cleanup_test_files.py
RENAMED
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/debug_38_nines.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_comprehensive.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_dialect_columns.py
RENAMED
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_improved_parsing.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_known_case.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_manual_analysis.py
RENAMED
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_mock_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_specific_binary.py
RENAMED
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_user_binary_value.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/validate_decimal128.py
RENAMED
|
File without changes
|
|
File without changes
|
{e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/verify_decimal_fix.py
RENAMED
|
File without changes
|