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.
Files changed (125) hide show
  1. {e6data_python_connector-3.0.1rc3/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc4}/PKG-INFO +19 -6
  2. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/README.md +18 -5
  3. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cluster_manager.py +9 -4
  4. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection.py +30 -8
  5. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection_pool.py +2 -0
  6. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cursor.py +3 -2
  7. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_manager.py +4 -3
  8. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6data_grpc.py +10 -6
  9. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode.py +155 -65
  10. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4/e6data_python_connector.egg-info}/PKG-INFO +19 -6
  11. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/setup.py +1 -1
  12. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_auto_resume.py +7 -2
  13. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/LICENSE +0 -0
  14. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/MANIFEST.in +0 -0
  15. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/__init__.py +0 -0
  16. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/aio.py +0 -0
  17. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_dialect.py +0 -0
  18. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_oauth.py +0 -0
  19. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_work.py +0 -0
  20. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
  21. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  22. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  23. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/common.py +0 -0
  24. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/connection_pool.py +0 -0
  25. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/constants.py +0 -0
  26. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/datainputstream.py +0 -0
  27. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/date_time_utils.py +0 -0
  28. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/dialect.py +0 -0
  29. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  30. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
  31. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  32. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/exceptions.py +0 -0
  33. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth.py +0 -0
  34. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_cache.py +0 -0
  35. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_common.py +0 -0
  36. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_batch.py +0 -0
  37. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode_worker.py +0 -0
  38. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_prefetch.py +0 -0
  39. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
  40. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/__init__.py +0 -0
  41. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/constants.py +0 -0
  42. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  43. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  44. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  45. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/ttypes.py +0 -0
  46. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/strategy.py +0 -0
  47. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/typeId.py +0 -0
  48. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
  49. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  50. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  51. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
  52. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
  53. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/pyproject.toml +0 -0
  54. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/setup.cfg +0 -0
  55. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/__init__.py +0 -0
  56. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_38_nines.py +0 -0
  57. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_all_cases.py +0 -0
  58. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_binary.py +0 -0
  59. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_correct_value.py +0 -0
  60. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/analyze_fields.py +0 -0
  61. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/check_decimal_errors.py +0 -0
  62. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/cleanup_test_files.py +0 -0
  63. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/conftest.py +0 -0
  64. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/debug_38_nines.py +0 -0
  65. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/debug_binary.py +0 -0
  66. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/final_test.py +0 -0
  67. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/__init__.py +0 -0
  68. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_preflight.py +0 -0
  69. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_result_batch_v2.py +0 -0
  70. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_sync_async.py +0 -0
  71. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/expiry_preflight.py +0 -0
  72. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/result_batch_benchmark_support.py +0 -0
  73. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/support.py +0 -0
  74. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_connection.py +0 -0
  75. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_pool.py +0 -0
  76. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_queries.py +0 -0
  77. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_resume.py +0 -0
  78. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_sqlalchemy.py +0 -0
  79. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_token_lifecycle.py +0 -0
  80. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_auth_boundary.py +0 -0
  81. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_oauth_continuation.py +0 -0
  82. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_readonly_matrix.py +0 -0
  83. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_result_batch_v2.py +0 -0
  84. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_shared_token_issuance.py +0 -0
  85. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  86. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/integration/token_observation.py +0 -0
  87. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/move_tests.py +0 -0
  88. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/quick_test.py +0 -0
  89. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_38_nines.py +0 -0
  90. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_all_decimal128_cases.py +0 -0
  91. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_efficiency.py +0 -0
  92. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_none_strategy.py +0 -0
  93. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_oauth.py +0 -0
  94. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_strategy.py +0 -0
  95. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_code_optimizations.py +0 -0
  96. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_comprehensive.py +0 -0
  97. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool.py +0 -0
  98. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool_e2e.py +0 -0
  99. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_current_implementation.py +0 -0
  100. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_binary_parsing.py +0 -0
  101. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_parsing.py +0 -0
  102. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_dialect_columns.py +0 -0
  103. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_fix.py +0 -0
  104. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_improved_parsing.py +0 -0
  105. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_known_case.py +0 -0
  106. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_manual_analysis.py +0 -0
  107. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_mock_server.py +0 -0
  108. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_multiprocessing_fix.py +0 -0
  109. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_new_implementation.py +0 -0
  110. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_authentication.py +0 -0
  111. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_oauth_resume_integration.py +0 -0
  112. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_pool_concurrency_simple.py +0 -0
  113. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_pool_threading_multiprocessing.py +0 -0
  114. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_simple_connection_pool.py +0 -0
  115. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_specific_binary.py +0 -0
  116. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy.py +0 -0
  117. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_logic.py +0 -0
  118. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_persistence_fix.py +0 -0
  119. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_strategy_sharing_fix.py +0 -0
  120. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/test_user_binary_value.py +0 -0
  121. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/tests.py +0 -0
  122. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/tests_grpc.py +0 -0
  123. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/validate_decimal128.py +0 -0
  124. {e6data_python_connector-3.0.1rc3 → e6data_python_connector-3.0.1rc4}/test/validate_implementation.py +0 -0
  125. {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.1rc3
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 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.
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 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.
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
- limit = options.get('grpc.max_receive_message_length', 64 * 1024 * 1024)
127
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
128
- raise ValueError('A finite positive receive limit is required.')
129
- options['grpc.max_receive_message_length'] = limit
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=64 * 1024 * 1024,
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
- for name in ('max_receive_message_length', 'grpc.max_receive_message_length'):
105
- if name in options:
106
- if options[name] != max_receive_message_bytes:
107
- raise ValueError('Set the finite receive limit using max_receive_message_bytes.')
108
- del options[name]
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
- self._grpc_options = grpc_options
209
- if grpc_options is None:
210
- self._grpc_options = dict()
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', 64 * 1024 * 1024)
531
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
532
- raise ValueError('V2 receive limit must be a finite positive integer.')
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": 300 * 1024 * 1024, # Maximum size of sent messages (300 MB).
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
- return self._result_failure
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 self._result_failure
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
- self.error = error
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 _worker_failed(self):
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 True
136
- except (ValueError, OSError):
137
- return True
138
- return False
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
- if self._worker_failed():
169
- raise ResultDecodeError('Decode worker failed before startup completed.')
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
- _remaining(deadline)
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
- _remaining(deadline)
309
- return chunks
310
- try:
311
- return self._parallel(columns, payloads, deadline, token)
312
- except BaseException as error:
313
- self.quarantine(error, deadline)
314
- raise
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
- with self.condition:
334
- while pending or next_index < len(payloads):
335
- _remaining(deadline)
336
- if self.error is not None:
337
- raise self.error
338
- if self._worker_failed():
339
- raise ResultDecodeError('Decode worker exited before returning a complete result.')
340
- for slot in self.slots:
341
- if slot.index in pending and slot.result is not None:
342
- result, slot.result = slot.result, None
343
- index = pending.pop(slot.index)
344
- if (not isinstance(result, tuple) or len(result) != 5 or
345
- result[1] != token or result[2] != index):
346
- raise ResultDecodeError('Decode worker returned an invalid job result.')
347
- flags = result[4]
348
- if not isinstance(flags, tuple) or any(name not in decimal_signals for name in flags):
349
- raise ResultDecodeError('Decode worker returned invalid decimal flags.')
350
- for name in flags:
351
- context.flags[decimal_signals[name]] = True
352
- if result[0] == 'error':
353
- raise ResultDecodeError('Result chunk decoding failed in worker (%s).' % result[3])
354
- if result[0] != 'result':
355
- raise ResultDecodeError('Decode worker returned an invalid result kind.')
356
- output[index] = result[3]
357
- if slot.index not in pending and next_index < len(payloads):
358
- index = next_index
359
- next_index += 1
360
- pending[slot.index] = index
361
- slot.jobs.put_nowait((token, index, metadata, payloads[index]))
362
- if pending:
363
- self.condition.wait(min(.02, _remaining(deadline)))
364
- _remaining(deadline)
365
- return [rows for rows in output if rows]
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.1rc3
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 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.
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
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (3, 0, '1rc3',)
15
+ VERSION = (3, 0, '1rc4',)
16
16
 
17
17
 
18
18
  def get_long_desc():
@@ -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)