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.
- {e6data_python_connector-2.3.8/e6data_python_connector.egg-info → e6data_python_connector-2.3.9rc2}/PKG-INFO +2 -2
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/README.md +1 -1
- e6data_python_connector-2.3.9rc2/e6data_python_connector/__init__.py +42 -0
- e6data_python_connector-2.3.9rc2/e6data_python_connector/auto_resume_coordinator.py +441 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_manager.py +22 -4
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6data_grpc.py +137 -267
- e6data_python_connector-2.3.9rc2/e6data_python_connector/process_safe_manager.py +455 -0
- e6data_python_connector-2.3.9rc2/e6data_python_connector/retry_handler.py +354 -0
- e6data_python_connector-2.3.9rc2/e6data_python_connector/session_manager.py +383 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2/e6data_python_connector.egg-info}/PKG-INFO +2 -2
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/SOURCES.txt +4 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/setup.py +1 -1
- e6data_python_connector-2.3.8/e6data_python_connector/__init__.py +0 -3
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/LICENSE +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/MANIFEST.in +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/dialect.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/pyproject.toml +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/setup.cfg +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/__init__.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_binary.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/analyze_fields.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/debug_binary.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/final_test.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/move_tests.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/quick_test.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_38_nines.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_fix.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_known_case.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_mock_server.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/tests.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/tests_grpc.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-2.3.8 → e6data_python_connector-2.3.9rc2}/test/validate_implementation.py +0 -0
- {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.
|
|
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
|
-

|
|
42
42
|
|
|
43
43
|
## Introduction
|
|
44
44
|
|
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
"""
|