e6data-python-connector 2.3.9rc2__tar.gz → 2.3.9rc4__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-2.3.9rc2/e6data_python_connector.egg-info → e6data_python_connector-2.3.9rc4}/PKG-INFO +2 -2
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/README.md +1 -1
- e6data_python_connector-2.3.9rc4/e6data_python_connector/__init__.py +3 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_manager.py +4 -22
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6data_grpc.py +272 -137
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4/e6data_python_connector.egg-info}/PKG-INFO +2 -2
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -4
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/setup.py +1 -1
- e6data_python_connector-2.3.9rc2/e6data_python_connector/__init__.py +0 -42
- e6data_python_connector-2.3.9rc2/e6data_python_connector/auto_resume_coordinator.py +0 -441
- e6data_python_connector-2.3.9rc2/e6data_python_connector/process_safe_manager.py +0 -455
- e6data_python_connector-2.3.9rc2/e6data_python_connector/retry_handler.py +0 -354
- e6data_python_connector-2.3.9rc2/e6data_python_connector/session_manager.py +0 -383
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/LICENSE +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/MANIFEST.in +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/dialect.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/pyproject.toml +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/setup.cfg +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/__init__.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_binary.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_fields.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/debug_binary.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/final_test.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/move_tests.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/quick_test.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_38_nines.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_fix.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_known_case.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_mock_server.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/tests.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/tests_grpc.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/validate_implementation.py +0 -0
- {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/verify_decimal_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.9rc4
|
|
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.
|
|
@@ -38,7 +38,7 @@ Dynamic: summary
|
|
|
38
38
|
|
|
39
39
|
# e6data Python Connector
|
|
40
40
|
|
|
41
|
-

|
|
42
42
|
|
|
43
43
|
## Introduction
|
|
44
44
|
|
|
@@ -8,7 +8,6 @@ import multiprocessing
|
|
|
8
8
|
|
|
9
9
|
from e6data_python_connector.strategy import _get_active_strategy, _set_active_strategy, _set_pending_strategy, \
|
|
10
10
|
_get_grpc_header as _get_strategy_header
|
|
11
|
-
from e6data_python_connector.retry_handler import get_retry_handler, LOCK_ACQUISITION_RETRY_CONFIG
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def _get_grpc_header(engine_ip=None, cluster=None, strategy=None):
|
|
@@ -87,33 +86,16 @@ class _StatusLock:
|
|
|
87
86
|
Acquires both the thread-level lock and process-level semaphore
|
|
88
87
|
to ensure the current operation can safely access shared resources.
|
|
89
88
|
|
|
90
|
-
Uses retry logic to handle lock contention in high-concurrency environments.
|
|
91
|
-
|
|
92
89
|
Raises:
|
|
93
90
|
TimeoutError: If the lock cannot be acquired within the timeout period.
|
|
94
91
|
|
|
95
92
|
Returns:
|
|
96
93
|
_StatusLock: The current instance of the lock, used for context management.
|
|
97
94
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if not thread_acquired:
|
|
103
|
-
raise TimeoutError("Failed to acquire thread lock")
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
mp_acquired = self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT)
|
|
107
|
-
if not mp_acquired:
|
|
108
|
-
self._status_thread_lock.release()
|
|
109
|
-
raise TimeoutError("Failed to acquire multiprocessing lock")
|
|
110
|
-
except Exception:
|
|
111
|
-
self._status_thread_lock.release()
|
|
112
|
-
raise
|
|
113
|
-
|
|
114
|
-
return self
|
|
115
|
-
|
|
116
|
-
return retry_handler.execute_with_retry(acquire_locks, "lock_acquisition")
|
|
95
|
+
|
|
96
|
+
self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
|
|
97
|
+
self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT)
|
|
98
|
+
return self
|
|
117
99
|
|
|
118
100
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
119
101
|
"""
|
|
@@ -26,9 +26,6 @@ from e6data_python_connector.constants import *
|
|
|
26
26
|
from e6data_python_connector.datainputstream import get_query_columns_info, read_rows_from_chunk
|
|
27
27
|
from e6data_python_connector.server import e6x_engine_pb2_grpc, e6x_engine_pb2
|
|
28
28
|
from e6data_python_connector.typeId import *
|
|
29
|
-
from e6data_python_connector.process_safe_manager import get_strategy_manager
|
|
30
|
-
from e6data_python_connector.auto_resume_coordinator import get_resume_coordinator
|
|
31
|
-
from e6data_python_connector.session_manager import get_session_manager
|
|
32
29
|
|
|
33
30
|
apilevel = '2.0'
|
|
34
31
|
threadsafety = 2 # Threads may share the e6xdb and connections.
|
|
@@ -115,70 +112,181 @@ class HiveParamEscaper(ParamEscaper):
|
|
|
115
112
|
|
|
116
113
|
_escaper = HiveParamEscaper()
|
|
117
114
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
# Thread-safe and process-safe storage for active deployment strategy
|
|
116
|
+
_strategy_lock = threading.Lock()
|
|
117
|
+
_strategy_manager = None
|
|
118
|
+
_shared_strategy = None
|
|
119
|
+
_local_strategy_cache = {
|
|
120
|
+
'active_strategy': None,
|
|
121
|
+
'last_check_time': 0,
|
|
122
|
+
'pending_strategy': None, # Strategy to use for next query
|
|
123
|
+
'query_strategy_map': {}, # Map of query_id to strategy used
|
|
124
|
+
'last_transition_time': 0, # Timestamp of last strategy transition
|
|
125
|
+
'session_invalidated': False # Flag to invalidate all sessions
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Strategy cache timeout in seconds (5 minutes)
|
|
129
|
+
STRATEGY_CACHE_TIMEOUT = 300
|
|
122
130
|
|
|
123
131
|
|
|
124
132
|
def _get_shared_strategy():
|
|
125
|
-
"""Get
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
"""Get or create the shared strategy storage."""
|
|
134
|
+
global _strategy_manager, _shared_strategy
|
|
135
|
+
|
|
136
|
+
# Try to use multiprocessing.Manager for process-safe storage
|
|
137
|
+
try:
|
|
138
|
+
if _strategy_manager is None:
|
|
139
|
+
_strategy_manager = multiprocessing.Manager()
|
|
140
|
+
_shared_strategy = _strategy_manager.dict()
|
|
141
|
+
_shared_strategy['active_strategy'] = None
|
|
142
|
+
_shared_strategy['last_check_time'] = 0
|
|
143
|
+
_shared_strategy['pending_strategy'] = None
|
|
144
|
+
_shared_strategy['query_strategy_map'] = _strategy_manager.dict()
|
|
145
|
+
_shared_strategy['last_transition_time'] = 0
|
|
146
|
+
_shared_strategy['session_invalidated'] = False
|
|
147
|
+
return _shared_strategy
|
|
148
|
+
except:
|
|
149
|
+
# Fall back to thread-local storage if Manager fails
|
|
150
|
+
return _local_strategy_cache
|
|
132
151
|
|
|
133
152
|
|
|
134
153
|
def _get_active_strategy():
|
|
135
|
-
"""Get the active deployment strategy (blue or green)."""
|
|
136
|
-
|
|
154
|
+
"""Get the active deployment strategy (blue or green) from shared memory."""
|
|
155
|
+
with _strategy_lock:
|
|
156
|
+
shared_strategy = _get_shared_strategy()
|
|
157
|
+
# current_time = time.time()
|
|
158
|
+
# Check if strategy is cached and not expired
|
|
159
|
+
# if (shared_strategy['active_strategy'] is not None and
|
|
160
|
+
# current_time - shared_strategy['last_check_time'] < STRATEGY_CACHE_TIMEOUT):
|
|
161
|
+
if shared_strategy['active_strategy'] is not None:
|
|
162
|
+
return shared_strategy['active_strategy']
|
|
163
|
+
return None
|
|
137
164
|
|
|
138
165
|
|
|
139
166
|
def _set_active_strategy(strategy):
|
|
140
|
-
"""Set the active deployment strategy."""
|
|
141
|
-
|
|
167
|
+
"""Set the active deployment strategy in shared memory."""
|
|
168
|
+
if not strategy:
|
|
169
|
+
return
|
|
170
|
+
# Normalize strategy to lowercase and validate
|
|
171
|
+
normalized_strategy = strategy.lower()
|
|
172
|
+
if normalized_strategy not in ['blue', 'green']:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
with _strategy_lock:
|
|
176
|
+
shared_strategy = _get_shared_strategy()
|
|
177
|
+
current_time = time.time()
|
|
178
|
+
|
|
179
|
+
# Only update transition time if strategy actually changed
|
|
180
|
+
if shared_strategy['active_strategy'] != normalized_strategy:
|
|
181
|
+
shared_strategy['last_transition_time'] = current_time
|
|
182
|
+
|
|
183
|
+
shared_strategy['active_strategy'] = normalized_strategy
|
|
184
|
+
shared_strategy['last_check_time'] = current_time
|
|
142
185
|
|
|
143
186
|
|
|
144
187
|
def _clear_strategy_cache():
|
|
145
188
|
"""Clear the cached strategy to force re-detection."""
|
|
146
|
-
|
|
189
|
+
with _strategy_lock:
|
|
190
|
+
shared_strategy = _get_shared_strategy()
|
|
191
|
+
shared_strategy['active_strategy'] = None
|
|
192
|
+
shared_strategy['last_check_time'] = 0
|
|
193
|
+
shared_strategy['pending_strategy'] = None
|
|
147
194
|
|
|
148
195
|
|
|
149
196
|
def _set_pending_strategy(strategy):
|
|
150
197
|
"""Set the pending strategy to be used for the next query."""
|
|
151
|
-
|
|
198
|
+
if not strategy:
|
|
199
|
+
return
|
|
200
|
+
# Normalize strategy to lowercase and validate
|
|
201
|
+
normalized_strategy = strategy.lower()
|
|
202
|
+
if normalized_strategy not in ['blue', 'green']:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
with _strategy_lock:
|
|
206
|
+
shared_strategy = _get_shared_strategy()
|
|
207
|
+
current_active = shared_strategy['active_strategy']
|
|
208
|
+
|
|
209
|
+
if normalized_strategy != current_active:
|
|
210
|
+
shared_strategy['pending_strategy'] = normalized_strategy
|
|
152
211
|
|
|
153
212
|
|
|
154
213
|
def _apply_pending_strategy():
|
|
155
214
|
"""Apply the pending strategy as the active strategy."""
|
|
156
|
-
|
|
215
|
+
with _strategy_lock:
|
|
216
|
+
shared_strategy = _get_shared_strategy()
|
|
217
|
+
if shared_strategy['pending_strategy']:
|
|
218
|
+
old_strategy = shared_strategy['active_strategy']
|
|
219
|
+
new_strategy = shared_strategy['pending_strategy']
|
|
220
|
+
current_time = time.time()
|
|
221
|
+
|
|
222
|
+
shared_strategy['active_strategy'] = new_strategy
|
|
223
|
+
shared_strategy['pending_strategy'] = None
|
|
224
|
+
shared_strategy['last_check_time'] = current_time
|
|
225
|
+
shared_strategy['last_transition_time'] = current_time
|
|
226
|
+
shared_strategy['session_invalidated'] = True # Invalidate all sessions
|
|
227
|
+
|
|
228
|
+
return new_strategy
|
|
229
|
+
return None
|
|
157
230
|
|
|
158
231
|
|
|
159
232
|
def _invalidate_all_sessions():
|
|
160
233
|
"""Invalidate all existing sessions to force fresh connections with new strategy."""
|
|
161
|
-
|
|
234
|
+
# This is a global flag that all connections will check
|
|
235
|
+
shared_strategy = _get_shared_strategy()
|
|
236
|
+
shared_strategy['session_invalidated'] = True
|
|
162
237
|
|
|
163
238
|
|
|
164
239
|
def _register_query_strategy(query_id, strategy):
|
|
165
240
|
"""Register the strategy used for a specific query."""
|
|
166
|
-
|
|
241
|
+
if not query_id or not strategy:
|
|
242
|
+
return
|
|
243
|
+
# Normalize strategy to lowercase and validate
|
|
244
|
+
normalized_strategy = strategy.lower()
|
|
245
|
+
if normalized_strategy not in ['blue', 'green']:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
with _strategy_lock:
|
|
249
|
+
shared_strategy = _get_shared_strategy()
|
|
250
|
+
query_map = shared_strategy.get('query_strategy_map', {})
|
|
251
|
+
query_map[query_id] = normalized_strategy
|
|
252
|
+
shared_strategy['query_strategy_map'] = query_map
|
|
167
253
|
|
|
168
254
|
|
|
169
255
|
def _get_query_strategy(query_id):
|
|
170
256
|
"""Get the strategy used for a specific query."""
|
|
171
|
-
|
|
257
|
+
current_active_strategy = _get_active_strategy()
|
|
258
|
+
if not query_id:
|
|
259
|
+
return current_active_strategy
|
|
260
|
+
with _strategy_lock:
|
|
261
|
+
shared_strategy = _get_shared_strategy()
|
|
262
|
+
query_map = shared_strategy.get('query_strategy_map', {})
|
|
263
|
+
return query_map.get(query_id, current_active_strategy)
|
|
172
264
|
|
|
173
265
|
|
|
174
266
|
def _cleanup_query_strategy(query_id):
|
|
175
267
|
"""Remove the strategy mapping for a completed query."""
|
|
176
|
-
|
|
268
|
+
if not query_id:
|
|
269
|
+
return
|
|
270
|
+
with _strategy_lock:
|
|
271
|
+
shared_strategy = _get_shared_strategy()
|
|
272
|
+
query_map = shared_strategy.get('query_strategy_map', {})
|
|
273
|
+
if query_id in query_map:
|
|
274
|
+
del query_map[query_id]
|
|
275
|
+
shared_strategy['query_strategy_map'] = query_map
|
|
177
276
|
|
|
178
277
|
|
|
179
278
|
def _get_strategy_debug_info():
|
|
180
279
|
"""Get debug information about current strategy state."""
|
|
181
|
-
|
|
280
|
+
with _strategy_lock:
|
|
281
|
+
shared_strategy = _get_shared_strategy()
|
|
282
|
+
return {
|
|
283
|
+
'active_strategy': shared_strategy.get('active_strategy'),
|
|
284
|
+
'pending_strategy': shared_strategy.get('pending_strategy'),
|
|
285
|
+
'last_check_time': shared_strategy.get('last_check_time', 0),
|
|
286
|
+
'last_transition_time': shared_strategy.get('last_transition_time', 0),
|
|
287
|
+
'query_count': len(shared_strategy.get('query_strategy_map', {})),
|
|
288
|
+
'current_time': time.time()
|
|
289
|
+
}
|
|
182
290
|
|
|
183
291
|
|
|
184
292
|
def _get_grpc_header(engine_ip=None, cluster=None, strategy=None):
|
|
@@ -262,11 +370,6 @@ class Connection(object):
|
|
|
262
370
|
self._auto_resume = auto_resume
|
|
263
371
|
|
|
264
372
|
self._grpc_options = grpc_options
|
|
265
|
-
|
|
266
|
-
# Process-safe session management
|
|
267
|
-
self._connection_id = _session_manager.register_connection(self)
|
|
268
|
-
self._session_invalidated_externally = False
|
|
269
|
-
self._last_session_generation = 0
|
|
270
373
|
if self._grpc_options is None:
|
|
271
374
|
self._grpc_options = dict()
|
|
272
375
|
self.grpc_prepare_timeout = self._grpc_options.get('grpc_prepare_timeout') or 10 * 60 # 10 minutes
|
|
@@ -277,11 +380,7 @@ class Connection(object):
|
|
|
277
380
|
"""
|
|
278
381
|
self.grpc_auto_resume_timeout_seconds = self._grpc_options.pop('grpc_auto_resume_timeout_seconds')
|
|
279
382
|
self._create_client()
|
|
280
|
-
|
|
281
|
-
generate a unique bas64 hash for the connection based on the host, port, username, and secure channel.
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
self._hash = str(datetime.datetime.now()) + '_hash'
|
|
383
|
+
self._hash = str(datetime.datetime.now().timestamp()) + 'hash' + str(id(self))
|
|
285
384
|
|
|
286
385
|
@property
|
|
287
386
|
def _get_grpc_options(self):
|
|
@@ -358,10 +457,6 @@ class Connection(object):
|
|
|
358
457
|
Raises:
|
|
359
458
|
Exception: If there is an error during the re-authentication process.
|
|
360
459
|
"""
|
|
361
|
-
# Clear current session
|
|
362
|
-
if hasattr(self, '_connection_id'):
|
|
363
|
-
_session_manager.invalidate_session(self._connection_id)
|
|
364
|
-
|
|
365
460
|
self.close()
|
|
366
461
|
self._create_client()
|
|
367
462
|
return self.get_session_id
|
|
@@ -372,114 +467,158 @@ class Connection(object):
|
|
|
372
467
|
To get the session id, if user is not authorised, first authenticate the user.
|
|
373
468
|
Also detects the active deployment strategy (blue/green) on first authentication.
|
|
374
469
|
"""
|
|
375
|
-
# Check
|
|
376
|
-
|
|
470
|
+
# Check if we need a fresh connection due to strategy change
|
|
471
|
+
shared_strategy = _get_shared_strategy()
|
|
472
|
+
pending_strategy = shared_strategy.get('pending_strategy')
|
|
473
|
+
active_strategy = shared_strategy.get('active_strategy')
|
|
474
|
+
session_invalidated = shared_strategy.get('session_invalidated', False)
|
|
475
|
+
|
|
476
|
+
# Check if session was invalidated globally
|
|
477
|
+
if self._session_id and session_invalidated:
|
|
377
478
|
self._session_id = None
|
|
378
|
-
self._session_invalidated_externally = False
|
|
379
479
|
self.close()
|
|
380
480
|
self._create_client()
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
# Check if we need to apply pending strategy
|
|
393
|
-
pending_strategy = _strategy_manager.get_pending_strategy()
|
|
394
|
-
if pending_strategy:
|
|
395
|
-
applied_strategy = _apply_pending_strategy()
|
|
396
|
-
if applied_strategy:
|
|
397
|
-
# Strategy was applied - invalidate session
|
|
481
|
+
# Clear the invalidation flag
|
|
482
|
+
shared_strategy['session_invalidated'] = False
|
|
483
|
+
|
|
484
|
+
# Only create fresh connection if we have no active queries
|
|
485
|
+
elif self._session_id and pending_strategy and pending_strategy != active_strategy:
|
|
486
|
+
query_map = shared_strategy.get('query_strategy_map', {})
|
|
487
|
+
if len(query_map) == 0:
|
|
488
|
+
# Apply the pending strategy immediately since no queries are active
|
|
489
|
+
_apply_pending_strategy()
|
|
490
|
+
# Force complete reconnection with new strategy
|
|
398
491
|
self._session_id = None
|
|
399
|
-
_session_manager.remove_session(self._connection_id)
|
|
400
492
|
self.close()
|
|
401
493
|
self._create_client()
|
|
402
494
|
|
|
403
|
-
# Authenticate if needed
|
|
404
495
|
if not self._session_id:
|
|
405
496
|
try:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
self.
|
|
409
|
-
|
|
410
|
-
|
|
497
|
+
authenticate_request = e6x_engine_pb2.AuthenticateRequest(
|
|
498
|
+
user=self.__username,
|
|
499
|
+
password=self.__password
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Check if we have a cached strategy
|
|
503
|
+
active_strategy = _get_active_strategy()
|
|
504
|
+
shared_strategy = _get_shared_strategy()
|
|
505
|
+
pending_strategy = shared_strategy.get('pending_strategy')
|
|
506
|
+
|
|
507
|
+
if active_strategy and not pending_strategy:
|
|
508
|
+
# Use cached strategy only if there's no pending strategy
|
|
509
|
+
try:
|
|
510
|
+
authenticate_response = self._client.authenticate(
|
|
511
|
+
authenticate_request,
|
|
512
|
+
metadata=_get_grpc_header(cluster=self.cluster_name, strategy=active_strategy)
|
|
513
|
+
)
|
|
514
|
+
self._session_id = authenticate_response.sessionId
|
|
515
|
+
if not self._session_id:
|
|
516
|
+
raise ValueError("Invalid credentials.")
|
|
517
|
+
# Check for new strategy in authenticate response
|
|
518
|
+
if hasattr(authenticate_response, 'new_strategy') and authenticate_response.new_strategy:
|
|
519
|
+
new_strategy = authenticate_response.new_strategy.lower()
|
|
520
|
+
if new_strategy != active_strategy:
|
|
521
|
+
_set_pending_strategy(new_strategy)
|
|
522
|
+
# Don't apply immediately - let new queries use fresh connections
|
|
523
|
+
except _InactiveRpcError as e:
|
|
524
|
+
print(self._hash, self.cluster_name, 'Error during authentication with active strategy:', active_strategy, file=sys.stderr)
|
|
525
|
+
if e.code() == grpc.StatusCode.UNKNOWN and 'status: 456' in e.details():
|
|
526
|
+
# Strategy changed, clear cache and retry
|
|
527
|
+
_clear_strategy_cache()
|
|
528
|
+
active_strategy = None
|
|
529
|
+
else:
|
|
530
|
+
raise e
|
|
531
|
+
elif pending_strategy:
|
|
532
|
+
# If there's a pending strategy, force re-authentication with new strategy
|
|
533
|
+
active_strategy = None
|
|
534
|
+
|
|
535
|
+
if not active_strategy:
|
|
536
|
+
# Check if we have a pending strategy to use
|
|
537
|
+
if pending_strategy:
|
|
538
|
+
# Use pending strategy and apply it immediately
|
|
539
|
+
_apply_pending_strategy()
|
|
540
|
+
active_strategy = _get_active_strategy()
|
|
541
|
+
strategies = [active_strategy]
|
|
542
|
+
else:
|
|
543
|
+
# Always try blue first, then green if it fails with 456
|
|
544
|
+
strategies = ['blue', 'green']
|
|
545
|
+
last_error = None
|
|
546
|
+
for strategy in strategies:
|
|
547
|
+
try:
|
|
548
|
+
authenticate_response = self._client.authenticate(
|
|
549
|
+
authenticate_request,
|
|
550
|
+
metadata=_get_grpc_header(cluster=self.cluster_name, strategy=strategy)
|
|
551
|
+
)
|
|
552
|
+
self._session_id = authenticate_response.sessionId
|
|
553
|
+
if self._session_id:
|
|
554
|
+
# Success! Cache this strategy
|
|
555
|
+
_set_active_strategy(strategy)
|
|
556
|
+
|
|
557
|
+
# Check for new strategy in authenticate response
|
|
558
|
+
if hasattr(authenticate_response,
|
|
559
|
+
'new_strategy') and authenticate_response.new_strategy:
|
|
560
|
+
new_strategy = authenticate_response.new_strategy.lower()
|
|
561
|
+
if new_strategy != strategy:
|
|
562
|
+
_set_pending_strategy(new_strategy)
|
|
563
|
+
# Don't apply immediately - let new queries use fresh connections
|
|
564
|
+
break
|
|
565
|
+
except _InactiveRpcError as e:
|
|
566
|
+
print(self._hash, self.cluster_name, 'Error during authentication with strategy:', strategy, file=sys.stderr)
|
|
567
|
+
if e.code() == grpc.StatusCode.UNKNOWN and 'status: 456' in e.details():
|
|
568
|
+
# Wrong strategy, try the next one
|
|
569
|
+
last_error = e
|
|
570
|
+
continue
|
|
571
|
+
else:
|
|
572
|
+
# Different error, handle it normally
|
|
573
|
+
raise e
|
|
574
|
+
|
|
575
|
+
if not self._session_id and last_error:
|
|
576
|
+
# Neither strategy worked
|
|
577
|
+
raise last_error
|
|
578
|
+
|
|
579
|
+
if not self._session_id:
|
|
580
|
+
raise ValueError("Invalid credentials.")
|
|
411
581
|
except _InactiveRpcError as e:
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
)
|
|
442
|
-
raise resume_error
|
|
582
|
+
if self._auto_resume:
|
|
583
|
+
if e.code() == grpc.StatusCode.UNAVAILABLE and 'status: 503' in e.details():
|
|
584
|
+
status = ClusterManager(
|
|
585
|
+
host=self._host,
|
|
586
|
+
port=self._port,
|
|
587
|
+
user=self.__username,
|
|
588
|
+
password=self.__password,
|
|
589
|
+
secure_channel=self._secure_channel,
|
|
590
|
+
cluster_uuid=self.cluster_name,
|
|
591
|
+
timeout=self.grpc_auto_resume_timeout_seconds
|
|
592
|
+
).resume()
|
|
593
|
+
print(self._hash, self.cluster_name, 'Cluster resume status:', status, file=sys.stderr)
|
|
594
|
+
if status:
|
|
595
|
+
# return self.get_session_id
|
|
596
|
+
authenticate_request = e6x_engine_pb2.AuthenticateRequest(
|
|
597
|
+
user=self.__username,
|
|
598
|
+
password=self.__password
|
|
599
|
+
)
|
|
600
|
+
authenticate_response = self._client.authenticate(
|
|
601
|
+
authenticate_request,
|
|
602
|
+
metadata=_get_grpc_header(cluster=self.cluster_name, strategy=_get_active_strategy())
|
|
603
|
+
)
|
|
604
|
+
self._session_id = authenticate_response.sessionId
|
|
605
|
+
# Check for new strategy in authenticate response
|
|
606
|
+
if hasattr(authenticate_response, 'new_strategy') and authenticate_response.new_strategy:
|
|
607
|
+
new_strategy = authenticate_response.new_strategy.lower()
|
|
608
|
+
if new_strategy != _get_active_strategy():
|
|
609
|
+
_set_pending_strategy(new_strategy)
|
|
610
|
+
# Don't apply immediately - let new queries use fresh connections
|
|
443
611
|
else:
|
|
444
|
-
raise
|
|
612
|
+
raise e
|
|
445
613
|
else:
|
|
446
|
-
raise
|
|
614
|
+
raise e
|
|
447
615
|
else:
|
|
448
616
|
raise e
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
617
|
+
except Exception as e:
|
|
618
|
+
print(self._hash, self.cluster_name, 'Error during authentication:', e, file=sys.stderr)
|
|
619
|
+
self._channel.close()
|
|
620
|
+
raise e
|
|
453
621
|
return self._session_id
|
|
454
|
-
|
|
455
|
-
def _perform_cluster_resume(self) -> bool:
|
|
456
|
-
"""
|
|
457
|
-
Perform cluster resume operation.
|
|
458
|
-
|
|
459
|
-
Returns:
|
|
460
|
-
True if resume was successful, False otherwise
|
|
461
|
-
"""
|
|
462
|
-
try:
|
|
463
|
-
print(self._hash, f'Auto resuming cluster {self.cluster_name}...')
|
|
464
|
-
|
|
465
|
-
cluster_manager = ClusterManager(
|
|
466
|
-
host=self._host,
|
|
467
|
-
port=self._port,
|
|
468
|
-
user=self.__username,
|
|
469
|
-
password=self.__password,
|
|
470
|
-
secure_channel=self._secure_channel,
|
|
471
|
-
cluster_uuid=self.cluster_name,
|
|
472
|
-
timeout=self.grpc_auto_resume_timeout_seconds,
|
|
473
|
-
grpc_options=self._grpc_options
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
status = cluster_manager.resume()
|
|
477
|
-
print(self._hash, f'Cluster {self.cluster_name} resume result: {status}')
|
|
478
|
-
return status
|
|
479
|
-
|
|
480
|
-
except Exception as e:
|
|
481
|
-
print(self._hash, f'Cluster resume failed: {e}')
|
|
482
|
-
return False
|
|
483
622
|
|
|
484
623
|
def __enter__(self):
|
|
485
624
|
"""
|
|
@@ -514,10 +653,6 @@ class Connection(object):
|
|
|
514
653
|
if self._channel is not None:
|
|
515
654
|
self._channel.close()
|
|
516
655
|
self._channel = None
|
|
517
|
-
|
|
518
|
-
# Clean up session tracking
|
|
519
|
-
if hasattr(self, '_connection_id'):
|
|
520
|
-
_session_manager.remove_session(self._connection_id)
|
|
521
656
|
self._session_id = None
|
|
522
657
|
|
|
523
658
|
def check_connection(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.9rc4
|
|
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.
|
|
@@ -38,7 +38,7 @@ Dynamic: summary
|
|
|
38
38
|
|
|
39
39
|
# e6data Python Connector
|
|
40
40
|
|
|
41
|
-

|
|
42
42
|
|
|
43
43
|
## Introduction
|
|
44
44
|
|
|
@@ -5,7 +5,6 @@ pyproject.toml
|
|
|
5
5
|
setup.cfg
|
|
6
6
|
setup.py
|
|
7
7
|
e6data_python_connector/__init__.py
|
|
8
|
-
e6data_python_connector/auto_resume_coordinator.py
|
|
9
8
|
e6data_python_connector/cluster_manager.py
|
|
10
9
|
e6data_python_connector/common.py
|
|
11
10
|
e6data_python_connector/constants.py
|
|
@@ -14,9 +13,6 @@ e6data_python_connector/date_time_utils.py
|
|
|
14
13
|
e6data_python_connector/dialect.py
|
|
15
14
|
e6data_python_connector/e6data_grpc.py
|
|
16
15
|
e6data_python_connector/exceptions.py
|
|
17
|
-
e6data_python_connector/process_safe_manager.py
|
|
18
|
-
e6data_python_connector/retry_handler.py
|
|
19
|
-
e6data_python_connector/session_manager.py
|
|
20
16
|
e6data_python_connector/strategy.py
|
|
21
17
|
e6data_python_connector/typeId.py
|
|
22
18
|
e6data_python_connector.egg-info/PKG-INFO
|