mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
720 lines
21 KiB
Python
720 lines
21 KiB
Python
"""Pools Module.
|
|
|
|
Resource pooling utilities using standard library only.
|
|
|
|
Key Features:
|
|
- Thread pool for concurrent tasks
|
|
- Connection pool for database connections
|
|
- Object pool for resource reuse
|
|
- Pool lifecycle management
|
|
- Thread-safe operations
|
|
- Free-threaded Python compatible
|
|
- Standard library only (no external dependencies)
|
|
|
|
Dependencies:
|
|
- threading (standard library)
|
|
- queue (standard library)
|
|
- contextlib (standard library)
|
|
- sys (for GIL detection)
|
|
"""
|
|
|
|
import threading
|
|
import queue
|
|
from typing import Any, Callable, Optional, Generic, TypeVar, List
|
|
from contextlib import contextmanager
|
|
import time
|
|
import sys
|
|
|
|
|
|
class PoolError(Exception):
|
|
"""Pool operation error"""
|
|
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
def _is_free_threaded() -> bool:
|
|
"""Check if running in free-threaded mode (Python 3.13+ with --disable-gil).
|
|
|
|
Returns:
|
|
True if running in free-threaded mode, False otherwise
|
|
"""
|
|
return hasattr(sys, 'flags') and getattr(sys.flags, 'gil', 1) == 0
|
|
|
|
|
|
class ObjectPool(Generic[T]):
|
|
"""Generic object pool for resource reuse.
|
|
|
|
Maintains a pool of reusable objects with automatic lifecycle management.
|
|
Thread-safe for concurrent access and free-threading compatible.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
factory: Callable[[], T],
|
|
max_size: int = 10,
|
|
timeout: float = 5.0,
|
|
reset_func: Optional[Callable[[T], None]] = None,
|
|
destroy_func: Optional[Callable[[T], None]] = None,
|
|
use_rlock: bool = True # Use RLock instead of Lock for better free-threaded compatibility
|
|
):
|
|
"""Initialize object pool.
|
|
|
|
Args:
|
|
factory: Function to create new objects
|
|
max_size: Maximum pool size
|
|
timeout: Timeout for acquiring objects
|
|
reset_func: Function to reset object before reuse
|
|
destroy_func: Function to destroy object when removing from pool
|
|
use_rlock: Use RLock instead of Lock for recursive locking in free-threaded mode
|
|
"""
|
|
self.factory = factory
|
|
self.max_size = max_size
|
|
self.timeout = timeout
|
|
self.reset_func = reset_func
|
|
self.destroy_func = destroy_func
|
|
|
|
self._pool = queue.Queue(maxsize=max_size)
|
|
self._active_count = 0
|
|
self._lock = threading.RLock() if use_rlock else threading.Lock()
|
|
self._closed = False
|
|
self._is_free_threaded = _is_free_threaded()
|
|
|
|
@property
|
|
def is_free_threaded(self) -> bool:
|
|
"""Check if running in free-threaded mode.
|
|
|
|
Returns:
|
|
True if running in free-threaded mode, False otherwise
|
|
"""
|
|
return self._is_free_threaded
|
|
|
|
def get_optimal_pool_size(self, estimated_concurrent_usage: Optional[int] = None) -> int:
|
|
"""Get optimal pool size based on Python version and usage patterns.
|
|
|
|
Args:
|
|
estimated_concurrent_usage: Expected number of concurrent users
|
|
|
|
Returns:
|
|
Optimal pool size
|
|
"""
|
|
if estimated_concurrent_usage is not None:
|
|
if self._is_free_threaded:
|
|
# In free-threaded mode, can support more concurrent usage
|
|
return max(self.max_size, estimated_concurrent_usage * 2)
|
|
else:
|
|
# With GIL, be more conservative
|
|
return max(self.max_size, estimated_concurrent_usage)
|
|
else:
|
|
# Use current max_size
|
|
return self.max_size
|
|
|
|
def acquire(self, timeout: Optional[float] = None) -> T:
|
|
"""Acquire an object from the pool.
|
|
|
|
Args:
|
|
timeout: Timeout in seconds (None = use pool default)
|
|
|
|
Returns:
|
|
Object from pool
|
|
|
|
Raises:
|
|
PoolError: If pool is closed or timeout
|
|
"""
|
|
if self._closed:
|
|
raise PoolError("Pool is closed")
|
|
|
|
if timeout is None:
|
|
timeout = self.timeout
|
|
|
|
try:
|
|
# Try to get existing object from pool
|
|
obj = self._pool.get(timeout=timeout)
|
|
return obj
|
|
|
|
except queue.Empty:
|
|
# No objects available, try to create new one
|
|
with self._lock:
|
|
if self._active_count < self.max_size:
|
|
# Create new object
|
|
self._active_count += 1
|
|
try:
|
|
obj = self.factory()
|
|
return obj
|
|
except Exception as e:
|
|
self._active_count -= 1
|
|
raise PoolError(f"Failed to create object: {e}") from e
|
|
|
|
# Pool is at capacity, wait for object
|
|
raise PoolError(f"Pool exhausted, timeout after {timeout}s")
|
|
|
|
def release(self, obj: T) -> None:
|
|
"""Release an object back to the pool.
|
|
|
|
Args:
|
|
obj: Object to release
|
|
|
|
Raises:
|
|
PoolError: If pool is closed
|
|
"""
|
|
if self._closed:
|
|
# Destroy object if pool is closed
|
|
if self.destroy_func:
|
|
try:
|
|
self.destroy_func(obj)
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
try:
|
|
# Reset object if reset function provided
|
|
if self.reset_func:
|
|
self.reset_func(obj)
|
|
|
|
# Return to pool
|
|
self._pool.put_nowait(obj)
|
|
|
|
except queue.Full:
|
|
# Pool is full, destroy the object
|
|
with self._lock:
|
|
self._active_count -= 1
|
|
if self.destroy_func:
|
|
try:
|
|
self.destroy_func(obj)
|
|
except Exception:
|
|
pass
|
|
|
|
@contextmanager
|
|
def get_object(self, timeout: Optional[float] = None):
|
|
"""Context manager for automatic acquire/release.
|
|
|
|
Args:
|
|
timeout: Timeout in seconds
|
|
|
|
Yields:
|
|
Object from pool
|
|
|
|
Example:
|
|
with pool.get_object() as obj:
|
|
# Use obj
|
|
obj.do_something()
|
|
# Automatically released
|
|
"""
|
|
obj = self.acquire(timeout)
|
|
try:
|
|
yield obj
|
|
finally:
|
|
self.release(obj)
|
|
|
|
def close(self) -> None:
|
|
"""Close the pool and destroy all objects."""
|
|
with self._lock:
|
|
if self._closed:
|
|
return
|
|
self._closed = True
|
|
|
|
# Destroy all pooled objects
|
|
if self.destroy_func:
|
|
while not self._pool.empty():
|
|
try:
|
|
obj = self._pool.get_nowait()
|
|
self.destroy_func(obj)
|
|
except (queue.Empty, Exception):
|
|
break
|
|
|
|
def __enter__(self):
|
|
"""Context manager entry."""
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Context manager exit."""
|
|
self.close()
|
|
return False
|
|
|
|
@property
|
|
def size(self) -> int:
|
|
"""Get current pool size.
|
|
|
|
Returns:
|
|
Number of objects in pool
|
|
"""
|
|
return self._pool.qsize()
|
|
|
|
@property
|
|
def active(self) -> int:
|
|
"""Get number of active objects.
|
|
|
|
Returns:
|
|
Number of objects currently in use
|
|
"""
|
|
with self._lock:
|
|
return self._active_count
|
|
|
|
|
|
class ConnectionPool:
|
|
"""Database connection pool.
|
|
|
|
Specialized pool for managing database connections.
|
|
Free-threading compatible with appropriate locking.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
connect_func: Callable[[], Any],
|
|
max_connections: int = 10,
|
|
timeout: float = 5.0,
|
|
test_on_borrow: bool = True
|
|
):
|
|
"""Initialize connection pool.
|
|
|
|
Args:
|
|
connect_func: Function to create new connection
|
|
max_connections: Maximum number of connections
|
|
timeout: Timeout for acquiring connection
|
|
test_on_borrow: Test connection health before returning
|
|
"""
|
|
self._is_free_threaded = _is_free_threaded()
|
|
self.connect_func = connect_func
|
|
self.test_on_borrow = test_on_borrow
|
|
|
|
# Create object pool with connection-specific handlers
|
|
self._pool = ObjectPool(
|
|
factory=connect_func,
|
|
max_size=max_connections,
|
|
timeout=timeout,
|
|
reset_func=None, # Connections don't need reset
|
|
destroy_func=self._close_connection
|
|
)
|
|
|
|
@property
|
|
def is_free_threaded(self) -> bool:
|
|
"""Check if running in free-threaded mode.
|
|
|
|
Returns:
|
|
True if running in free-threaded mode, False otherwise
|
|
"""
|
|
return self._is_free_threaded
|
|
|
|
def _close_connection(self, conn: Any) -> None:
|
|
"""Close a database connection.
|
|
|
|
Args:
|
|
conn: Connection to close
|
|
"""
|
|
try:
|
|
if hasattr(conn, 'close'):
|
|
conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _test_connection(self, conn: Any) -> bool:
|
|
"""Test if connection is still valid.
|
|
|
|
Args:
|
|
conn: Connection to test
|
|
|
|
Returns:
|
|
True if connection is valid
|
|
"""
|
|
try:
|
|
# Try to execute a simple query
|
|
if hasattr(conn, 'execute'):
|
|
conn.execute('SELECT 1')
|
|
return True
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def acquire(self, timeout: Optional[float] = None) -> Any:
|
|
"""Acquire a connection from pool.
|
|
|
|
Args:
|
|
timeout: Timeout in seconds
|
|
|
|
Returns:
|
|
Database connection
|
|
|
|
Raises:
|
|
PoolError: If unable to acquire connection
|
|
"""
|
|
conn = self._pool.acquire(timeout)
|
|
|
|
# Test connection if required
|
|
if self.test_on_borrow:
|
|
if not self._test_connection(conn):
|
|
# Connection is bad, destroy it and try again
|
|
self._close_connection(conn)
|
|
return self.acquire(timeout)
|
|
|
|
return conn
|
|
|
|
def release(self, conn: Any) -> None:
|
|
"""Release connection back to pool.
|
|
|
|
Args:
|
|
conn: Connection to release
|
|
"""
|
|
self._pool.release(conn)
|
|
|
|
@contextmanager
|
|
def get_connection(self, timeout: Optional[float] = None):
|
|
"""Context manager for connection.
|
|
|
|
Args:
|
|
timeout: Timeout in seconds
|
|
|
|
Yields:
|
|
Database connection
|
|
|
|
Example:
|
|
with pool.get_connection() as conn:
|
|
cursor = conn.execute("SELECT * FROM users")
|
|
# Connection automatically released
|
|
"""
|
|
with self._pool.get_object(timeout) as conn:
|
|
yield conn
|
|
|
|
def close(self) -> None:
|
|
"""Close all connections in pool."""
|
|
self._pool.close()
|
|
|
|
@property
|
|
def size(self) -> int:
|
|
"""Get pool size."""
|
|
return self._pool.size
|
|
|
|
@property
|
|
def active(self) -> int:
|
|
"""Get active connection count."""
|
|
return self._pool.active
|
|
|
|
|
|
class WorkerPool:
|
|
"""Worker pool for task execution.
|
|
|
|
Maintains a pool of worker threads for executing tasks.
|
|
Free-threading compatible with appropriate locking.
|
|
"""
|
|
|
|
def __init__(self, workers: int = 4, queue_size: int = 100):
|
|
"""Initialize worker pool.
|
|
|
|
Args:
|
|
workers: Number of worker threads
|
|
queue_size: Maximum queue size (0 = unlimited)
|
|
"""
|
|
self._is_free_threaded = _is_free_threaded()
|
|
self.workers = workers
|
|
self.queue_size = queue_size
|
|
|
|
if queue_size > 0:
|
|
self._queue = queue.Queue(maxsize=queue_size)
|
|
else:
|
|
self._queue = queue.Queue()
|
|
|
|
self._threads: List[threading.Thread] = []
|
|
self._running = False
|
|
self._lock = threading.RLock() if self._is_free_threaded else threading.Lock()
|
|
|
|
@property
|
|
def is_free_threaded(self) -> bool:
|
|
"""Check if running in free-threaded mode.
|
|
|
|
Returns:
|
|
True if running in free-threaded mode, False otherwise
|
|
"""
|
|
return self._is_free_threaded
|
|
|
|
def get_optimal_workers(self, base_workers: Optional[int] = None) -> int:
|
|
"""Get optimal number of workers based on Python version.
|
|
|
|
Args:
|
|
base_workers: Base number of workers (None = use self.workers)
|
|
|
|
Returns:
|
|
Optimal number of workers
|
|
"""
|
|
if base_workers is None:
|
|
base_workers = self.workers
|
|
|
|
if self._is_free_threaded:
|
|
# In free-threaded mode, can use more workers efficiently
|
|
return base_workers * 2
|
|
else:
|
|
# With GIL, be more conservative
|
|
return base_workers
|
|
|
|
def start(self) -> None:
|
|
"""Start worker threads."""
|
|
with self._lock:
|
|
if self._running:
|
|
return
|
|
|
|
self._running = True
|
|
|
|
# Get optimal number of workers based on Python version
|
|
optimal_workers = self.get_optimal_workers()
|
|
|
|
# Create and start worker threads
|
|
for i in range(optimal_workers):
|
|
thread = threading.Thread(
|
|
target=self._worker,
|
|
name=f"Worker-{i}",
|
|
daemon=True
|
|
)
|
|
thread.start()
|
|
self._threads.append(thread)
|
|
|
|
def _worker(self) -> None:
|
|
"""Worker thread main loop."""
|
|
while self._running:
|
|
try:
|
|
# Get task from queue (blocking to avoid polling)
|
|
task = self._queue.get()
|
|
|
|
if task is None: # Poison pill
|
|
break
|
|
|
|
# Execute task
|
|
func, args, kwargs = task
|
|
try:
|
|
func(*args, **kwargs)
|
|
except Exception:
|
|
# Ignore task errors (could log here)
|
|
pass
|
|
finally:
|
|
self._queue.task_done()
|
|
|
|
except queue.Empty:
|
|
# This shouldn't happen with blocking get, but handle gracefully
|
|
continue
|
|
|
|
def submit(
|
|
self,
|
|
func: Callable,
|
|
*args,
|
|
timeout: Optional[float] = None,
|
|
**kwargs
|
|
) -> None:
|
|
"""Submit a task to the pool.
|
|
|
|
Args:
|
|
func: Function to execute
|
|
*args: Positional arguments for func
|
|
timeout: Timeout for queue insertion
|
|
**kwargs: Keyword arguments for func
|
|
|
|
Raises:
|
|
PoolError: If pool is not running or queue is full
|
|
"""
|
|
if not self._running:
|
|
raise PoolError("Worker pool is not running")
|
|
|
|
try:
|
|
task = (func, args, kwargs)
|
|
self._queue.put(task, timeout=timeout)
|
|
except queue.Full:
|
|
raise PoolError("Worker queue is full")
|
|
|
|
def shutdown(self, wait: bool = True, timeout: Optional[float] = None) -> None:
|
|
"""Shutdown worker pool.
|
|
|
|
Args:
|
|
wait: Wait for tasks to complete
|
|
timeout: Maximum time to wait in seconds
|
|
"""
|
|
with self._lock:
|
|
if not self._running:
|
|
return
|
|
|
|
self._running = False
|
|
|
|
if wait:
|
|
# Wait for all tasks to complete using queue.join()
|
|
start_time = time.monotonic()
|
|
try:
|
|
self._queue.join()
|
|
except Exception:
|
|
# If join() fails, fall back to polling
|
|
while not self._queue.empty():
|
|
if timeout and (time.monotonic() - start_time) > timeout:
|
|
break
|
|
time.sleep(0.1)
|
|
|
|
# Send poison pills to workers - ensure we send exactly one per worker
|
|
for _ in range(len(self._threads)):
|
|
try:
|
|
# Use blocking put with timeout to avoid deadlock
|
|
self._queue.put(None, block=True, timeout=1.0)
|
|
except queue.Full:
|
|
# Queue is full, but this shouldn't happen if we used queue.join()
|
|
pass
|
|
except Exception:
|
|
# If put fails, continue anyway
|
|
pass
|
|
|
|
# Wait for threads to finish
|
|
for thread in self._threads:
|
|
if timeout:
|
|
remaining = timeout - (time.monotonic() - start_time)
|
|
thread.join(timeout=max(0, remaining))
|
|
else:
|
|
thread.join()
|
|
|
|
self._threads.clear()
|
|
|
|
@property
|
|
def pending(self) -> int:
|
|
"""Get number of pending tasks.
|
|
|
|
Returns:
|
|
Number of tasks in queue
|
|
"""
|
|
return self._queue.qsize()
|
|
|
|
def __enter__(self):
|
|
"""Context manager entry."""
|
|
self.start()
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Context manager exit."""
|
|
self.shutdown(wait=True)
|
|
return False
|
|
|
|
|
|
class Pools:
|
|
"""Factory class for creating various types of pools."""
|
|
|
|
@staticmethod
|
|
def is_free_threaded() -> bool:
|
|
"""Check if running in free-threaded mode.
|
|
|
|
Returns:
|
|
True if running in free-threaded mode, False otherwise
|
|
"""
|
|
return _is_free_threaded()
|
|
|
|
@staticmethod
|
|
def create_pool(
|
|
factory: Callable[[], T],
|
|
max_size: int = 10,
|
|
**kwargs
|
|
) -> ObjectPool[T]:
|
|
"""Create a generic object pool.
|
|
|
|
Args:
|
|
factory: Function to create objects
|
|
max_size: Maximum pool size
|
|
**kwargs: Additional pool arguments
|
|
|
|
Returns:
|
|
ObjectPool instance
|
|
"""
|
|
return ObjectPool(factory=factory, max_size=max_size, **kwargs)
|
|
|
|
@staticmethod
|
|
def create_pool_optimized(
|
|
factory: Callable[[], T],
|
|
max_size: int = 10,
|
|
**kwargs
|
|
) -> ObjectPool[T]:
|
|
"""Create a generic object pool optimized for current Python version.
|
|
|
|
Args:
|
|
factory: Function to create objects
|
|
max_size: Base maximum pool size (will be adjusted for free-threading)
|
|
**kwargs: Additional pool arguments
|
|
|
|
Returns:
|
|
ObjectPool instance optimized for current environment
|
|
"""
|
|
is_free_threaded = _is_free_threaded()
|
|
|
|
# Adjust max_size based on threading mode if not explicitly set
|
|
if 'max_size' not in kwargs and is_free_threaded:
|
|
max_size = max_size * 2 # More capacity in free-threaded mode
|
|
|
|
return ObjectPool(factory=factory, max_size=max_size, use_rlock=True, **kwargs)
|
|
|
|
@staticmethod
|
|
def create_connection_pool(
|
|
connect_func: Callable[[], Any],
|
|
max_connections: int = 10,
|
|
**kwargs
|
|
) -> ConnectionPool:
|
|
"""Create a connection pool.
|
|
|
|
Args:
|
|
connect_func: Function to create connections
|
|
max_connections: Maximum number of connections
|
|
**kwargs: Additional pool arguments
|
|
|
|
Returns:
|
|
ConnectionPool instance
|
|
"""
|
|
return ConnectionPool(
|
|
connect_func=connect_func,
|
|
max_connections=max_connections,
|
|
**kwargs
|
|
)
|
|
|
|
@staticmethod
|
|
def create_connection_pool_optimized(
|
|
connect_func: Callable[[], Any],
|
|
max_connections: int = 10,
|
|
**kwargs
|
|
) -> ConnectionPool:
|
|
"""Create a connection pool optimized for current Python version.
|
|
|
|
Args:
|
|
connect_func: Function to create connections
|
|
max_connections: Base maximum connections (will be adjusted for free-threading)
|
|
**kwargs: Additional pool arguments
|
|
|
|
Returns:
|
|
ConnectionPool instance optimized for current environment
|
|
"""
|
|
if _is_free_threaded():
|
|
# In free-threaded mode, can handle more concurrent connections
|
|
max_connections = max_connections * 2
|
|
return ConnectionPool(
|
|
connect_func=connect_func,
|
|
max_connections=max_connections,
|
|
**kwargs
|
|
)
|
|
|
|
@staticmethod
|
|
def create_worker_pool(
|
|
workers: int = 4,
|
|
queue_size: int = 100
|
|
) -> WorkerPool:
|
|
"""Create a worker pool.
|
|
|
|
Args:
|
|
workers: Number of worker threads
|
|
queue_size: Maximum queue size
|
|
|
|
Returns:
|
|
WorkerPool instance
|
|
"""
|
|
return WorkerPool(workers=workers, queue_size=queue_size)
|
|
|
|
@staticmethod
|
|
def create_worker_pool_optimized(
|
|
workers: int = 4,
|
|
queue_size: int = 100
|
|
) -> WorkerPool:
|
|
"""Create a worker pool optimized for current Python version.
|
|
|
|
Args:
|
|
workers: Base number of worker threads (will be adjusted for free-threading)
|
|
queue_size: Maximum queue size
|
|
|
|
Returns:
|
|
WorkerPool instance optimized for current environment
|
|
"""
|
|
if _is_free_threaded():
|
|
# In free-threaded mode, can use more workers efficiently
|
|
workers = workers * 2
|
|
return WorkerPool(workers=workers, queue_size=queue_size)
|