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.
Files changed (82) hide show
  1. {e6data_python_connector-2.3.9rc2/e6data_python_connector.egg-info → e6data_python_connector-2.3.9rc4}/PKG-INFO +2 -2
  2. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/README.md +1 -1
  3. e6data_python_connector-2.3.9rc4/e6data_python_connector/__init__.py +3 -0
  4. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_manager.py +4 -22
  5. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6data_grpc.py +272 -137
  6. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4/e6data_python_connector.egg-info}/PKG-INFO +2 -2
  7. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -4
  8. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/setup.py +1 -1
  9. e6data_python_connector-2.3.9rc2/e6data_python_connector/__init__.py +0 -42
  10. e6data_python_connector-2.3.9rc2/e6data_python_connector/auto_resume_coordinator.py +0 -441
  11. e6data_python_connector-2.3.9rc2/e6data_python_connector/process_safe_manager.py +0 -455
  12. e6data_python_connector-2.3.9rc2/e6data_python_connector/retry_handler.py +0 -354
  13. e6data_python_connector-2.3.9rc2/e6data_python_connector/session_manager.py +0 -383
  14. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/LICENSE +0 -0
  15. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/MANIFEST.in +0 -0
  16. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
  17. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  18. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  19. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/common.py +0 -0
  20. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/constants.py +0 -0
  21. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/datainputstream.py +0 -0
  22. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/date_time_utils.py +0 -0
  23. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/dialect.py +0 -0
  24. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  25. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
  26. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  27. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/exceptions.py +0 -0
  28. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
  29. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/__init__.py +0 -0
  30. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/constants.py +0 -0
  31. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  32. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  33. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  34. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/server/ttypes.py +0 -0
  35. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/strategy.py +0 -0
  36. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector/typeId.py +0 -0
  37. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  38. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  39. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
  40. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
  41. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/pyproject.toml +0 -0
  42. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/setup.cfg +0 -0
  43. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/__init__.py +0 -0
  44. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_38_nines.py +0 -0
  45. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_all_cases.py +0 -0
  46. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_binary.py +0 -0
  47. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_correct_value.py +0 -0
  48. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/analyze_fields.py +0 -0
  49. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/check_decimal_errors.py +0 -0
  50. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/cleanup_test_files.py +0 -0
  51. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/debug_38_nines.py +0 -0
  52. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/debug_binary.py +0 -0
  53. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/final_test.py +0 -0
  54. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/move_tests.py +0 -0
  55. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/quick_test.py +0 -0
  56. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_38_nines.py +0 -0
  57. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_all_decimal128_cases.py +0 -0
  58. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_efficiency.py +0 -0
  59. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_none_strategy.py +0 -0
  60. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_cluster_manager_strategy.py +0 -0
  61. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_comprehensive.py +0 -0
  62. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_current_implementation.py +0 -0
  63. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_decimal128_binary_parsing.py +0 -0
  64. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_decimal128_parsing.py +0 -0
  65. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_fix.py +0 -0
  66. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_improved_parsing.py +0 -0
  67. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_known_case.py +0 -0
  68. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_manual_analysis.py +0 -0
  69. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_mock_server.py +0 -0
  70. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_multiprocessing_fix.py +0 -0
  71. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_new_implementation.py +0 -0
  72. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_specific_binary.py +0 -0
  73. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy.py +0 -0
  74. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_logic.py +0 -0
  75. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_persistence_fix.py +0 -0
  76. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_strategy_sharing_fix.py +0 -0
  77. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/test_user_binary_value.py +0 -0
  78. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/tests.py +0 -0
  79. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/tests_grpc.py +0 -0
  80. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/validate_decimal128.py +0 -0
  81. {e6data_python_connector-2.3.9rc2 → e6data_python_connector-2.3.9rc4}/test/validate_implementation.py +0 -0
  82. {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.9rc2
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
- ![version](https://img.shields.io/badge/version-2.3.9rc2-blue.svg)
41
+ ![version](https://img.shields.io/badge/version-2.3.9rc4-blue.svg)
42
42
 
43
43
  ## Introduction
44
44
 
@@ -1,6 +1,6 @@
1
1
  # e6data Python Connector
2
2
 
3
- ![version](https://img.shields.io/badge/version-2.3.9rc2-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.9rc4-blue.svg)
4
4
 
5
5
  ## Introduction
6
6
 
@@ -0,0 +1,3 @@
1
+ from e6data_python_connector.e6data_grpc import Connection, Cursor
2
+
3
+ __all__ = ['Connection', 'Cursor']
@@ -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
- retry_handler = get_retry_handler('lock_acquisition')
99
-
100
- def acquire_locks():
101
- thread_acquired = self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
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
- # Initialize global managers
119
- _strategy_manager = get_strategy_manager()
120
- _resume_coordinator = get_resume_coordinator()
121
- _session_manager = get_session_manager()
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 shared strategy state (deprecated - use strategy manager directly)."""
126
- # For backward compatibility
127
- return {
128
- 'active_strategy': _strategy_manager.get_active_strategy(),
129
- 'pending_strategy': _strategy_manager.get_pending_strategy(),
130
- 'session_generation': _strategy_manager.get_session_generation()
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
- return _strategy_manager.get_active_strategy()
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
- return _strategy_manager.set_active_strategy(strategy)
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
- return _strategy_manager.clear_strategy_cache()
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
- return _strategy_manager.set_pending_strategy(strategy)
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
- return _strategy_manager.apply_pending_strategy()
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
- return _strategy_manager.invalidate_all_sessions()
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
- return _strategy_manager.register_query_strategy(query_id, strategy)
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
- return _strategy_manager.get_query_strategy(query_id)
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
- return _strategy_manager.cleanup_query_strategy(query_id)
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
- return _strategy_manager.get_debug_info()
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 for external session invalidation
376
- if self._session_invalidated_externally:
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
- # Check session validity through session manager
383
- session_info = _session_manager.get_session(self._connection_id)
384
- if session_info and session_info.is_valid:
385
- self._session_id = session_info.session_id
386
- return self._session_id
387
- elif self._session_id:
388
- # Session exists but is invalid - clear it
389
- self._session_id = None
390
- _session_manager.remove_session(self._connection_id)
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
- session_id = _session_manager.authenticate_with_retry(self)
407
- if session_id:
408
- self._session_id = session_id
409
- else:
410
- raise ValueError("Authentication failed")
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
- # Handle 503 errors with auto-resume
413
- if (self._auto_resume and
414
- e.code() == grpc.StatusCode.UNAVAILABLE and
415
- 'status: 503' in e.details()):
416
-
417
- should_resume, reason = _resume_coordinator.should_attempt_resume(
418
- self._host, self._port, self.cluster_name
419
- )
420
-
421
- if should_resume:
422
- if _resume_coordinator.start_resume_attempt(self._host, self._port, self.cluster_name):
423
- try:
424
- success = self._perform_cluster_resume()
425
- _resume_coordinator.complete_resume_attempt(
426
- self._host, self._port, self.cluster_name, success
427
- )
428
-
429
- if success:
430
- # Try authentication again after successful resume
431
- session_id = _session_manager.authenticate_with_retry(self)
432
- if session_id:
433
- self._session_id = session_id
434
- else:
435
- raise ValueError("Authentication failed even after cluster resume")
436
- else:
437
- raise ValueError("Cluster resume failed")
438
- except Exception as resume_error:
439
- _resume_coordinator.complete_resume_attempt(
440
- self._host, self._port, self.cluster_name, False, str(resume_error)
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 ValueError("Cannot attempt resume: another process is handling it")
612
+ raise e
445
613
  else:
446
- raise ValueError(f"Cannot resume cluster: {reason}")
614
+ raise e
447
615
  else:
448
616
  raise e
449
-
450
- if not self._session_id:
451
- raise ValueError("Failed to obtain valid session ID")
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.9rc2
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
- ![version](https://img.shields.io/badge/version-2.3.9rc2-blue.svg)
41
+ ![version](https://img.shields.io/badge/version-2.3.9rc4-blue.svg)
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
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (2, 3, 9,'rc2')
15
+ VERSION = (2, 3, 9,'rc4')
16
16
 
17
17
 
18
18
  def get_long_desc():