e6data-python-connector 2.3.8__tar.gz → 2.3.9rc2__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.8/e6data_python_connector.egg-info → e6data_python_connector-2.3.9rc2}/PKG-INFO +2 -2
  2. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/README.md +1 -1
  3. e6data_python_connector-2.3.9rc2/e6data_python_connector/__init__.py +42 -0
  4. e6data_python_connector-2.3.9rc2/e6data_python_connector/auto_resume_coordinator.py +441 -0
  5. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_manager.py +22 -4
  6. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6data_grpc.py +137 -267
  7. e6data_python_connector-2.3.9rc2/e6data_python_connector/process_safe_manager.py +455 -0
  8. e6data_python_connector-2.3.9rc2/e6data_python_connector/retry_handler.py +354 -0
  9. e6data_python_connector-2.3.9rc2/e6data_python_connector/session_manager.py +383 -0
  10. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2/e6data_python_connector.egg-info}/PKG-INFO +2 -2
  11. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/SOURCES.txt +4 -0
  12. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/setup.py +1 -1
  13. e6data_python_connector-2.3.8/e6data_python_connector/__init__.py +0 -3
  14. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/LICENSE +0 -0
  15. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/MANIFEST.in +0 -0
  16. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
  17. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  18. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  19. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/common.py +0 -0
  20. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/constants.py +0 -0
  21. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/datainputstream.py +0 -0
  22. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/date_time_utils.py +0 -0
  23. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/dialect.py +0 -0
  24. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  25. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
  26. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  27. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/exceptions.py +0 -0
  28. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
  29. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/__init__.py +0 -0
  30. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/constants.py +0 -0
  31. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  32. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  33. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  34. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/ttypes.py +0 -0
  35. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/strategy.py +0 -0
  36. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/typeId.py +0 -0
  37. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  38. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  39. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
  40. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
  41. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/pyproject.toml +0 -0
  42. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/setup.cfg +0 -0
  43. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/__init__.py +0 -0
  44. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_38_nines.py +0 -0
  45. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_all_cases.py +0 -0
  46. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_binary.py +0 -0
  47. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_correct_value.py +0 -0
  48. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_fields.py +0 -0
  49. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/check_decimal_errors.py +0 -0
  50. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/cleanup_test_files.py +0 -0
  51. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/debug_38_nines.py +0 -0
  52. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/debug_binary.py +0 -0
  53. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/final_test.py +0 -0
  54. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/move_tests.py +0 -0
  55. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/quick_test.py +0 -0
  56. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_38_nines.py +0 -0
  57. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_all_decimal128_cases.py +0 -0
  58. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_efficiency.py +0 -0
  59. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_none_strategy.py +0 -0
  60. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_strategy.py +0 -0
  61. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_comprehensive.py +0 -0
  62. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_current_implementation.py +0 -0
  63. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_decimal128_binary_parsing.py +0 -0
  64. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_decimal128_parsing.py +0 -0
  65. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_fix.py +0 -0
  66. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_improved_parsing.py +0 -0
  67. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_known_case.py +0 -0
  68. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_manual_analysis.py +0 -0
  69. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_mock_server.py +0 -0
  70. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_multiprocessing_fix.py +0 -0
  71. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_new_implementation.py +0 -0
  72. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_specific_binary.py +0 -0
  73. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy.py +0 -0
  74. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_logic.py +0 -0
  75. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_persistence_fix.py +0 -0
  76. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_sharing_fix.py +0 -0
  77. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_user_binary_value.py +0 -0
  78. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/tests.py +0 -0
  79. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/tests_grpc.py +0 -0
  80. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/validate_decimal128.py +0 -0
  81. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/validate_implementation.py +0 -0
  82. {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/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.8
3
+ Version: 2.3.9rc2
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.8-blue.svg)
41
+ ![version](https://img.shields.io/badge/version-2.3.9rc2-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.8-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.9rc2-blue.svg)
4
4
 
5
5
  ## Introduction
6
6
 
@@ -0,0 +1,42 @@
1
+ """
2
+ e6data Python Connector - Enhanced with Process-Safe Auto-Resume
3
+
4
+ A DB-API 2.0 compliant database connector for the e6data distributed SQL Engine
5
+ with robust process-safe synchronization for high-concurrency environments.
6
+ """
7
+
8
+ # Version info
9
+ __version__ = "1.0.0-process-safe"
10
+ __author__ = "e6data Team"
11
+
12
+ # Core connection interface
13
+ from e6data_python_connector.e6data_grpc import Connection, Cursor
14
+
15
+ # Process-safe components (optional imports for advanced use cases)
16
+ try:
17
+ from .process_safe_manager import get_strategy_manager
18
+ from .auto_resume_coordinator import get_resume_coordinator
19
+ from .session_manager import get_session_manager
20
+ from .retry_handler import RetryHandler, RetryConfig
21
+
22
+ # Mark as available
23
+ __has_process_safe_features__ = True
24
+
25
+ except ImportError as e:
26
+ # Graceful degradation if components are missing
27
+ __has_process_safe_features__ = False
28
+ import warnings
29
+ warnings.warn(f"Process-safe features not available: {e}", ImportWarning)
30
+
31
+ # Public API
32
+ __all__ = ['Connection', 'Cursor']
33
+
34
+ # Add process-safe components to public API if available
35
+ if __has_process_safe_features__:
36
+ __all__.extend([
37
+ 'get_strategy_manager',
38
+ 'get_resume_coordinator',
39
+ 'get_session_manager',
40
+ 'RetryHandler',
41
+ 'RetryConfig'
42
+ ])
@@ -0,0 +1,441 @@
1
+ """
2
+ Auto-resume coordinator for process-safe cluster resume operations.
3
+
4
+ This module provides coordinated cluster resume functionality that prevents
5
+ multiple processes from attempting to resume the same cluster simultaneously.
6
+ """
7
+
8
+ import logging
9
+ import multiprocessing
10
+ import threading
11
+ import time
12
+ import hashlib
13
+ from typing import Optional, Dict, Any, Tuple
14
+ from dataclasses import dataclass
15
+
16
+ from .process_safe_manager import get_strategy_manager, DEFAULT_LOCK_TIMEOUT
17
+
18
+ # Set up logging
19
+ _logger = logging.getLogger(__name__)
20
+
21
+ # Configuration constants
22
+ AUTO_RESUME_COOLDOWN = 60 # 1 minute between resume attempts per cluster
23
+ MAX_RESUME_ATTEMPTS = 3
24
+ RESUME_LOCK_TIMEOUT = 30.0 # 30 seconds
25
+ RESUME_STATUS_TIMEOUT = 600 # 10 minutes maximum wait for resume
26
+
27
+
28
+ @dataclass
29
+ class ResumeAttempt:
30
+ """Information about a cluster resume attempt."""
31
+ cluster_id: str
32
+ timestamp: float
33
+ worker_id: str
34
+ attempt_count: int
35
+ status: str # 'in_progress', 'completed', 'failed'
36
+ result: Optional[bool] = None
37
+
38
+
39
+ class ProcessSafeResumeCoordinator:
40
+ """
41
+ Coordinates cluster resume operations across multiple processes.
42
+
43
+ Ensures that only one process attempts to resume a cluster at a time,
44
+ and provides proper coordination and status tracking.
45
+ """
46
+
47
+ def __init__(self):
48
+ self._manager = None
49
+ self._shared_state = None
50
+ self._initialization_lock = threading.Lock()
51
+ self._fallback_state = {
52
+ 'active_resumes': {}, # cluster_id -> ResumeAttempt
53
+ 'resume_history': {}, # cluster_id -> list of attempts
54
+ 'global_lock_holder': None,
55
+ 'last_cleanup': 0
56
+ }
57
+ self._is_fallback_mode = False
58
+ self._worker_id = self._generate_worker_id()
59
+
60
+ def _generate_worker_id(self) -> str:
61
+ """Generate a unique worker ID for this process."""
62
+ import os
63
+ pid = os.getpid()
64
+ tid = threading.get_ident()
65
+ timestamp = time.time()
66
+ hash_input = f"{pid}_{tid}_{timestamp}".encode()
67
+ return hashlib.md5(hash_input).hexdigest()[:8]
68
+
69
+ def _initialize_manager(self):
70
+ """Initialize multiprocessing manager with retry logic."""
71
+ if self._shared_state is not None:
72
+ return True
73
+
74
+ with self._initialization_lock:
75
+ if self._shared_state is not None:
76
+ return True
77
+
78
+ try:
79
+ # Try to reuse the strategy manager's multiprocessing manager
80
+ strategy_manager = get_strategy_manager()
81
+ if hasattr(strategy_manager, '_manager') and strategy_manager._manager:
82
+ self._manager = strategy_manager._manager
83
+ else:
84
+ self._manager = multiprocessing.Manager()
85
+
86
+ # Initialize shared state
87
+ self._shared_state = self._manager.dict()
88
+ self._shared_state['active_resumes'] = self._manager.dict()
89
+ self._shared_state['resume_history'] = self._manager.dict()
90
+ self._shared_state['global_lock'] = self._manager.Lock()
91
+ self._shared_state['last_cleanup'] = 0
92
+
93
+ _logger.info("Successfully initialized auto-resume coordinator")
94
+ return True
95
+
96
+ except Exception as e:
97
+ _logger.error(f"Failed to initialize auto-resume coordinator: {e}")
98
+ self._is_fallback_mode = True
99
+ return False
100
+
101
+ def _get_state(self):
102
+ """Get shared state, initializing if necessary."""
103
+ if self._is_fallback_mode:
104
+ return self._fallback_state
105
+
106
+ if self._shared_state is None:
107
+ if not self._initialize_manager():
108
+ return self._fallback_state
109
+
110
+ return self._shared_state
111
+
112
+ def _cleanup_old_attempts(self, state):
113
+ """Clean up old resume attempts to prevent memory leaks."""
114
+ current_time = time.time()
115
+ last_cleanup = state.get('last_cleanup', 0)
116
+
117
+ # Only cleanup every 5 minutes
118
+ if current_time - last_cleanup < 300:
119
+ return
120
+
121
+ try:
122
+ if self._is_fallback_mode:
123
+ # Clean up old active resumes
124
+ active_resumes = state.get('active_resumes', {})
125
+ expired_keys = []
126
+ for cluster_id, attempt_data in active_resumes.items():
127
+ if current_time - attempt_data['timestamp'] > RESUME_STATUS_TIMEOUT:
128
+ expired_keys.append(cluster_id)
129
+
130
+ for key in expired_keys:
131
+ del active_resumes[key]
132
+
133
+ # Clean up old history entries (keep last 10 per cluster)
134
+ resume_history = state.get('resume_history', {})
135
+ for cluster_id, attempts in resume_history.items():
136
+ if len(attempts) > 10:
137
+ attempts[:] = attempts[-10:]
138
+
139
+ state['last_cleanup'] = current_time
140
+
141
+ else:
142
+ # Process-safe cleanup
143
+ active_resumes = state.get('active_resumes', {})
144
+ if hasattr(active_resumes, 'items'):
145
+ expired_keys = []
146
+ for cluster_id, attempt_data in active_resumes.items():
147
+ if current_time - attempt_data.get('timestamp', 0) > RESUME_STATUS_TIMEOUT:
148
+ expired_keys.append(cluster_id)
149
+
150
+ for key in expired_keys:
151
+ try:
152
+ del active_resumes[key]
153
+ except KeyError:
154
+ pass # Already cleaned up by another process
155
+
156
+ state['last_cleanup'] = current_time
157
+
158
+ except Exception as e:
159
+ _logger.warning(f"Error during cleanup: {e}")
160
+
161
+ def _get_cluster_key(self, host: str, port: int, cluster_name: str) -> str:
162
+ """Generate a unique key for a cluster."""
163
+ return f"{host}:{port}:{cluster_name}"
164
+
165
+ def should_attempt_resume(self, host: str, port: int, cluster_name: str) -> Tuple[bool, Optional[str]]:
166
+ """
167
+ Check if this worker should attempt to resume the cluster.
168
+
169
+ Returns:
170
+ Tuple of (should_resume, reason)
171
+ """
172
+ cluster_key = self._get_cluster_key(host, port, cluster_name)
173
+ current_time = time.time()
174
+ state = self._get_state()
175
+
176
+ # Cleanup old attempts
177
+ self._cleanup_old_attempts(state)
178
+
179
+ if self._is_fallback_mode:
180
+ active_resumes = state.get('active_resumes', {})
181
+
182
+ # Check if there's an active resume for this cluster
183
+ if cluster_key in active_resumes:
184
+ attempt = active_resumes[cluster_key]
185
+ if current_time - attempt['timestamp'] < RESUME_STATUS_TIMEOUT:
186
+ if attempt['status'] == 'in_progress':
187
+ return False, f"Resume already in progress by worker {attempt['worker_id']}"
188
+ elif attempt['status'] == 'completed' and attempt.get('result'):
189
+ if current_time - attempt['timestamp'] < AUTO_RESUME_COOLDOWN:
190
+ return False, "Cluster recently resumed successfully"
191
+
192
+ # Check resume history for cooldown
193
+ resume_history = state.get('resume_history', {})
194
+ if cluster_key in resume_history:
195
+ recent_attempts = [
196
+ att for att in resume_history[cluster_key]
197
+ if current_time - att['timestamp'] < AUTO_RESUME_COOLDOWN
198
+ ]
199
+ if recent_attempts:
200
+ last_attempt = max(recent_attempts, key=lambda x: x['timestamp'])
201
+ if last_attempt['status'] == 'completed' and last_attempt.get('result'):
202
+ return False, "Cluster recently resumed (from history)"
203
+ elif len(recent_attempts) >= MAX_RESUME_ATTEMPTS:
204
+ return False, f"Too many recent resume attempts ({len(recent_attempts)})"
205
+
206
+ return True, "No recent resume activity"
207
+
208
+ else:
209
+ # Process-safe implementation
210
+ try:
211
+ global_lock = state.get('global_lock')
212
+ if global_lock and global_lock.acquire(timeout=5.0):
213
+ try:
214
+ active_resumes = state.get('active_resumes', {})
215
+
216
+ # Check if there's an active resume for this cluster
217
+ if cluster_key in active_resumes:
218
+ attempt_data = active_resumes[cluster_key]
219
+ if current_time - attempt_data.get('timestamp', 0) < RESUME_STATUS_TIMEOUT:
220
+ if attempt_data.get('status') == 'in_progress':
221
+ return False, f"Resume already in progress by worker {attempt_data.get('worker_id')}"
222
+ elif (attempt_data.get('status') == 'completed' and
223
+ attempt_data.get('result') and
224
+ current_time - attempt_data.get('timestamp', 0) < AUTO_RESUME_COOLDOWN):
225
+ return False, "Cluster recently resumed successfully"
226
+
227
+ return True, "No active resume in progress"
228
+
229
+ finally:
230
+ global_lock.release()
231
+ else:
232
+ _logger.warning("Failed to acquire global lock for resume check")
233
+ return False, "Lock contention - skip resume attempt"
234
+
235
+ except Exception as e:
236
+ _logger.error(f"Error checking resume eligibility: {e}")
237
+ return False, f"Error during check: {e}"
238
+
239
+ def start_resume_attempt(self, host: str, port: int, cluster_name: str) -> bool:
240
+ """
241
+ Register the start of a resume attempt.
242
+
243
+ Returns:
244
+ True if this worker should proceed with resume, False otherwise
245
+ """
246
+ cluster_key = self._get_cluster_key(host, port, cluster_name)
247
+ current_time = time.time()
248
+ state = self._get_state()
249
+
250
+ attempt = {
251
+ 'cluster_id': cluster_key,
252
+ 'timestamp': current_time,
253
+ 'worker_id': self._worker_id,
254
+ 'attempt_count': 1,
255
+ 'status': 'in_progress',
256
+ 'result': None
257
+ }
258
+
259
+ if self._is_fallback_mode:
260
+ active_resumes = state.get('active_resumes', {})
261
+
262
+ # Double-check that no one else started while we were deciding
263
+ if cluster_key in active_resumes:
264
+ existing = active_resumes[cluster_key]
265
+ if (existing['status'] == 'in_progress' and
266
+ current_time - existing['timestamp'] < RESUME_STATUS_TIMEOUT):
267
+ return False
268
+
269
+ # Register our attempt
270
+ active_resumes[cluster_key] = attempt
271
+ _logger.info(f"Worker {self._worker_id} starting resume for cluster {cluster_key}")
272
+ return True
273
+
274
+ else:
275
+ try:
276
+ global_lock = state.get('global_lock')
277
+ if global_lock and global_lock.acquire(timeout=RESUME_LOCK_TIMEOUT):
278
+ try:
279
+ active_resumes = state.get('active_resumes', {})
280
+
281
+ # Double-check that no one else started while we were waiting
282
+ if cluster_key in active_resumes:
283
+ existing = active_resumes[cluster_key]
284
+ if (existing.get('status') == 'in_progress' and
285
+ current_time - existing.get('timestamp', 0) < RESUME_STATUS_TIMEOUT):
286
+ return False
287
+
288
+ # Register our attempt
289
+ active_resumes[cluster_key] = attempt
290
+ _logger.info(f"Worker {self._worker_id} starting resume for cluster {cluster_key}")
291
+ return True
292
+
293
+ finally:
294
+ global_lock.release()
295
+ else:
296
+ _logger.warning(f"Failed to acquire resume lock for {cluster_key} within {RESUME_LOCK_TIMEOUT}s")
297
+ return False
298
+
299
+ except Exception as e:
300
+ _logger.error(f"Error starting resume attempt: {e}")
301
+ return False
302
+
303
+ def complete_resume_attempt(self, host: str, port: int, cluster_name: str,
304
+ success: bool, error_message: Optional[str] = None):
305
+ """
306
+ Mark a resume attempt as completed.
307
+
308
+ Args:
309
+ host: Cluster host
310
+ port: Cluster port
311
+ cluster_name: Cluster name
312
+ success: Whether the resume was successful
313
+ error_message: Error message if failed
314
+ """
315
+ cluster_key = self._get_cluster_key(host, port, cluster_name)
316
+ current_time = time.time()
317
+ state = self._get_state()
318
+
319
+ if self._is_fallback_mode:
320
+ active_resumes = state.get('active_resumes', {})
321
+
322
+ if cluster_key in active_resumes:
323
+ attempt = active_resumes[cluster_key]
324
+ if attempt['worker_id'] == self._worker_id:
325
+ attempt['status'] = 'completed' if success else 'failed'
326
+ attempt['result'] = success
327
+ attempt['completion_time'] = current_time
328
+ if error_message:
329
+ attempt['error'] = error_message
330
+
331
+ # Add to history
332
+ resume_history = state.get('resume_history', {})
333
+ if cluster_key not in resume_history:
334
+ resume_history[cluster_key] = []
335
+ resume_history[cluster_key].append(dict(attempt))
336
+
337
+ _logger.info(f"Worker {self._worker_id} completed resume for {cluster_key}: {'success' if success else 'failed'}")
338
+
339
+ else:
340
+ try:
341
+ global_lock = state.get('global_lock')
342
+ if global_lock and global_lock.acquire(timeout=10.0):
343
+ try:
344
+ active_resumes = state.get('active_resumes', {})
345
+
346
+ if cluster_key in active_resumes:
347
+ attempt_data = active_resumes[cluster_key]
348
+ if attempt_data.get('worker_id') == self._worker_id:
349
+ # Update the attempt
350
+ attempt_data['status'] = 'completed' if success else 'failed'
351
+ attempt_data['result'] = success
352
+ attempt_data['completion_time'] = current_time
353
+ if error_message:
354
+ attempt_data['error'] = error_message
355
+
356
+ active_resumes[cluster_key] = attempt_data
357
+
358
+ _logger.info(f"Worker {self._worker_id} completed resume for {cluster_key}: {'success' if success else 'failed'}")
359
+
360
+ finally:
361
+ global_lock.release()
362
+
363
+ except Exception as e:
364
+ _logger.error(f"Error completing resume attempt: {e}")
365
+
366
+ def get_resume_status(self, host: str, port: int, cluster_name: str) -> Optional[Dict[str, Any]]:
367
+ """Get the current resume status for a cluster."""
368
+ cluster_key = self._get_cluster_key(host, port, cluster_name)
369
+ state = self._get_state()
370
+
371
+ if self._is_fallback_mode:
372
+ active_resumes = state.get('active_resumes', {})
373
+ return active_resumes.get(cluster_key)
374
+ else:
375
+ try:
376
+ global_lock = state.get('global_lock')
377
+ if global_lock and global_lock.acquire(timeout=5.0):
378
+ try:
379
+ active_resumes = state.get('active_resumes', {})
380
+ return active_resumes.get(cluster_key)
381
+ finally:
382
+ global_lock.release()
383
+ except Exception as e:
384
+ _logger.error(f"Error getting resume status: {e}")
385
+ return None
386
+
387
+ def get_debug_info(self) -> Dict[str, Any]:
388
+ """Get debug information about resume coordinator state."""
389
+ state = self._get_state()
390
+ current_time = time.time()
391
+
392
+ if self._is_fallback_mode:
393
+ active_resumes = state.get('active_resumes', {})
394
+ resume_history = state.get('resume_history', {})
395
+
396
+ return {
397
+ 'worker_id': self._worker_id,
398
+ 'is_fallback_mode': True,
399
+ 'active_resume_count': len(active_resumes),
400
+ 'total_clusters_in_history': len(resume_history),
401
+ 'current_time': current_time,
402
+ 'last_cleanup': state.get('last_cleanup', 0)
403
+ }
404
+ else:
405
+ try:
406
+ global_lock = state.get('global_lock')
407
+ if global_lock and global_lock.acquire(timeout=2.0):
408
+ try:
409
+ active_resumes = state.get('active_resumes', {})
410
+ return {
411
+ 'worker_id': self._worker_id,
412
+ 'is_fallback_mode': False,
413
+ 'active_resume_count': len(active_resumes) if active_resumes else 0,
414
+ 'current_time': current_time,
415
+ 'last_cleanup': state.get('last_cleanup', 0)
416
+ }
417
+ finally:
418
+ global_lock.release()
419
+ else:
420
+ return {
421
+ 'worker_id': self._worker_id,
422
+ 'is_fallback_mode': False,
423
+ 'error': 'Failed to acquire lock for debug info',
424
+ 'current_time': current_time
425
+ }
426
+ except Exception as e:
427
+ return {
428
+ 'worker_id': self._worker_id,
429
+ 'is_fallback_mode': False,
430
+ 'error': str(e),
431
+ 'current_time': current_time
432
+ }
433
+
434
+
435
+ # Global instance
436
+ _resume_coordinator = ProcessSafeResumeCoordinator()
437
+
438
+
439
+ def get_resume_coordinator() -> ProcessSafeResumeCoordinator:
440
+ """Get the global resume coordinator instance."""
441
+ return _resume_coordinator
@@ -8,6 +8,7 @@ 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
11
12
 
12
13
 
13
14
  def _get_grpc_header(engine_ip=None, cluster=None, strategy=None):
@@ -86,16 +87,33 @@ class _StatusLock:
86
87
  Acquires both the thread-level lock and process-level semaphore
87
88
  to ensure the current operation can safely access shared resources.
88
89
 
90
+ Uses retry logic to handle lock contention in high-concurrency environments.
91
+
89
92
  Raises:
90
93
  TimeoutError: If the lock cannot be acquired within the timeout period.
91
94
 
92
95
  Returns:
93
96
  _StatusLock: The current instance of the lock, used for context management.
94
97
  """
95
-
96
- self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
97
- self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT)
98
- return self
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")
99
117
 
100
118
  def __exit__(self, exc_type, exc_val, exc_tb):
101
119
  """