Coverage for src/dataknobs_fsm/resources/base.py: 61%
119 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:51 -0600
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:51 -0600
1"""Base interfaces and classes for resource management."""
3from abc import ABC, abstractmethod
4from dataclasses import dataclass
5from datetime import datetime
6from enum import Enum
7from typing import Any, Dict, List, Protocol, runtime_checkable
8from contextlib import contextmanager
11class ResourceStatus(Enum):
12 """Status of a resource."""
14 IDLE = "idle"
15 ACTIVE = "active"
16 BUSY = "busy"
17 ERROR = "error"
18 CLOSED = "closed"
19 MAINTENANCE = "maintenance"
22class ResourceHealth(Enum):
23 """Health status of a resource."""
25 HEALTHY = "healthy"
26 DEGRADED = "degraded"
27 UNHEALTHY = "unhealthy"
28 UNKNOWN = "unknown"
31@dataclass
32class ResourceMetrics:
33 """Metrics for resource usage."""
35 total_acquisitions: int = 0
36 active_connections: int = 0
37 failed_acquisitions: int = 0
38 average_hold_time: float = 0.0
39 last_acquisition_time: datetime | None = None
40 last_release_time: datetime | None = None
41 health_check_failures: int = 0
42 last_health_check: datetime | None = None
43 average_acquisition_time: float = 0.0
44 total_timeout_events: int = 0
45 last_timeout_time: datetime | None = None
47 def record_acquisition(self, acquisition_time: float | None = None) -> None:
48 """Record a resource acquisition.
50 Args:
51 acquisition_time: Time taken to acquire the resource in seconds.
52 """
53 self.total_acquisitions += 1
54 self.active_connections += 1
55 self.last_acquisition_time = datetime.now()
57 # Update average acquisition time if provided
58 if acquisition_time is not None:
59 if self.average_acquisition_time == 0:
60 self.average_acquisition_time = acquisition_time
61 else:
62 # Rolling average
63 self.average_acquisition_time = (self.average_acquisition_time * 0.9) + (acquisition_time * 0.1)
65 def record_release(self, hold_time: float) -> None:
66 """Record a resource release.
68 Args:
69 hold_time: How long the resource was held in seconds.
70 """
71 self.active_connections = max(0, self.active_connections - 1)
72 self.last_release_time = datetime.now()
74 # Update average hold time
75 if self.average_hold_time == 0:
76 self.average_hold_time = hold_time
77 else:
78 # Rolling average
79 self.average_hold_time = (self.average_hold_time * 0.9) + (hold_time * 0.1)
81 def record_failure(self) -> None:
82 """Record a failed acquisition."""
83 self.failed_acquisitions += 1
85 def record_health_check(self, success: bool) -> None:
86 """Record a health check result.
88 Args:
89 success: Whether the health check passed.
90 """
91 self.last_health_check = datetime.now()
92 if not success:
93 self.health_check_failures += 1
95 def record_timeout(self) -> None:
96 """Record a timeout event."""
97 self.total_timeout_events += 1
98 self.last_timeout_time = datetime.now()
99 self.failed_acquisitions += 1
102@runtime_checkable
103class IResourceProvider(Protocol):
104 """Interface for resource providers."""
106 def acquire(self, **kwargs) -> Any:
107 """Acquire a resource.
109 Args:
110 **kwargs: Provider-specific parameters.
112 Returns:
113 The acquired resource.
115 Raises:
116 ResourceError: If acquisition fails.
117 """
118 ...
120 def release(self, resource: Any) -> None:
121 """Release a resource.
123 Args:
124 resource: The resource to release.
125 """
126 ...
128 def validate(self, resource: Any) -> bool:
129 """Validate that a resource is still valid.
131 Args:
132 resource: The resource to validate.
134 Returns:
135 True if the resource is valid.
136 """
137 ...
139 def health_check(self) -> ResourceHealth:
140 """Check the health of the resource provider.
142 Returns:
143 The health status.
144 """
145 ...
147 def get_metrics(self) -> ResourceMetrics:
148 """Get resource metrics.
150 Returns:
151 Current metrics.
152 """
153 ...
156@runtime_checkable
157class IResourcePool(Protocol):
158 """Interface for resource pools."""
160 def acquire(self, timeout: float | None = None) -> Any:
161 """Acquire a resource from the pool.
163 Args:
164 timeout: Acquisition timeout in seconds.
166 Returns:
167 The acquired resource.
169 Raises:
170 ResourceError: If acquisition fails.
171 """
172 ...
174 def release(self, resource: Any) -> None:
175 """Return a resource to the pool.
177 Args:
178 resource: The resource to return.
179 """
180 ...
182 def size(self) -> int:
183 """Get the current pool size.
185 Returns:
186 Number of resources in the pool.
187 """
188 ...
190 def available(self) -> int:
191 """Get the number of available resources.
193 Returns:
194 Number of available resources.
195 """
196 ...
198 def close(self) -> None:
199 """Close the pool and release all resources."""
200 ...
203class BaseResourceProvider(ABC):
204 """Base class for resource providers."""
206 def __init__(self, name: str, config: Dict[str, Any] | None = None):
207 """Initialize the provider.
209 Args:
210 name: Provider name.
211 config: Provider configuration.
212 """
213 self.name = name
214 self.config = config or {}
215 self.status = ResourceStatus.IDLE
216 self.metrics = ResourceMetrics()
217 self._resources: List[Any] = []
219 @abstractmethod
220 def acquire(self, **kwargs) -> Any:
221 """Acquire a resource.
223 Args:
224 **kwargs: Provider-specific parameters.
226 Returns:
227 The acquired resource.
228 """
229 pass
231 @abstractmethod
232 def release(self, resource: Any) -> None:
233 """Release a resource.
235 Args:
236 resource: The resource to release.
237 """
238 pass
240 def validate(self, resource: Any) -> bool:
241 """Validate a resource.
243 Args:
244 resource: The resource to validate.
246 Returns:
247 True if valid.
248 """
249 return resource is not None
251 def health_check(self) -> ResourceHealth:
252 """Check provider health.
254 Returns:
255 Health status.
256 """
257 if self.status == ResourceStatus.ERROR:
258 return ResourceHealth.UNHEALTHY
259 elif self.status == ResourceStatus.MAINTENANCE:
260 return ResourceHealth.DEGRADED
261 else:
262 return ResourceHealth.HEALTHY
264 def get_metrics(self) -> ResourceMetrics:
265 """Get provider metrics.
267 Returns:
268 Current metrics.
269 """
270 return self.metrics
272 @contextmanager
273 def resource_context(self, **kwargs):
274 """Context manager for resource acquisition.
276 Args:
277 **kwargs: Acquisition parameters.
279 Yields:
280 The acquired resource.
281 """
282 resource = None
283 start_time = datetime.now()
284 try:
285 resource = self.acquire(**kwargs)
286 self.metrics.record_acquisition()
287 yield resource
288 except Exception:
289 self.metrics.record_failure()
290 raise
291 finally:
292 if resource is not None:
293 hold_time = (datetime.now() - start_time).total_seconds()
294 self.release(resource)
295 self.metrics.record_release(hold_time)
297 def close(self) -> None:
298 """Close the provider and release all resources."""
299 for resource in self._resources[:]:
300 try:
301 self.release(resource)
302 except Exception:
303 pass # Best effort cleanup
304 self._resources.clear()
305 self.status = ResourceStatus.CLOSED