shared-tensor 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,361 @@
1
+ """
2
+ Async Task Management System
3
+
4
+ Provides task queue and execution management for long-running functions
5
+ that shouldn't be limited by HTTP timeouts.
6
+ """
7
+
8
+ import uuid
9
+ import time
10
+ import pickle
11
+ import threading
12
+ import queue
13
+ import logging
14
+ from enum import Enum
15
+ from typing import Any, Dict, Optional
16
+ from dataclasses import dataclass, asdict
17
+ from concurrent.futures import ThreadPoolExecutor
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class TaskStatus(Enum):
23
+ """Task execution status"""
24
+ PENDING = "pending" # Task submitted, waiting to start
25
+ RUNNING = "running" # Task currently executing
26
+ COMPLETED = "completed" # Task finished successfully
27
+ FAILED = "failed" # Task failed with error
28
+ CANCELLED = "cancelled" # Task was cancelled
29
+
30
+
31
+ @dataclass
32
+ class TaskInfo:
33
+ """Task information container"""
34
+ task_id: str
35
+ function_path: str
36
+ args_hex: str
37
+ kwargs_hex: str
38
+ options: Dict[str, Any]
39
+ status: TaskStatus
40
+ created_at: float
41
+ started_at: Optional[float] = None
42
+ completed_at: Optional[float] = None
43
+ result_hex: Optional[str] = None
44
+ error_message: Optional[str] = None
45
+ progress: Optional[Dict[str, Any]] = None
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ """Convert to dictionary for JSON serialization"""
49
+ data = asdict(self)
50
+ data['status'] = self.status.value
51
+ return data
52
+
53
+ @classmethod
54
+ def from_dict(cls, data: Dict[str, Any]) -> 'TaskInfo':
55
+ """Create from dictionary"""
56
+ data['status'] = TaskStatus(data['status'])
57
+ return cls(**data)
58
+
59
+
60
+ class TaskExecutor:
61
+ """Executes tasks in background threads"""
62
+
63
+ def __init__(self, max_workers: int = 4):
64
+ self.max_workers = max_workers
65
+ self.executor = ThreadPoolExecutor(max_workers=max_workers)
66
+ self.running_tasks: Dict[str, threading.Future] = {}
67
+ self._shutdown = False
68
+
69
+ def submit_task(self, task_info: TaskInfo, task_manager: 'TaskManager') -> bool:
70
+ """Submit a task for execution"""
71
+ if self._shutdown:
72
+ return False
73
+
74
+ try:
75
+ future = self.executor.submit(self._execute_task, task_info, task_manager)
76
+ self.running_tasks[task_info.task_id] = future
77
+ return True
78
+ except Exception as e:
79
+ logger.error(f"Failed to submit task {task_info.task_id}: {e}")
80
+ return False
81
+
82
+ def _execute_task(self, task_info: TaskInfo, task_manager: 'TaskManager'):
83
+ """Execute a single task"""
84
+ from shared_tensor.utils import import_function_from_path, serialize_result, deserialize_args
85
+
86
+ task_id = task_info.task_id
87
+ logger.info(f"Starting task execution: {task_id}")
88
+
89
+ try:
90
+ # Update task status to running
91
+ task_info.status = TaskStatus.RUNNING
92
+ task_info.started_at = time.time()
93
+ task_manager.update_task(task_info)
94
+
95
+ # Import the function
96
+ func = import_function_from_path(task_info.function_path)
97
+
98
+ # Deserialize arguments using ForkingPickler
99
+ args = deserialize_args(task_info.args_hex)
100
+ kwargs = deserialize_args(task_info.kwargs_hex) if task_info.kwargs_hex else {}
101
+
102
+ logger.debug(f"Executing {task_info.function_path} with args={args}, kwargs={kwargs}")
103
+
104
+ # Execute the function
105
+ result = func(*args, **kwargs)
106
+
107
+ # Serialize result using ForkingPickler
108
+ result_hex = serialize_result(result).hex()
109
+
110
+ # Update task with success
111
+ task_info.status = TaskStatus.COMPLETED
112
+ task_info.completed_at = time.time()
113
+ task_info.result_hex = result_hex
114
+ task_manager.update_task(task_info)
115
+
116
+ logger.info(f"Task completed successfully: {task_id}")
117
+
118
+ except Exception as e:
119
+ logger.error(f"Task execution failed: {task_id} - {e}")
120
+
121
+ # Update task with error
122
+ task_info.status = TaskStatus.FAILED
123
+ task_info.completed_at = time.time()
124
+ task_info.error_message = str(e)
125
+ task_manager.update_task(task_info)
126
+
127
+ finally:
128
+ # Clean up
129
+ self.running_tasks.pop(task_id, None)
130
+
131
+ def cancel_task(self, task_id: str) -> bool:
132
+ """Cancel a running task"""
133
+ future = self.running_tasks.get(task_id)
134
+ if future:
135
+ return future.cancel()
136
+ return False
137
+
138
+ def shutdown(self, wait: bool = True):
139
+ """Shutdown the executor"""
140
+ self._shutdown = True
141
+ self.executor.shutdown(wait=wait)
142
+
143
+
144
+ class TaskManager:
145
+ """Manages task lifecycle and storage"""
146
+
147
+ def __init__(self, max_tasks: int = 1000, cleanup_interval: int = 3600):
148
+ self.max_tasks = max_tasks
149
+ self.cleanup_interval = cleanup_interval
150
+ self._tasks: Dict[str, TaskInfo] = {}
151
+ self._lock = threading.RLock()
152
+ self._task_queue = queue.Queue()
153
+ self._executor = TaskExecutor()
154
+ self._running = False
155
+ self._worker_thread = None
156
+ self._cleanup_thread = None
157
+
158
+ def start(self):
159
+ """Start the task manager"""
160
+ if self._running:
161
+ return
162
+
163
+ self._running = True
164
+
165
+ # Start worker thread for processing tasks
166
+ self._worker_thread = threading.Thread(target=self._worker_loop, daemon=True)
167
+ self._worker_thread.start()
168
+
169
+ # Start cleanup thread
170
+ self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
171
+ self._cleanup_thread.start()
172
+
173
+ logger.info("TaskManager started")
174
+
175
+ def stop(self):
176
+ """Stop the task manager"""
177
+ self._running = False
178
+
179
+ if self._worker_thread:
180
+ self._worker_thread.join(timeout=5)
181
+
182
+ if self._cleanup_thread:
183
+ self._cleanup_thread.join(timeout=5)
184
+
185
+ self._executor.shutdown(wait=True)
186
+ logger.info("TaskManager stopped")
187
+
188
+ def submit_task(self, function_path: str, args: tuple = (), kwargs: Dict[str, Any] = None, options: Dict[str, Any] = None) -> str:
189
+ """Submit a new task for execution"""
190
+ if kwargs is None:
191
+ kwargs = {}
192
+
193
+ # Create task info
194
+ task_id = str(uuid.uuid4())
195
+
196
+ from shared_tensor.utils import serialize_result
197
+
198
+ args_hex = serialize_result(args).hex() if args else ""
199
+ kwargs_hex = serialize_result(kwargs).hex() if kwargs else ""
200
+
201
+ task_info = TaskInfo(
202
+ task_id=task_id,
203
+ function_path=function_path,
204
+ args_hex=args_hex,
205
+ kwargs_hex=kwargs_hex,
206
+ status=TaskStatus.PENDING,
207
+ created_at=time.time()
208
+ )
209
+
210
+ with self._lock:
211
+ self._tasks[task_id] = task_info
212
+ self._task_queue.put(task_id)
213
+
214
+ logger.info(f"Task submitted: {task_id} - {function_path}")
215
+ return task_id
216
+
217
+ def get_task(self, task_id: str) -> Optional[TaskInfo]:
218
+ """Get task information"""
219
+ with self._lock:
220
+ return self._tasks.get(task_id)
221
+
222
+ def update_task(self, task_info: TaskInfo):
223
+ """Update task information"""
224
+ with self._lock:
225
+ self._tasks[task_info.task_id] = task_info
226
+
227
+ def list_tasks(self, status: Optional[TaskStatus] = None) -> Dict[str, TaskInfo]:
228
+ """List tasks, optionally filtered by status"""
229
+ with self._lock:
230
+ if status is None:
231
+ return self._tasks.copy()
232
+ else:
233
+ return {tid: task for tid, task in self._tasks.items()
234
+ if task.status == status}
235
+
236
+ def cancel_task(self, task_id: str) -> bool:
237
+ """Cancel a task"""
238
+ task = self.get_task(task_id)
239
+ if not task:
240
+ return False
241
+
242
+ if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
243
+ return False
244
+
245
+ # Try to cancel if running
246
+ if task.status == TaskStatus.RUNNING:
247
+ cancelled = self._executor.cancel_task(task_id)
248
+ else:
249
+ cancelled = True
250
+
251
+ if cancelled:
252
+ task.status = TaskStatus.CANCELLED
253
+ task.completed_at = time.time()
254
+ self.update_task(task)
255
+
256
+ return cancelled
257
+
258
+ def get_task_result(self, task_id: str) -> Any:
259
+ """Get task result (raises exception if not completed)"""
260
+ task = self.get_task(task_id)
261
+ if not task:
262
+ raise ValueError(f"Task {task_id} not found")
263
+
264
+ if task.status == TaskStatus.FAILED:
265
+ raise RuntimeError(f"Task failed: {task.error_message}")
266
+
267
+ if task.status != TaskStatus.COMPLETED:
268
+ raise RuntimeError(f"Task not completed, current status: {task.status.value}")
269
+
270
+ if task.result_hex:
271
+ # Import deserialize function
272
+ from .utils import deserialize_args
273
+
274
+ # For consistency, we should actually use a deserialize_result function
275
+ # but for now we'll use the existing deserialize_args
276
+ try:
277
+ import torch
278
+ result_bytes = bytes.fromhex(task.result_hex)
279
+ return torch.multiprocessing.reducer.ForkingPickler.loads(result_bytes)
280
+ except ImportError:
281
+ return pickle.loads(bytes.fromhex(task.result_hex))
282
+ return None
283
+
284
+ def _worker_loop(self):
285
+ """Main worker loop for processing tasks"""
286
+ while self._running:
287
+ try:
288
+ # Get next task with timeout
289
+ try:
290
+ task_id = self._task_queue.get(timeout=1)
291
+ except queue.Empty:
292
+ continue
293
+
294
+ task = self.get_task(task_id)
295
+ if not task or task.status != TaskStatus.PENDING:
296
+ continue
297
+
298
+ # Submit to executor
299
+ self._executor.submit_task(task, self)
300
+
301
+ except Exception as e:
302
+ logger.error(f"Error in worker loop: {e}")
303
+
304
+ def _cleanup_loop(self):
305
+ """Cleanup old completed tasks"""
306
+ while self._running:
307
+ try:
308
+ time.sleep(self.cleanup_interval)
309
+
310
+ current_time = time.time()
311
+ to_remove = []
312
+
313
+ with self._lock:
314
+ for task_id, task in self._tasks.items():
315
+ # Remove completed tasks older than 1 hour
316
+ if (task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]
317
+ and task.completed_at
318
+ and current_time - task.completed_at > 3600):
319
+ to_remove.append(task_id)
320
+
321
+ # Also remove if we have too many tasks
322
+ if len(self._tasks) > self.max_tasks:
323
+ # Remove oldest completed tasks
324
+ completed_tasks = [(tid, task) for tid, task in self._tasks.items()
325
+ if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]]
326
+ completed_tasks.sort(key=lambda x: x[1].completed_at or 0)
327
+
328
+ excess_count = len(self._tasks) - self.max_tasks
329
+ for tid, _ in completed_tasks[:excess_count]:
330
+ to_remove.append(tid)
331
+
332
+ # Remove tasks
333
+ for task_id in to_remove:
334
+ self._tasks.pop(task_id, None)
335
+
336
+ if to_remove:
337
+ logger.info(f"Cleaned up {len(to_remove)} old tasks")
338
+
339
+ except Exception as e:
340
+ logger.error(f"Error in cleanup loop: {e}")
341
+
342
+
343
+ # Global task manager instance
344
+ _task_manager: Optional[TaskManager] = None
345
+
346
+
347
+ def get_task_manager() -> TaskManager:
348
+ """Get the global task manager instance"""
349
+ global _task_manager
350
+ if _task_manager is None:
351
+ _task_manager = TaskManager()
352
+ _task_manager.start()
353
+ return _task_manager
354
+
355
+
356
+ def shutdown_task_manager():
357
+ """Shutdown the global task manager"""
358
+ global _task_manager
359
+ if _task_manager:
360
+ _task_manager.stop()
361
+ _task_manager = None
@@ -0,0 +1,265 @@
1
+ """
2
+ Shared Tensor JSON-RPC Client
3
+
4
+ Handles communication with remote shared tensor servers using JSON-RPC 2.0 protocol.
5
+ """
6
+
7
+ import pickle
8
+ import requests
9
+ import logging
10
+ from typing import Any, Dict, Optional
11
+
12
+ import torch
13
+
14
+ from shared_tensor.errors import SharedTensorClientError, SharedTensorServerError
15
+ from shared_tensor.jsonrpc import (
16
+ JsonRpcRequest,
17
+ JsonRpcResponse,
18
+ JsonRpcErrorCodes,
19
+ parse_response,
20
+ )
21
+ from shared_tensor.utils import serialize_result
22
+
23
+
24
+ __all__ = ["SharedTensorClient", "execute_remote_function"]
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class SharedTensorClient:
30
+ """
31
+ JSON-RPC client for shared tensor communication
32
+ """
33
+
34
+ def __init__(self, server_port: int = 2537, timeout: int = 30, verbose_debug: bool = False):
35
+ """
36
+ Initialize the client
37
+
38
+ Args:
39
+ server_port: PORT of the shared tensor server
40
+ timeout: Request timeout in seconds, critical when sync provider used
41
+ verbose_debug: Whether to log detailed debug messages
42
+ """
43
+ self.server_url = f"http://localhost:{server_port}"
44
+ self.verbose_debug = verbose_debug
45
+ self.timeout = timeout
46
+
47
+ logger.debug(f"SharedTensorClient initialized with server URL: {self.server_url} with timeout {self.timeout} and verbose_debug {self.verbose_debug}")
48
+
49
+ self.session = requests.Session()
50
+
51
+ self.session.headers.update({
52
+ 'Content-Type': 'application/json',
53
+ 'User-Agent': 'SharedTensorClient/1.0'
54
+ })
55
+
56
+ def _send_request(self, request: JsonRpcRequest) -> JsonRpcResponse:
57
+ """
58
+ Send a JSON-RPC request to the server
59
+
60
+ Args:
61
+ request: JsonRpcRequest object
62
+
63
+ Returns:
64
+ JsonRpcResponse object
65
+
66
+ Raises:
67
+ SharedTensorClientError: If unable to connect to server
68
+ SharedTensorClientError: If request times out
69
+ SharedTensorClientError: If server response is invalid
70
+ """
71
+ try:
72
+ if self.verbose_debug:
73
+ logger.debug(f"Sending request to server {self.server_url}: {request.to_json()}")
74
+ else:
75
+ logger.debug(f"Sending request to server {self.server_url}")
76
+
77
+ response = self.session.post(
78
+ f"{self.server_url}/jsonrpc",
79
+ data=request.to_json(),
80
+ timeout=self.timeout
81
+ )
82
+ if self.verbose_debug:
83
+ logger.debug(f"Received response from server {self.server_url}: {response.text}")
84
+ else:
85
+ logger.debug(f"Received response from server {self.server_url}")
86
+
87
+ if response.status_code != 200:
88
+ logger.warning(f"Server {self.server_url} returned HTTP {response.status_code}: {response.text}")
89
+ raise SharedTensorClientError(
90
+ f"Server {self.server_url} returned HTTP {response.status_code}: {response.text}"
91
+ )
92
+ try:
93
+ rpc_response = parse_response(response.text)
94
+ if self.verbose_debug:
95
+ logger.debug(f"Parsed JSON-RPC response: {rpc_response}")
96
+ else:
97
+ logger.debug(f"Parsed JSON-RPC response from server {self.server_url}")
98
+ return rpc_response
99
+ except ValueError as e:
100
+ logger.warning(f"Invalid JSON-RPC response: {e}")
101
+ raise ValueError(f"Invalid JSON-RPC response: {e}")
102
+
103
+ except requests.exceptions.ConnectionError as e:
104
+ logger.warning(f"Unable to connect to server {self.server_url}: {e}")
105
+ raise SharedTensorClientError(f"Unable to connect to server {self.server_url}: {e}")
106
+ except requests.exceptions.Timeout as e:
107
+ logger.warning(f"Request to server {self.server_url} timed out after {self.timeout} seconds: {e}")
108
+ raise SharedTensorClientError(f"Request to server {self.server_url} timed out after {self.timeout} seconds: {e}")
109
+ except requests.exceptions.RequestException as e:
110
+ logger.warning(f"Request to server {self.server_url} failed: {e}")
111
+ raise SharedTensorClientError(f"Request to server {self.server_url} failed: {e}")
112
+
113
+ def _create_request(self, method: str, params: Optional[Dict[str, Any]] = None) -> JsonRpcRequest:
114
+ """Create a JSON-RPC request"""
115
+ return JsonRpcRequest(method=method, params=params)
116
+
117
+ def execute_function(
118
+ self,
119
+ function_path: str,
120
+ args: tuple = (),
121
+ kwargs: Dict[str, Any] = None,
122
+ options: Dict[str, Any] = None
123
+ ) -> Any:
124
+ """
125
+ Execute a function on the remote server
126
+
127
+ Args:
128
+ function_path: Function path in format "module.submodule:function_name"
129
+ args: Positional arguments for the function
130
+ kwargs: Keyword arguments for the function
131
+ options: Options for the function
132
+
133
+ Returns:
134
+ The result returned by the remote function
135
+
136
+ Raises:
137
+ SharedTensorClientError: If unable to communicate with server
138
+ SharedTensorServerError: If remote execution fails
139
+ """
140
+ if kwargs is None:
141
+ kwargs = {}
142
+
143
+ try:
144
+ serialized_args = serialize_result(args).hex()
145
+ serialized_kwargs = serialize_result(kwargs).hex()
146
+
147
+ request = JsonRpcRequest(
148
+ method="execute_function",
149
+ params={
150
+ "function_path": function_path,
151
+ "args": serialized_args,
152
+ "kwargs": serialized_kwargs,
153
+ "options": options,
154
+ "encoding": "pickle_hex"
155
+ }
156
+ )
157
+
158
+ response = self._send_request(request)
159
+
160
+ if response.error:
161
+ error_code = response.error.get('code', JsonRpcErrorCodes.INTERNAL_ERROR)
162
+ error_message = response.error.get('message', 'Unknown error')
163
+ error_data = response.error.get('data')
164
+
165
+ raise SharedTensorServerError(
166
+ f"Remote execution failed [{error_code}]: {error_message}"
167
+ + (f" - {error_data}" if error_data else "")
168
+ )
169
+
170
+ if response.result is None:
171
+ return None
172
+
173
+ result_data = response.result
174
+ if not isinstance(result_data, dict) or 'handler' not in result_data:
175
+ raise SharedTensorClientError("Invalid response format: missing 'handler' field")
176
+
177
+ handler_hex = result_data['handler']
178
+ handler_bytes = bytes.fromhex(handler_hex)
179
+ return torch.multiprocessing.reducer.ForkingPickler.loads(handler_bytes)
180
+
181
+ except (pickle.PickleError, ValueError) as e:
182
+ raise SharedTensorClientError(f"Serialization error: {e}")
183
+
184
+ def ping(self) -> bool:
185
+ """
186
+ Ping the server to check if it's alive
187
+
188
+ Returns:
189
+ True if server is responding, False otherwise
190
+ """
191
+ try:
192
+ request = JsonRpcRequest(method="ping")
193
+ response = self._send_request(request)
194
+ return response.error is None
195
+ except Exception:
196
+ return False
197
+
198
+ def get_server_info(self) -> Dict[str, Any]:
199
+ """
200
+ Get information about the server
201
+
202
+ Returns:
203
+ Dictionary containing server information
204
+ """
205
+ request = JsonRpcRequest(method="get_server_info")
206
+ response = self._send_request(request)
207
+
208
+ if response.error:
209
+ raise SharedTensorServerError(f"Failed to get server info: {response.error}")
210
+
211
+ return response.result or {}
212
+
213
+ def list_functions(self) -> Dict[str, str]:
214
+ """
215
+ List all available functions on the server
216
+
217
+ Returns:
218
+ Dictionary mapping function names to their paths
219
+ """
220
+ request = JsonRpcRequest(method="list_functions")
221
+ response = self._send_request(request)
222
+
223
+ if response.error:
224
+ raise SharedTensorServerError(f"Failed to list functions: {response.error}")
225
+
226
+ return response.result or {}
227
+
228
+ def close(self):
229
+ """Close the client session"""
230
+ self.session.close()
231
+
232
+ def __enter__(self):
233
+ return self
234
+
235
+ def __exit__(self, exc_type, exc_val, exc_tb):
236
+ self.close()
237
+
238
+
239
+ def execute_remote_function(
240
+ function_path: str,
241
+ args: tuple = (),
242
+ kwargs: Dict[str, Any] = None,
243
+ server_port: int = 2537,
244
+ timeout: int = 30,
245
+ verbose_debug: bool = False
246
+ ) -> Any:
247
+ """
248
+ Convenience function to execute a remote function
249
+
250
+ Args:
251
+ function_path: Function path in format "module.submodule:function_name"
252
+ args: Positional arguments
253
+ kwargs: Keyword arguments
254
+ server_port: Server port
255
+ timeout: Request timeout in seconds
256
+ verbose_debug: Whether to log detailed debug messages
257
+ Returns:
258
+ Function execution result
259
+ """
260
+ if verbose_debug:
261
+ logger.debug(f"Executing remote function {function_path} with args {args} and kwargs {kwargs} on server {server_port} with timeout {timeout} and verbose_debug {verbose_debug}")
262
+ else:
263
+ logger.debug(f"Executing remote function {function_path} on server {server_port} with timeout {timeout}")
264
+ with SharedTensorClient(server_port=server_port, timeout=timeout, verbose_debug=verbose_debug) as client:
265
+ return client.execute_function(function_path, args, kwargs)
@@ -0,0 +1,16 @@
1
+
2
+
3
+ __all__ = ["SharedTensorError", "SharedTensorServerError", "SharedTensorClientError", "SharedTensorProviderError"]
4
+
5
+
6
+ class SharedTensorError(Exception):
7
+ pass
8
+
9
+ class SharedTensorServerError(SharedTensorError):
10
+ pass
11
+
12
+ class SharedTensorClientError(SharedTensorError):
13
+ pass
14
+
15
+ class SharedTensorProviderError(SharedTensorError):
16
+ pass