dependabot/pip/docstring-parser-0.17.0
parent
f916c89cc1
commit
4a86e28893
@ -0,0 +1,354 @@
|
||||
"""
|
||||
Examples demonstrating the concurrent wrapper decorator functionality.
|
||||
|
||||
This file shows how to use the concurrent and concurrent_class_executor
|
||||
decorators to enable concurrent execution of functions and class methods.
|
||||
"""
|
||||
|
||||
import time
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
import requests
|
||||
|
||||
from swarms.utils.concurrent_wrapper import (
|
||||
concurrent,
|
||||
concurrent_class_executor,
|
||||
thread_executor,
|
||||
process_executor,
|
||||
async_executor,
|
||||
batch_executor,
|
||||
ExecutorType,
|
||||
)
|
||||
|
||||
|
||||
# Example 1: Basic concurrent function execution
|
||||
@concurrent(
|
||||
name="data_processor",
|
||||
description="Process data concurrently",
|
||||
max_workers=4,
|
||||
timeout=30,
|
||||
retry_on_failure=True,
|
||||
max_retries=2,
|
||||
)
|
||||
def process_data(data: str) -> str:
|
||||
"""Simulate data processing with a delay."""
|
||||
time.sleep(1) # Simulate work
|
||||
return f"processed_{data}"
|
||||
|
||||
|
||||
# Example 2: Thread-based executor for I/O bound tasks
|
||||
@thread_executor(max_workers=8, timeout=60)
|
||||
def fetch_url(url: str) -> Dict[str, Any]:
|
||||
"""Fetch data from a URL."""
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
return {
|
||||
"url": url,
|
||||
"status_code": response.status_code,
|
||||
"content_length": len(response.content),
|
||||
"success": response.status_code == 200,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"url": url, "error": str(e), "success": False}
|
||||
|
||||
|
||||
# Example 3: Process-based executor for CPU-intensive tasks
|
||||
@process_executor(max_workers=2, timeout=120)
|
||||
def cpu_intensive_task(n: int) -> float:
|
||||
"""Perform CPU-intensive computation."""
|
||||
result = 0.0
|
||||
for i in range(n):
|
||||
result += (i**0.5) * (i**0.3)
|
||||
return result
|
||||
|
||||
|
||||
# Example 4: Async executor for async functions
|
||||
@async_executor(max_workers=5)
|
||||
async def async_task(task_id: int) -> str:
|
||||
"""Simulate an async task."""
|
||||
await asyncio.sleep(0.5) # Simulate async work
|
||||
return f"async_result_{task_id}"
|
||||
|
||||
|
||||
# Example 5: Batch processing
|
||||
@batch_executor(batch_size=10, max_workers=3)
|
||||
def process_item(item: str) -> str:
|
||||
"""Process a single item."""
|
||||
time.sleep(0.1) # Simulate work
|
||||
return item.upper()
|
||||
|
||||
|
||||
# Example 6: Class with concurrent methods
|
||||
@concurrent_class_executor(
|
||||
name="DataProcessor",
|
||||
max_workers=4,
|
||||
methods=["process_batch", "validate_data"],
|
||||
)
|
||||
class DataProcessor:
|
||||
"""A class with concurrent processing capabilities."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
|
||||
def process_batch(self, data: str) -> str:
|
||||
"""Process a batch of data."""
|
||||
time.sleep(0.5) # Simulate processing
|
||||
return f"processed_{data}"
|
||||
|
||||
def validate_data(self, data: str) -> bool:
|
||||
"""Validate data."""
|
||||
time.sleep(0.2) # Simulate validation
|
||||
return len(data) > 0
|
||||
|
||||
def normal_method(self, x: int) -> int:
|
||||
"""A normal method (not concurrent)."""
|
||||
return x * 2
|
||||
|
||||
|
||||
# Example 7: Function with custom configuration
|
||||
@concurrent(
|
||||
name="custom_processor",
|
||||
description="Custom concurrent processor",
|
||||
max_workers=6,
|
||||
timeout=45,
|
||||
executor_type=ExecutorType.THREAD,
|
||||
return_exceptions=True,
|
||||
ordered=False,
|
||||
retry_on_failure=True,
|
||||
max_retries=3,
|
||||
retry_delay=0.5,
|
||||
)
|
||||
def custom_processor(item: str, multiplier: int = 1) -> str:
|
||||
"""Custom processor with parameters."""
|
||||
time.sleep(0.3)
|
||||
return f"{item}_{multiplier}" * multiplier
|
||||
|
||||
|
||||
def example_1_basic_concurrent_execution():
|
||||
"""Example 1: Basic concurrent execution."""
|
||||
print("=== Example 1: Basic Concurrent Execution ===")
|
||||
|
||||
# Prepare data
|
||||
data_items = [f"item_{i}" for i in range(10)]
|
||||
|
||||
# Execute concurrently
|
||||
results = process_data.concurrent_execute(*data_items)
|
||||
|
||||
# Process results
|
||||
successful_results = [r.value for r in results if r.success]
|
||||
failed_results = [r for r in results if not r.success]
|
||||
|
||||
print(f"Successfully processed: {len(successful_results)} items")
|
||||
print(f"Failed: {len(failed_results)} items")
|
||||
print(f"Sample results: {successful_results[:3]}")
|
||||
print()
|
||||
|
||||
|
||||
def example_2_thread_based_execution():
|
||||
"""Example 2: Thread-based execution for I/O bound tasks."""
|
||||
print("=== Example 2: Thread-based Execution ===")
|
||||
|
||||
# URLs to fetch
|
||||
urls = [
|
||||
"https://httpbin.org/get",
|
||||
"https://httpbin.org/status/200",
|
||||
"https://httpbin.org/status/404",
|
||||
"https://httpbin.org/delay/1",
|
||||
"https://httpbin.org/delay/2",
|
||||
]
|
||||
|
||||
# Execute concurrently
|
||||
results = fetch_url.concurrent_execute(*urls)
|
||||
|
||||
# Process results
|
||||
successful_fetches = [
|
||||
r.value
|
||||
for r in results
|
||||
if r.success and r.value.get("success")
|
||||
]
|
||||
failed_fetches = [
|
||||
r.value
|
||||
for r in results
|
||||
if r.success and not r.value.get("success")
|
||||
]
|
||||
|
||||
print(f"Successful fetches: {len(successful_fetches)}")
|
||||
print(f"Failed fetches: {len(failed_fetches)}")
|
||||
print(
|
||||
f"Sample successful result: {successful_fetches[0] if successful_fetches else 'None'}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
def example_3_process_based_execution():
|
||||
"""Example 3: Process-based execution for CPU-intensive tasks."""
|
||||
print("=== Example 3: Process-based Execution ===")
|
||||
|
||||
# CPU-intensive tasks
|
||||
tasks = [100000, 200000, 300000, 400000]
|
||||
|
||||
# Execute concurrently
|
||||
results = cpu_intensive_task.concurrent_execute(*tasks)
|
||||
|
||||
# Process results
|
||||
successful_results = [r.value for r in results if r.success]
|
||||
execution_times = [r.execution_time for r in results if r.success]
|
||||
|
||||
print(f"Completed {len(successful_results)} CPU-intensive tasks")
|
||||
print(
|
||||
f"Average execution time: {sum(execution_times) / len(execution_times):.3f}s"
|
||||
)
|
||||
print(
|
||||
f"Sample result: {successful_results[0] if successful_results else 'None'}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
def example_4_batch_processing():
|
||||
"""Example 4: Batch processing."""
|
||||
print("=== Example 4: Batch Processing ===")
|
||||
|
||||
# Items to process
|
||||
items = [f"item_{i}" for i in range(25)]
|
||||
|
||||
# Process in batches
|
||||
results = process_item.concurrent_batch(items, batch_size=5)
|
||||
|
||||
# Process results
|
||||
successful_results = [r.value for r in results if r.success]
|
||||
|
||||
print(f"Processed {len(successful_results)} items in batches")
|
||||
print(f"Sample results: {successful_results[:5]}")
|
||||
print()
|
||||
|
||||
|
||||
def example_5_class_concurrent_execution():
|
||||
"""Example 5: Class with concurrent methods."""
|
||||
print("=== Example 5: Class Concurrent Execution ===")
|
||||
|
||||
# Create processor instance
|
||||
processor = DataProcessor({"batch_size": 10})
|
||||
|
||||
# Prepare data
|
||||
data_items = [f"data_{i}" for i in range(8)]
|
||||
|
||||
# Execute concurrent methods
|
||||
process_results = processor.process_batch.concurrent_execute(
|
||||
*data_items
|
||||
)
|
||||
validate_results = processor.validate_data.concurrent_execute(
|
||||
*data_items
|
||||
)
|
||||
|
||||
# Process results
|
||||
processed_items = [r.value for r in process_results if r.success]
|
||||
valid_items = [r.value for r in validate_results if r.success]
|
||||
|
||||
print(f"Processed {len(processed_items)} items")
|
||||
print(f"Validated {len(valid_items)} items")
|
||||
print(f"Sample processed: {processed_items[:3]}")
|
||||
print(f"Sample validation: {valid_items[:3]}")
|
||||
print()
|
||||
|
||||
|
||||
def example_6_custom_configuration():
|
||||
"""Example 6: Custom configuration with exceptions and retries."""
|
||||
print("=== Example 6: Custom Configuration ===")
|
||||
|
||||
# Items with different multipliers
|
||||
items = [f"item_{i}" for i in range(6)]
|
||||
multipliers = [1, 2, 3, 1, 2, 3]
|
||||
|
||||
# Execute with custom configuration
|
||||
results = custom_processor.concurrent_execute(
|
||||
*items, **{"multiplier": multipliers}
|
||||
)
|
||||
|
||||
# Process results
|
||||
successful_results = [r.value for r in results if r.success]
|
||||
failed_results = [r for r in results if not r.success]
|
||||
|
||||
print(f"Successful: {len(successful_results)}")
|
||||
print(f"Failed: {len(failed_results)}")
|
||||
print(f"Sample results: {successful_results[:3]}")
|
||||
print()
|
||||
|
||||
|
||||
def example_7_concurrent_mapping():
|
||||
"""Example 7: Concurrent mapping over a list."""
|
||||
print("=== Example 7: Concurrent Mapping ===")
|
||||
|
||||
# Items to map over
|
||||
items = [f"map_item_{i}" for i in range(15)]
|
||||
|
||||
# Map function over items
|
||||
results = process_data.concurrent_map(items)
|
||||
|
||||
# Process results
|
||||
mapped_results = [r.value for r in results if r.success]
|
||||
|
||||
print(f"Mapped over {len(mapped_results)} items")
|
||||
print(f"Sample mapped results: {mapped_results[:5]}")
|
||||
print()
|
||||
|
||||
|
||||
def example_8_error_handling():
|
||||
"""Example 8: Error handling and retries."""
|
||||
print("=== Example 8: Error Handling ===")
|
||||
|
||||
@concurrent(
|
||||
max_workers=3,
|
||||
return_exceptions=True,
|
||||
retry_on_failure=True,
|
||||
max_retries=2,
|
||||
)
|
||||
def unreliable_function(x: int) -> int:
|
||||
"""A function that sometimes fails."""
|
||||
if x % 3 == 0:
|
||||
raise ValueError(f"Failed for {x}")
|
||||
time.sleep(0.1)
|
||||
return x * 2
|
||||
|
||||
# Execute with potential failures
|
||||
results = unreliable_function.concurrent_execute(*range(10))
|
||||
|
||||
# Process results
|
||||
successful_results = [r.value for r in results if r.success]
|
||||
failed_results = [r.exception for r in results if not r.success]
|
||||
|
||||
print(f"Successful: {len(successful_results)}")
|
||||
print(f"Failed: {len(failed_results)}")
|
||||
print(f"Sample successful: {successful_results[:3]}")
|
||||
print(
|
||||
f"Sample failures: {[type(e).__name__ for e in failed_results[:3]]}"
|
||||
)
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all examples."""
|
||||
print("Concurrent Wrapper Examples")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
try:
|
||||
example_1_basic_concurrent_execution()
|
||||
example_2_thread_based_execution()
|
||||
example_3_process_based_execution()
|
||||
example_4_batch_processing()
|
||||
example_5_class_concurrent_execution()
|
||||
example_6_custom_configuration()
|
||||
example_7_concurrent_mapping()
|
||||
example_8_error_handling()
|
||||
|
||||
print("All examples completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error running examples: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,629 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import inspect
|
||||
import time
|
||||
from concurrent.futures import (
|
||||
ThreadPoolExecutor,
|
||||
ProcessPoolExecutor,
|
||||
as_completed,
|
||||
)
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Generic,
|
||||
)
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from swarms.utils.loguru_logger import initialize_logger
|
||||
|
||||
logger = initialize_logger("concurrent_wrapper")
|
||||
|
||||
T = TypeVar("T")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
# Global function for process pool execution (must be picklable)
|
||||
def _execute_task_in_process(task_data):
|
||||
"""
|
||||
Execute a task in a separate process.
|
||||
This function must be at module level to be picklable.
|
||||
"""
|
||||
(
|
||||
func,
|
||||
task_args,
|
||||
task_kwargs,
|
||||
task_id,
|
||||
max_retries,
|
||||
retry_on_failure,
|
||||
retry_delay,
|
||||
return_exceptions,
|
||||
) = task_data
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
result = func(*task_args, **task_kwargs)
|
||||
execution_time = time.time() - start_time
|
||||
return ConcurrentResult(
|
||||
value=result,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
except Exception as e:
|
||||
if attempt == max_retries or not retry_on_failure:
|
||||
execution_time = time.time() - start_time
|
||||
if return_exceptions:
|
||||
return ConcurrentResult(
|
||||
exception=e,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
time.sleep(retry_delay * (2**attempt))
|
||||
|
||||
# This should never be reached, but just in case
|
||||
return ConcurrentResult(
|
||||
exception=Exception("Max retries exceeded")
|
||||
)
|
||||
|
||||
|
||||
class ExecutorType(Enum):
|
||||
"""Enum for different types of executors."""
|
||||
|
||||
THREAD = "thread"
|
||||
PROCESS = "process"
|
||||
ASYNC = "async"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConcurrentConfig:
|
||||
"""Configuration for concurrent execution."""
|
||||
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
max_workers: int = 4
|
||||
timeout: Optional[float] = None
|
||||
executor_type: ExecutorType = ExecutorType.THREAD
|
||||
return_exceptions: bool = False
|
||||
chunk_size: Optional[int] = None
|
||||
ordered: bool = True
|
||||
retry_on_failure: bool = False
|
||||
max_retries: int = 3
|
||||
retry_delay: float = 1.0
|
||||
|
||||
|
||||
class ConcurrentResult(Generic[T]):
|
||||
"""Result wrapper for concurrent execution."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
value: T = None,
|
||||
exception: Exception = None,
|
||||
execution_time: float = 0.0,
|
||||
worker_id: Optional[int] = None,
|
||||
):
|
||||
self.value = value
|
||||
self.exception = exception
|
||||
self.execution_time = execution_time
|
||||
self.worker_id = worker_id
|
||||
self.success = exception is None
|
||||
|
||||
def __repr__(self):
|
||||
if self.success:
|
||||
return f"ConcurrentResult(value={self.value}, time={self.execution_time:.3f}s)"
|
||||
else:
|
||||
return f"ConcurrentResult(exception={type(self.exception).__name__}: {self.exception})"
|
||||
|
||||
|
||||
def concurrent(
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
max_workers: int = 4,
|
||||
timeout: Optional[float] = None,
|
||||
executor_type: ExecutorType = ExecutorType.THREAD,
|
||||
return_exceptions: bool = False,
|
||||
chunk_size: Optional[int] = None,
|
||||
ordered: bool = True,
|
||||
retry_on_failure: bool = False,
|
||||
max_retries: int = 3,
|
||||
retry_delay: float = 1.0,
|
||||
):
|
||||
"""
|
||||
A decorator that enables concurrent execution of functions.
|
||||
|
||||
Args:
|
||||
name (Optional[str]): Name for the concurrent operation
|
||||
description (Optional[str]): Description of the operation
|
||||
max_workers (int): Maximum number of worker threads/processes
|
||||
timeout (Optional[float]): Timeout in seconds for each task
|
||||
executor_type (ExecutorType): Type of executor (thread, process, async)
|
||||
return_exceptions (bool): Whether to return exceptions instead of raising
|
||||
chunk_size (Optional[int]): Size of chunks for batch processing
|
||||
ordered (bool): Whether to maintain order of results
|
||||
retry_on_failure (bool): Whether to retry failed tasks
|
||||
max_retries (int): Maximum number of retries per task
|
||||
retry_delay (float): Delay between retries in seconds
|
||||
|
||||
Returns:
|
||||
Callable: Decorated function that can execute concurrently
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
||||
config = ConcurrentConfig(
|
||||
name=name or func.__name__,
|
||||
description=description
|
||||
or f"Concurrent execution of {func.__name__}",
|
||||
max_workers=max_workers,
|
||||
timeout=timeout,
|
||||
executor_type=executor_type,
|
||||
return_exceptions=return_exceptions,
|
||||
chunk_size=chunk_size,
|
||||
ordered=ordered,
|
||||
retry_on_failure=retry_on_failure,
|
||||
max_retries=max_retries,
|
||||
retry_delay=retry_delay,
|
||||
)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
def _execute_single_task(
|
||||
task_args, task_kwargs, task_id=None
|
||||
):
|
||||
"""Execute a single task with retry logic."""
|
||||
start_time = time.time()
|
||||
|
||||
for attempt in range(config.max_retries + 1):
|
||||
try:
|
||||
result = func(*task_args, **task_kwargs)
|
||||
execution_time = time.time() - start_time
|
||||
return ConcurrentResult(
|
||||
value=result,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
except Exception as e:
|
||||
if (
|
||||
attempt == config.max_retries
|
||||
or not config.retry_on_failure
|
||||
):
|
||||
execution_time = time.time() - start_time
|
||||
if config.return_exceptions:
|
||||
return ConcurrentResult(
|
||||
exception=e,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
f"Task {task_id} failed (attempt {attempt + 1}/{config.max_retries + 1}): {e}"
|
||||
)
|
||||
time.sleep(config.retry_delay * (2**attempt))
|
||||
|
||||
def concurrent_execute(*args_list, **kwargs_list):
|
||||
"""Execute the function concurrently with multiple argument sets."""
|
||||
if not args_list and not kwargs_list:
|
||||
raise ValueError(
|
||||
"At least one set of arguments must be provided"
|
||||
)
|
||||
|
||||
# Prepare tasks
|
||||
tasks = []
|
||||
if args_list:
|
||||
for args in args_list:
|
||||
if isinstance(args, (list, tuple)):
|
||||
tasks.append((args, {}))
|
||||
else:
|
||||
tasks.append(([args], {}))
|
||||
|
||||
if kwargs_list:
|
||||
for kwargs in kwargs_list:
|
||||
if isinstance(kwargs, dict):
|
||||
tasks.append(((), kwargs))
|
||||
else:
|
||||
raise ValueError(
|
||||
"kwargs_list must contain dictionaries"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Starting concurrent execution of {len(tasks)} tasks with {config.max_workers} workers"
|
||||
)
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if config.executor_type == ExecutorType.THREAD:
|
||||
results = _execute_with_thread_pool(tasks)
|
||||
elif config.executor_type == ExecutorType.PROCESS:
|
||||
results = _execute_with_process_pool(tasks)
|
||||
elif config.executor_type == ExecutorType.ASYNC:
|
||||
results = _execute_with_async(tasks)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported executor type: {config.executor_type}"
|
||||
)
|
||||
|
||||
total_time = time.time() - start_time
|
||||
successful_tasks = sum(
|
||||
1 for r in results if r.success
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Completed {len(tasks)} tasks in {total_time:.3f}s "
|
||||
f"({successful_tasks}/{len(tasks)} successful)"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Concurrent execution failed: {e}")
|
||||
raise
|
||||
|
||||
def _execute_with_thread_pool(tasks):
|
||||
"""Execute tasks using ThreadPoolExecutor."""
|
||||
results = []
|
||||
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=config.max_workers
|
||||
) as executor:
|
||||
if config.ordered:
|
||||
future_to_task = {
|
||||
executor.submit(
|
||||
_execute_single_task, task[0], task[1], i
|
||||
): i
|
||||
for i, task in enumerate(tasks)
|
||||
}
|
||||
|
||||
for future in as_completed(
|
||||
future_to_task, timeout=config.timeout
|
||||
):
|
||||
try:
|
||||
result = future.result(
|
||||
timeout=config.timeout
|
||||
)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
if config.return_exceptions:
|
||||
results.append(
|
||||
ConcurrentResult(exception=e)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
futures = [
|
||||
executor.submit(
|
||||
_execute_single_task, task[0], task[1], i
|
||||
)
|
||||
for i, task in enumerate(tasks)
|
||||
]
|
||||
|
||||
for future in as_completed(
|
||||
futures, timeout=config.timeout
|
||||
):
|
||||
try:
|
||||
result = future.result(
|
||||
timeout=config.timeout
|
||||
)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
if config.return_exceptions:
|
||||
results.append(
|
||||
ConcurrentResult(exception=e)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
return results
|
||||
|
||||
def _execute_with_process_pool(tasks):
|
||||
"""Execute tasks using ProcessPoolExecutor."""
|
||||
results = []
|
||||
|
||||
# Prepare task data for process execution
|
||||
task_data_list = []
|
||||
for i, task in enumerate(tasks):
|
||||
task_data = (
|
||||
func, # The function to execute
|
||||
task[0], # args
|
||||
task[1], # kwargs
|
||||
i, # task_id
|
||||
config.max_retries,
|
||||
config.retry_on_failure,
|
||||
config.retry_delay,
|
||||
config.return_exceptions,
|
||||
)
|
||||
task_data_list.append(task_data)
|
||||
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=config.max_workers
|
||||
) as executor:
|
||||
if config.ordered:
|
||||
future_to_task = {
|
||||
executor.submit(
|
||||
_execute_task_in_process, task_data
|
||||
): i
|
||||
for i, task_data in enumerate(task_data_list)
|
||||
}
|
||||
|
||||
for future in as_completed(
|
||||
future_to_task, timeout=config.timeout
|
||||
):
|
||||
try:
|
||||
result = future.result(
|
||||
timeout=config.timeout
|
||||
)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
if config.return_exceptions:
|
||||
results.append(
|
||||
ConcurrentResult(exception=e)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
futures = [
|
||||
executor.submit(
|
||||
_execute_task_in_process, task_data
|
||||
)
|
||||
for task_data in task_data_list
|
||||
]
|
||||
|
||||
for future in as_completed(
|
||||
futures, timeout=config.timeout
|
||||
):
|
||||
try:
|
||||
result = future.result(
|
||||
timeout=config.timeout
|
||||
)
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
if config.return_exceptions:
|
||||
results.append(
|
||||
ConcurrentResult(exception=e)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
return results
|
||||
|
||||
async def _execute_with_async(tasks):
|
||||
"""Execute tasks using asyncio."""
|
||||
|
||||
async def _async_task(
|
||||
task_args, task_kwargs, task_id=None
|
||||
):
|
||||
start_time = time.time()
|
||||
|
||||
for attempt in range(config.max_retries + 1):
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: func(*task_args, **task_kwargs),
|
||||
)
|
||||
execution_time = time.time() - start_time
|
||||
return ConcurrentResult(
|
||||
value=result,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
except Exception as e:
|
||||
if (
|
||||
attempt == config.max_retries
|
||||
or not config.retry_on_failure
|
||||
):
|
||||
execution_time = time.time() - start_time
|
||||
if config.return_exceptions:
|
||||
return ConcurrentResult(
|
||||
exception=e,
|
||||
execution_time=execution_time,
|
||||
worker_id=task_id,
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
f"Async task {task_id} failed (attempt {attempt + 1}/{config.max_retries + 1}): {e}"
|
||||
)
|
||||
await asyncio.sleep(
|
||||
config.retry_delay * (2**attempt)
|
||||
)
|
||||
|
||||
semaphore = asyncio.Semaphore(config.max_workers)
|
||||
|
||||
async def _limited_task(task_args, task_kwargs, task_id):
|
||||
async with semaphore:
|
||||
return await _async_task(
|
||||
task_args, task_kwargs, task_id
|
||||
)
|
||||
|
||||
tasks_coros = [
|
||||
_limited_task(task[0], task[1], i)
|
||||
for i, task in enumerate(tasks)
|
||||
]
|
||||
|
||||
if config.ordered:
|
||||
results = []
|
||||
for coro in asyncio.as_completed(tasks_coros):
|
||||
try:
|
||||
result = await coro
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
if config.return_exceptions:
|
||||
results.append(
|
||||
ConcurrentResult(exception=e)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
return results
|
||||
else:
|
||||
return await asyncio.gather(
|
||||
*tasks_coros,
|
||||
return_exceptions=config.return_exceptions,
|
||||
)
|
||||
|
||||
def concurrent_batch(
|
||||
items: List[Any],
|
||||
batch_size: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> List[ConcurrentResult]:
|
||||
"""Execute the function concurrently on a batch of items."""
|
||||
batch_size = batch_size or config.chunk_size or len(items)
|
||||
|
||||
tasks = []
|
||||
for item in items:
|
||||
if isinstance(item, (list, tuple)):
|
||||
tasks.append((item, kwargs))
|
||||
else:
|
||||
tasks.append(([item], kwargs))
|
||||
|
||||
return concurrent_execute(
|
||||
*[task[0] for task in tasks],
|
||||
**[task[1] for task in tasks],
|
||||
)
|
||||
|
||||
def concurrent_map(
|
||||
items: List[Any], **kwargs
|
||||
) -> List[ConcurrentResult]:
|
||||
"""Map the function over a list of items concurrently."""
|
||||
return concurrent_batch(items, **kwargs)
|
||||
|
||||
# Attach methods to the wrapper
|
||||
wrapper.concurrent_execute = concurrent_execute
|
||||
wrapper.concurrent_batch = concurrent_batch
|
||||
wrapper.concurrent_map = concurrent_map
|
||||
wrapper.config = config
|
||||
|
||||
# Add metadata
|
||||
wrapper.__concurrent_config__ = config
|
||||
wrapper.__concurrent_enabled__ = True
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def concurrent_class_executor(
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
max_workers: int = 4,
|
||||
timeout: Optional[float] = None,
|
||||
executor_type: ExecutorType = ExecutorType.THREAD,
|
||||
return_exceptions: bool = False,
|
||||
chunk_size: Optional[int] = None,
|
||||
ordered: bool = True,
|
||||
retry_on_failure: bool = False,
|
||||
max_retries: int = 3,
|
||||
retry_delay: float = 1.0,
|
||||
methods: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
A decorator that enables concurrent execution for class methods.
|
||||
|
||||
Args:
|
||||
name (Optional[str]): Name for the concurrent operation
|
||||
description (Optional[str]): Description of the operation
|
||||
max_workers (int): Maximum number of worker threads/processes
|
||||
timeout (Optional[float]): Timeout in seconds for each task
|
||||
executor_type (ExecutorType): Type of executor (thread, process, async)
|
||||
return_exceptions (bool): Whether to return exceptions instead of raising
|
||||
chunk_size (Optional[int]): Size of chunks for batch processing
|
||||
ordered (bool): Whether to maintain order of results
|
||||
retry_on_failure (bool): Whether to retry failed tasks
|
||||
max_retries (int): Maximum number of retries per task
|
||||
retry_delay (float): Delay between retries in seconds
|
||||
methods (Optional[List[str]]): List of method names to make concurrent
|
||||
|
||||
Returns:
|
||||
Class: Class with concurrent execution capabilities
|
||||
"""
|
||||
|
||||
def decorator(cls):
|
||||
config = ConcurrentConfig(
|
||||
name=name or f"{cls.__name__}_concurrent",
|
||||
description=description
|
||||
or f"Concurrent execution for {cls.__name__}",
|
||||
max_workers=max_workers,
|
||||
timeout=timeout,
|
||||
executor_type=executor_type,
|
||||
return_exceptions=return_exceptions,
|
||||
chunk_size=chunk_size,
|
||||
ordered=ordered,
|
||||
retry_on_failure=retry_on_failure,
|
||||
max_retries=max_retries,
|
||||
retry_delay=retry_delay,
|
||||
)
|
||||
|
||||
# Get methods to make concurrent
|
||||
target_methods = methods or [
|
||||
name
|
||||
for name, method in inspect.getmembers(
|
||||
cls, inspect.isfunction
|
||||
)
|
||||
if not name.startswith("_")
|
||||
]
|
||||
|
||||
for method_name in target_methods:
|
||||
if hasattr(cls, method_name):
|
||||
original_method = getattr(cls, method_name)
|
||||
|
||||
# Create concurrent version of the method
|
||||
concurrent_decorator = concurrent(
|
||||
name=f"{cls.__name__}.{method_name}",
|
||||
description=f"Concurrent execution of {cls.__name__}.{method_name}",
|
||||
max_workers=config.max_workers,
|
||||
timeout=config.timeout,
|
||||
executor_type=config.executor_type,
|
||||
return_exceptions=config.return_exceptions,
|
||||
chunk_size=config.chunk_size,
|
||||
ordered=config.ordered,
|
||||
retry_on_failure=config.retry_on_failure,
|
||||
max_retries=config.max_retries,
|
||||
retry_delay=config.retry_delay,
|
||||
)
|
||||
|
||||
# Apply the concurrent decorator to the method
|
||||
setattr(
|
||||
cls,
|
||||
method_name,
|
||||
concurrent_decorator(original_method),
|
||||
)
|
||||
|
||||
# Add class-level concurrent configuration
|
||||
cls.__concurrent_config__ = config
|
||||
cls.__concurrent_enabled__ = True
|
||||
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# Convenience functions for common use cases
|
||||
def thread_executor(**kwargs):
|
||||
"""Convenience decorator for thread-based concurrent execution."""
|
||||
return concurrent(executor_type=ExecutorType.THREAD, **kwargs)
|
||||
|
||||
|
||||
def process_executor(**kwargs):
|
||||
"""Convenience decorator for process-based concurrent execution."""
|
||||
return concurrent(executor_type=ExecutorType.PROCESS, **kwargs)
|
||||
|
||||
|
||||
def async_executor(**kwargs):
|
||||
"""Convenience decorator for async-based concurrent execution."""
|
||||
return concurrent(executor_type=ExecutorType.ASYNC, **kwargs)
|
||||
|
||||
|
||||
def batch_executor(batch_size: int = 10, **kwargs):
|
||||
"""Convenience decorator for batch processing."""
|
||||
return concurrent(chunk_size=batch_size, **kwargs)
|
Loading…
Reference in new issue