puffinflow 2.dev0__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.
- puffinflow/__init__.py +132 -0
- puffinflow/core/__init__.py +110 -0
- puffinflow/core/agent/__init__.py +320 -0
- puffinflow/core/agent/base.py +1635 -0
- puffinflow/core/agent/checkpoint.py +50 -0
- puffinflow/core/agent/context.py +521 -0
- puffinflow/core/agent/decorators/__init__.py +90 -0
- puffinflow/core/agent/decorators/builder.py +454 -0
- puffinflow/core/agent/decorators/flexible.py +714 -0
- puffinflow/core/agent/decorators/inspection.py +144 -0
- puffinflow/core/agent/dependencies.py +57 -0
- puffinflow/core/agent/scheduling/__init__.py +21 -0
- puffinflow/core/agent/scheduling/builder.py +160 -0
- puffinflow/core/agent/scheduling/exceptions.py +35 -0
- puffinflow/core/agent/scheduling/inputs.py +137 -0
- puffinflow/core/agent/scheduling/parser.py +209 -0
- puffinflow/core/agent/scheduling/scheduler.py +413 -0
- puffinflow/core/agent/state.py +141 -0
- puffinflow/core/config.py +62 -0
- puffinflow/core/coordination/__init__.py +137 -0
- puffinflow/core/coordination/agent_group.py +359 -0
- puffinflow/core/coordination/agent_pool.py +629 -0
- puffinflow/core/coordination/agent_team.py +577 -0
- puffinflow/core/coordination/coordinator.py +720 -0
- puffinflow/core/coordination/deadlock.py +1759 -0
- puffinflow/core/coordination/fluent_api.py +421 -0
- puffinflow/core/coordination/primitives.py +478 -0
- puffinflow/core/coordination/rate_limiter.py +520 -0
- puffinflow/core/observability/__init__.py +47 -0
- puffinflow/core/observability/agent.py +139 -0
- puffinflow/core/observability/alerting.py +73 -0
- puffinflow/core/observability/config.py +127 -0
- puffinflow/core/observability/context.py +88 -0
- puffinflow/core/observability/core.py +147 -0
- puffinflow/core/observability/decorators.py +105 -0
- puffinflow/core/observability/events.py +71 -0
- puffinflow/core/observability/interfaces.py +196 -0
- puffinflow/core/observability/metrics.py +137 -0
- puffinflow/core/observability/tracing.py +209 -0
- puffinflow/core/reliability/__init__.py +27 -0
- puffinflow/core/reliability/bulkhead.py +96 -0
- puffinflow/core/reliability/circuit_breaker.py +149 -0
- puffinflow/core/reliability/leak_detector.py +122 -0
- puffinflow/core/resources/__init__.py +77 -0
- puffinflow/core/resources/allocation.py +790 -0
- puffinflow/core/resources/pool.py +645 -0
- puffinflow/core/resources/quotas.py +567 -0
- puffinflow/core/resources/requirements.py +217 -0
- puffinflow/version.py +21 -0
- puffinflow-2.dev0.dist-info/METADATA +334 -0
- puffinflow-2.dev0.dist-info/RECORD +55 -0
- puffinflow-2.dev0.dist-info/WHEEL +5 -0
- puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
- puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
- puffinflow-2.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Lightweight circuit breaker implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CircuitState(Enum):
|
|
13
|
+
CLOSED = "closed" # Normal operation
|
|
14
|
+
OPEN = "open" # Failing, blocking requests
|
|
15
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CircuitBreakerConfig:
|
|
20
|
+
failure_threshold: int = 5
|
|
21
|
+
recovery_timeout: float = 60.0
|
|
22
|
+
success_threshold: int = 3 # For half-open state
|
|
23
|
+
timeout: float = 30.0
|
|
24
|
+
name: str = "default"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CircuitBreakerError(Exception):
|
|
28
|
+
"""Raised when circuit breaker is open"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CircuitBreaker:
|
|
34
|
+
"""Lightweight circuit breaker for state execution"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: CircuitBreakerConfig):
|
|
37
|
+
self.config = config
|
|
38
|
+
self.state = CircuitState.CLOSED
|
|
39
|
+
self._failure_count = 0
|
|
40
|
+
self._success_count = 0
|
|
41
|
+
self._last_failure_time = 0
|
|
42
|
+
self._lock = asyncio.Lock()
|
|
43
|
+
|
|
44
|
+
@asynccontextmanager
|
|
45
|
+
async def protect(self) -> AsyncGenerator[None, None]:
|
|
46
|
+
"""Context manager for protecting code blocks"""
|
|
47
|
+
async with self._lock:
|
|
48
|
+
await self._check_state()
|
|
49
|
+
|
|
50
|
+
if self.state == CircuitState.OPEN:
|
|
51
|
+
raise CircuitBreakerError(
|
|
52
|
+
f"Circuit breaker '{self.config.name}' is OPEN"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
time.time()
|
|
56
|
+
try:
|
|
57
|
+
yield
|
|
58
|
+
await self._record_success()
|
|
59
|
+
except Exception:
|
|
60
|
+
await self._record_failure()
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
async def _check_state(self) -> None:
|
|
64
|
+
"""Check and update circuit breaker state"""
|
|
65
|
+
now = time.time()
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
self.state == CircuitState.OPEN
|
|
69
|
+
and now - self._last_failure_time >= self.config.recovery_timeout
|
|
70
|
+
):
|
|
71
|
+
self.state = CircuitState.HALF_OPEN
|
|
72
|
+
self._success_count = 0
|
|
73
|
+
|
|
74
|
+
elif self.state == CircuitState.HALF_OPEN:
|
|
75
|
+
if self._success_count >= self.config.success_threshold:
|
|
76
|
+
self.state = CircuitState.CLOSED
|
|
77
|
+
self._failure_count = 0
|
|
78
|
+
|
|
79
|
+
async def _record_success(self) -> None:
|
|
80
|
+
"""Record successful execution"""
|
|
81
|
+
async with self._lock:
|
|
82
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
83
|
+
self._success_count += 1
|
|
84
|
+
elif self.state == CircuitState.CLOSED:
|
|
85
|
+
self._failure_count = max(
|
|
86
|
+
0, self._failure_count - 1
|
|
87
|
+
) # Gradually recover
|
|
88
|
+
|
|
89
|
+
async def _record_failure(self) -> None:
|
|
90
|
+
"""Record failed execution"""
|
|
91
|
+
async with self._lock:
|
|
92
|
+
now = time.time()
|
|
93
|
+
self._failure_count += 1
|
|
94
|
+
self._last_failure_time = int(now)
|
|
95
|
+
self._success_count = 0
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
self.state == CircuitState.CLOSED
|
|
99
|
+
and self._failure_count >= self.config.failure_threshold
|
|
100
|
+
) or self.state == CircuitState.HALF_OPEN:
|
|
101
|
+
self.state = CircuitState.OPEN
|
|
102
|
+
|
|
103
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
104
|
+
"""Get current metrics"""
|
|
105
|
+
return {
|
|
106
|
+
"name": self.config.name,
|
|
107
|
+
"state": self.state.value,
|
|
108
|
+
"failure_count": self._failure_count,
|
|
109
|
+
"success_count": self._success_count,
|
|
110
|
+
"last_failure_time": self._last_failure_time,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async def force_open(self) -> None:
|
|
114
|
+
"""Manually open the circuit"""
|
|
115
|
+
async with self._lock:
|
|
116
|
+
self.state = CircuitState.OPEN
|
|
117
|
+
self._last_failure_time = int(time.time())
|
|
118
|
+
|
|
119
|
+
async def force_close(self) -> None:
|
|
120
|
+
"""Manually close the circuit"""
|
|
121
|
+
async with self._lock:
|
|
122
|
+
self.state = CircuitState.CLOSED
|
|
123
|
+
self._failure_count = 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Global circuit breaker registry
|
|
127
|
+
class CircuitBreakerRegistry:
|
|
128
|
+
"""Simple registry for circuit breakers"""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self._breakers: dict[str, CircuitBreaker] = {}
|
|
132
|
+
|
|
133
|
+
def get_or_create(
|
|
134
|
+
self, name: str, config: Optional[CircuitBreakerConfig] = None
|
|
135
|
+
) -> CircuitBreaker:
|
|
136
|
+
"""Get existing or create new circuit breaker"""
|
|
137
|
+
if name not in self._breakers:
|
|
138
|
+
if config is None:
|
|
139
|
+
config = CircuitBreakerConfig(name=name)
|
|
140
|
+
self._breakers[name] = CircuitBreaker(config)
|
|
141
|
+
return self._breakers[name]
|
|
142
|
+
|
|
143
|
+
def get_all_metrics(self) -> dict[str, dict[str, Any]]:
|
|
144
|
+
"""Get metrics for all circuit breakers"""
|
|
145
|
+
return {name: breaker.get_metrics() for name, breaker in self._breakers.items()}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Global registry instance
|
|
149
|
+
circuit_registry = CircuitBreakerRegistry()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Resource leak detection."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ResourceLeak:
|
|
10
|
+
state_name: str
|
|
11
|
+
agent_name: str
|
|
12
|
+
resources: dict[str, float]
|
|
13
|
+
allocated_at: float
|
|
14
|
+
held_for_seconds: float
|
|
15
|
+
leak_threshold_seconds: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ResourceAllocation:
|
|
20
|
+
state_name: str
|
|
21
|
+
agent_name: str
|
|
22
|
+
resources: dict[str, float]
|
|
23
|
+
allocated_at: float
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResourceLeakDetector:
|
|
27
|
+
"""Detect and handle resource leaks"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, leak_threshold_seconds: float = 300.0): # 5 minutes default
|
|
30
|
+
self.leak_threshold = leak_threshold_seconds
|
|
31
|
+
self.allocations: dict[str, ResourceAllocation] = {}
|
|
32
|
+
self.detected_leaks: list[ResourceLeak] = []
|
|
33
|
+
self._max_leaks_history = 100
|
|
34
|
+
|
|
35
|
+
def track_allocation(
|
|
36
|
+
self, state_name: str, agent_name: str, resources: dict[str, float]
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Track resource allocation"""
|
|
39
|
+
key = f"{agent_name}:{state_name}"
|
|
40
|
+
self.allocations[key] = ResourceAllocation(
|
|
41
|
+
state_name=state_name,
|
|
42
|
+
agent_name=agent_name,
|
|
43
|
+
resources=resources.copy(),
|
|
44
|
+
allocated_at=time.time(),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def track_release(self, state_name: str, agent_name: str) -> None:
|
|
48
|
+
"""Track resource release"""
|
|
49
|
+
key = f"{agent_name}:{state_name}"
|
|
50
|
+
if key in self.allocations:
|
|
51
|
+
del self.allocations[key]
|
|
52
|
+
|
|
53
|
+
def detect_leaks(self) -> list[ResourceLeak]:
|
|
54
|
+
"""Find resources held too long"""
|
|
55
|
+
current_leaks = []
|
|
56
|
+
now = time.time()
|
|
57
|
+
|
|
58
|
+
for _key, allocation in self.allocations.items():
|
|
59
|
+
held_for = now - allocation.allocated_at
|
|
60
|
+
|
|
61
|
+
if held_for > self.leak_threshold:
|
|
62
|
+
leak = ResourceLeak(
|
|
63
|
+
state_name=allocation.state_name,
|
|
64
|
+
agent_name=allocation.agent_name,
|
|
65
|
+
resources=allocation.resources.copy(),
|
|
66
|
+
allocated_at=allocation.allocated_at,
|
|
67
|
+
held_for_seconds=held_for,
|
|
68
|
+
leak_threshold_seconds=self.leak_threshold,
|
|
69
|
+
)
|
|
70
|
+
current_leaks.append(leak)
|
|
71
|
+
|
|
72
|
+
# Add to history if not already there
|
|
73
|
+
if not any(
|
|
74
|
+
leak_item.state_name == leak.state_name
|
|
75
|
+
and leak_item.agent_name == leak.agent_name
|
|
76
|
+
and abs(leak_item.allocated_at - leak.allocated_at) < 1.0
|
|
77
|
+
for leak_item in self.detected_leaks
|
|
78
|
+
):
|
|
79
|
+
self.detected_leaks.append(leak)
|
|
80
|
+
|
|
81
|
+
# Trim history
|
|
82
|
+
if len(self.detected_leaks) > self._max_leaks_history:
|
|
83
|
+
self.detected_leaks = self.detected_leaks[-self._max_leaks_history :]
|
|
84
|
+
|
|
85
|
+
return current_leaks
|
|
86
|
+
|
|
87
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
88
|
+
"""Get leak detection metrics"""
|
|
89
|
+
current_leaks = self.detect_leaks()
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"total_allocations": len(self.allocations),
|
|
93
|
+
"current_leaks": len(current_leaks),
|
|
94
|
+
"total_detected_leaks": len(self.detected_leaks),
|
|
95
|
+
"leak_threshold_seconds": self.leak_threshold,
|
|
96
|
+
"oldest_allocation_age": self._get_oldest_allocation_age(),
|
|
97
|
+
"leaks_by_agent": self._group_leaks_by_agent(current_leaks),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
def _get_oldest_allocation_age(self) -> Optional[float]:
|
|
101
|
+
"""Get age of oldest allocation"""
|
|
102
|
+
if not self.allocations:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
now = time.time()
|
|
106
|
+
oldest = min(alloc.allocated_at for alloc in self.allocations.values())
|
|
107
|
+
return now - oldest
|
|
108
|
+
|
|
109
|
+
def _group_leaks_by_agent(self, leaks: list[ResourceLeak]) -> dict[str, int]:
|
|
110
|
+
"""Group leaks by agent"""
|
|
111
|
+
groups: dict[str, int] = {}
|
|
112
|
+
for leak in leaks:
|
|
113
|
+
groups[leak.agent_name] = groups.get(leak.agent_name, 0) + 1
|
|
114
|
+
return groups
|
|
115
|
+
|
|
116
|
+
def clear_leak_history(self) -> None:
|
|
117
|
+
"""Clear leak detection history"""
|
|
118
|
+
self.detected_leaks.clear()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Global leak detector instance
|
|
122
|
+
leak_detector = ResourceLeakDetector()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Resource management module for workflow orchestrator."""
|
|
2
|
+
|
|
3
|
+
# Import submodules for import path tests
|
|
4
|
+
from . import allocation, pool, quotas, requirements
|
|
5
|
+
from .allocation import (
|
|
6
|
+
AllocationRequest,
|
|
7
|
+
AllocationResult,
|
|
8
|
+
AllocationStrategy,
|
|
9
|
+
BestFitAllocator,
|
|
10
|
+
FairShareAllocator,
|
|
11
|
+
FirstFitAllocator,
|
|
12
|
+
PriorityAllocator,
|
|
13
|
+
ResourceAllocator,
|
|
14
|
+
WorstFitAllocator,
|
|
15
|
+
)
|
|
16
|
+
from .pool import (
|
|
17
|
+
ResourceAllocationError,
|
|
18
|
+
ResourceOverflowError,
|
|
19
|
+
ResourcePool,
|
|
20
|
+
ResourceQuotaExceededError,
|
|
21
|
+
ResourceUsageStats,
|
|
22
|
+
)
|
|
23
|
+
from .quotas import (
|
|
24
|
+
QuotaExceededError,
|
|
25
|
+
QuotaLimit,
|
|
26
|
+
QuotaManager,
|
|
27
|
+
QuotaMetrics,
|
|
28
|
+
QuotaPolicy,
|
|
29
|
+
QuotaScope,
|
|
30
|
+
)
|
|
31
|
+
from .requirements import (
|
|
32
|
+
ResourceRequirements,
|
|
33
|
+
ResourceType,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AllocationRequest",
|
|
38
|
+
"AllocationResult",
|
|
39
|
+
# Allocation
|
|
40
|
+
"AllocationStrategy",
|
|
41
|
+
"BestFitAllocator",
|
|
42
|
+
"FairShareAllocator",
|
|
43
|
+
"FirstFitAllocator",
|
|
44
|
+
"PriorityAllocator",
|
|
45
|
+
"QuotaExceededError",
|
|
46
|
+
"QuotaLimit",
|
|
47
|
+
# Quotas
|
|
48
|
+
"QuotaManager",
|
|
49
|
+
"QuotaMetrics",
|
|
50
|
+
"QuotaPolicy",
|
|
51
|
+
"QuotaScope",
|
|
52
|
+
"ResourceAllocationError",
|
|
53
|
+
"ResourceAllocator",
|
|
54
|
+
"ResourceOverflowError",
|
|
55
|
+
# Pool
|
|
56
|
+
"ResourcePool",
|
|
57
|
+
"ResourceQuotaExceededError",
|
|
58
|
+
"ResourceRequirements",
|
|
59
|
+
# Requirements
|
|
60
|
+
"ResourceType",
|
|
61
|
+
"ResourceUsageStats",
|
|
62
|
+
"WorstFitAllocator",
|
|
63
|
+
"allocation",
|
|
64
|
+
# Submodules
|
|
65
|
+
"pool",
|
|
66
|
+
"quotas",
|
|
67
|
+
"requirements",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Clean up module namespace
|
|
71
|
+
import sys as _sys
|
|
72
|
+
|
|
73
|
+
_current_module = _sys.modules[__name__]
|
|
74
|
+
for _attr_name in dir(_current_module):
|
|
75
|
+
if not _attr_name.startswith("_") and _attr_name not in __all__:
|
|
76
|
+
delattr(_current_module, _attr_name)
|
|
77
|
+
del _sys, _current_module, _attr_name
|