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,567 @@
|
|
|
1
|
+
"""Quota management for resource allocation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
from .requirements import ResourceType
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QuotaScope(Enum):
|
|
19
|
+
"""Scope of quota enforcement."""
|
|
20
|
+
|
|
21
|
+
AGENT = "agent" # Per-agent quotas
|
|
22
|
+
POOL = "pool" # Per-pool quotas
|
|
23
|
+
WORKFLOW = "workflow" # Per-workflow quotas
|
|
24
|
+
STATE = "state" # Per-state quotas
|
|
25
|
+
USER = "user" # Per-user quotas (for multi-tenancy)
|
|
26
|
+
GLOBAL = "global" # Global system quotas
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QuotaPolicy(Enum):
|
|
30
|
+
"""Quota enforcement policies."""
|
|
31
|
+
|
|
32
|
+
HARD = "hard" # Strict enforcement, reject if exceeds
|
|
33
|
+
SOFT = "soft" # Allow temporary exceed with warning
|
|
34
|
+
BURST = "burst" # Allow burst up to certain limit
|
|
35
|
+
RATE_LIMIT = "rate_limit" # Limit rate of resource usage
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class QuotaLimit:
|
|
40
|
+
"""Definition of a quota limit."""
|
|
41
|
+
|
|
42
|
+
resource_type: ResourceType
|
|
43
|
+
limit: float
|
|
44
|
+
scope: QuotaScope
|
|
45
|
+
policy: QuotaPolicy = QuotaPolicy.HARD
|
|
46
|
+
burst_limit: Optional[float] = None # For burst policy
|
|
47
|
+
rate_limit: Optional[float] = None # Requests per second
|
|
48
|
+
window_size: timedelta = field(default_factory=lambda: timedelta(minutes=1))
|
|
49
|
+
cooldown: timedelta = field(default_factory=lambda: timedelta(minutes=5))
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
if self.policy == QuotaPolicy.BURST and self.burst_limit is None:
|
|
53
|
+
self.burst_limit = self.limit * 1.5
|
|
54
|
+
if self.policy == QuotaPolicy.RATE_LIMIT and self.rate_limit is None:
|
|
55
|
+
self.rate_limit = self.limit
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class QuotaUsage:
|
|
60
|
+
"""Track quota usage."""
|
|
61
|
+
|
|
62
|
+
current: float = 0.0
|
|
63
|
+
peak: float = 0.0
|
|
64
|
+
total_allocated: float = 0.0
|
|
65
|
+
total_released: float = 0.0
|
|
66
|
+
allocations: int = 0
|
|
67
|
+
violations: int = 0
|
|
68
|
+
last_violation: Optional[datetime] = None
|
|
69
|
+
last_reset: datetime = field(default_factory=datetime.utcnow)
|
|
70
|
+
|
|
71
|
+
# For rate limiting
|
|
72
|
+
request_times: list[float] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
def reset(self) -> None:
|
|
75
|
+
"""Reset usage statistics."""
|
|
76
|
+
self.current = 0.0
|
|
77
|
+
self.allocations = 0
|
|
78
|
+
self.last_reset = datetime.utcnow()
|
|
79
|
+
self.request_times.clear()
|
|
80
|
+
|
|
81
|
+
def add_allocation(self, amount: float) -> None:
|
|
82
|
+
"""Record an allocation."""
|
|
83
|
+
self.current += amount
|
|
84
|
+
self.total_allocated += amount
|
|
85
|
+
self.peak = max(self.peak, self.current)
|
|
86
|
+
self.allocations += 1
|
|
87
|
+
self.request_times.append(time.time())
|
|
88
|
+
|
|
89
|
+
def remove_allocation(self, amount: float) -> None:
|
|
90
|
+
"""Record a release."""
|
|
91
|
+
self.current = max(0, self.current - amount)
|
|
92
|
+
self.total_released += amount
|
|
93
|
+
|
|
94
|
+
def record_violation(self) -> None:
|
|
95
|
+
"""Record a quota violation."""
|
|
96
|
+
self.violations += 1
|
|
97
|
+
self.last_violation = datetime.utcnow()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class QuotaMetrics:
|
|
102
|
+
"""Metrics for quota usage."""
|
|
103
|
+
|
|
104
|
+
scope: QuotaScope
|
|
105
|
+
scope_id: str
|
|
106
|
+
resource_type: ResourceType
|
|
107
|
+
usage: QuotaUsage
|
|
108
|
+
limit: QuotaLimit
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def utilization(self) -> float:
|
|
112
|
+
"""Get current utilization percentage."""
|
|
113
|
+
if self.limit.limit == 0:
|
|
114
|
+
return 0.0
|
|
115
|
+
return (self.usage.current / self.limit.limit) * 100
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def is_exceeded(self) -> bool:
|
|
119
|
+
"""Check if quota is exceeded."""
|
|
120
|
+
return self.usage.current > self.limit.limit
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict[str, Any]:
|
|
123
|
+
"""Convert to dictionary."""
|
|
124
|
+
return {
|
|
125
|
+
"scope": self.scope.value,
|
|
126
|
+
"scope_id": self.scope_id,
|
|
127
|
+
"resource_type": self.resource_type.name,
|
|
128
|
+
"current_usage": self.usage.current,
|
|
129
|
+
"limit": self.limit.limit,
|
|
130
|
+
"utilization": self.utilization,
|
|
131
|
+
"peak_usage": self.usage.peak,
|
|
132
|
+
"allocations": self.usage.allocations,
|
|
133
|
+
"violations": self.usage.violations,
|
|
134
|
+
"policy": self.limit.policy.value,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class QuotaExceededError(Exception):
|
|
139
|
+
"""Raised when quota is exceeded."""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
scope: QuotaScope,
|
|
144
|
+
scope_id: str,
|
|
145
|
+
resource_type: ResourceType,
|
|
146
|
+
requested: float,
|
|
147
|
+
available: float,
|
|
148
|
+
):
|
|
149
|
+
self.scope = scope
|
|
150
|
+
self.scope_id = scope_id
|
|
151
|
+
self.resource_type = resource_type
|
|
152
|
+
self.requested = requested
|
|
153
|
+
self.available = available
|
|
154
|
+
super().__init__(
|
|
155
|
+
f"Quota exceeded for {scope.value} '{scope_id}': "
|
|
156
|
+
f"requested {requested} {resource_type.name}, "
|
|
157
|
+
f"available {available}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class QuotaManager:
|
|
162
|
+
"""Manages resource quotas across different scopes."""
|
|
163
|
+
|
|
164
|
+
def __init__(self) -> None:
|
|
165
|
+
# Quota limits by scope
|
|
166
|
+
self._limits: dict[QuotaScope, dict[str, dict[ResourceType, QuotaLimit]]] = {
|
|
167
|
+
scope: defaultdict(dict) for scope in QuotaScope
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Usage tracking
|
|
171
|
+
self._usage: dict[QuotaScope, dict[str, dict[ResourceType, QuotaUsage]]] = {
|
|
172
|
+
scope: defaultdict(lambda: defaultdict(QuotaUsage)) for scope in QuotaScope
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Locks for thread safety
|
|
176
|
+
self._locks: dict[str, asyncio.Lock] = {}
|
|
177
|
+
|
|
178
|
+
# Background tasks
|
|
179
|
+
self._cleanup_task: Optional[asyncio.Task] = None
|
|
180
|
+
self._running = False
|
|
181
|
+
|
|
182
|
+
async def start(self) -> None:
|
|
183
|
+
"""Start the quota manager."""
|
|
184
|
+
self._running = True
|
|
185
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
186
|
+
logger.info("quota_manager_started")
|
|
187
|
+
|
|
188
|
+
async def stop(self) -> None:
|
|
189
|
+
"""Stop the quota manager."""
|
|
190
|
+
self._running = False
|
|
191
|
+
if self._cleanup_task:
|
|
192
|
+
self._cleanup_task.cancel()
|
|
193
|
+
await asyncio.gather(self._cleanup_task, return_exceptions=True)
|
|
194
|
+
logger.info("quota_manager_stopped")
|
|
195
|
+
|
|
196
|
+
def set_quota(
|
|
197
|
+
self,
|
|
198
|
+
scope: QuotaScope,
|
|
199
|
+
scope_id: str,
|
|
200
|
+
resource_type: ResourceType,
|
|
201
|
+
limit: Union[float, QuotaLimit],
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Set a quota limit."""
|
|
204
|
+
if isinstance(limit, (int, float)):
|
|
205
|
+
quota_limit = QuotaLimit(
|
|
206
|
+
resource_type=resource_type, limit=float(limit), scope=scope
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
quota_limit = limit
|
|
210
|
+
|
|
211
|
+
self._limits[scope][scope_id][resource_type] = quota_limit
|
|
212
|
+
|
|
213
|
+
logger.info(
|
|
214
|
+
"quota_set",
|
|
215
|
+
scope=scope.value,
|
|
216
|
+
scope_id=scope_id,
|
|
217
|
+
resource_type=resource_type.name,
|
|
218
|
+
limit=quota_limit.limit,
|
|
219
|
+
policy=quota_limit.policy.value,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def remove_quota(
|
|
223
|
+
self,
|
|
224
|
+
scope: QuotaScope,
|
|
225
|
+
scope_id: str,
|
|
226
|
+
resource_type: Optional[ResourceType] = None,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Remove a quota limit."""
|
|
229
|
+
if resource_type:
|
|
230
|
+
self._limits[scope][scope_id].pop(resource_type, None)
|
|
231
|
+
self._usage[scope][scope_id].pop(resource_type, None)
|
|
232
|
+
else:
|
|
233
|
+
# Remove all quotas for this scope_id
|
|
234
|
+
self._limits[scope].pop(scope_id, None)
|
|
235
|
+
self._usage[scope].pop(scope_id, None)
|
|
236
|
+
|
|
237
|
+
async def check_quota(
|
|
238
|
+
self,
|
|
239
|
+
scope: QuotaScope,
|
|
240
|
+
scope_id: str,
|
|
241
|
+
resource_type: ResourceType,
|
|
242
|
+
requested: float,
|
|
243
|
+
) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Check if allocation would exceed quota.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if allocation is allowed, False otherwise
|
|
249
|
+
"""
|
|
250
|
+
# Get lock for this scope_id
|
|
251
|
+
lock_key = f"{scope.value}:{scope_id}"
|
|
252
|
+
if lock_key not in self._locks:
|
|
253
|
+
self._locks[lock_key] = asyncio.Lock()
|
|
254
|
+
|
|
255
|
+
async with self._locks[lock_key]:
|
|
256
|
+
# Check if quota exists
|
|
257
|
+
if scope_id not in self._limits[scope]:
|
|
258
|
+
return True # No quota set
|
|
259
|
+
|
|
260
|
+
if resource_type not in self._limits[scope][scope_id]:
|
|
261
|
+
return True # No quota for this resource
|
|
262
|
+
|
|
263
|
+
limit = self._limits[scope][scope_id][resource_type]
|
|
264
|
+
usage = self._usage[scope][scope_id][resource_type]
|
|
265
|
+
|
|
266
|
+
# Check based on policy
|
|
267
|
+
if limit.policy == QuotaPolicy.HARD:
|
|
268
|
+
return usage.current + requested <= limit.limit
|
|
269
|
+
|
|
270
|
+
elif limit.policy == QuotaPolicy.SOFT:
|
|
271
|
+
# Allow but warn if exceeding
|
|
272
|
+
if usage.current + requested > limit.limit:
|
|
273
|
+
logger.warning(
|
|
274
|
+
"soft_quota_exceeded",
|
|
275
|
+
scope=scope.value,
|
|
276
|
+
scope_id=scope_id,
|
|
277
|
+
resource_type=resource_type.name,
|
|
278
|
+
current=usage.current,
|
|
279
|
+
requested=requested,
|
|
280
|
+
limit=limit.limit,
|
|
281
|
+
)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
elif limit.policy == QuotaPolicy.BURST:
|
|
285
|
+
# Allow burst up to burst_limit
|
|
286
|
+
return (
|
|
287
|
+
limit.burst_limit is not None
|
|
288
|
+
and usage.current + requested <= limit.burst_limit
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
else: # limit.policy == QuotaPolicy.RATE_LIMIT
|
|
292
|
+
# Check rate limit
|
|
293
|
+
return self._check_rate_limit(usage, limit)
|
|
294
|
+
|
|
295
|
+
def _check_rate_limit(self, usage: QuotaUsage, limit: QuotaLimit) -> bool:
|
|
296
|
+
"""Check if rate limit is exceeded."""
|
|
297
|
+
current_time = time.time()
|
|
298
|
+
window_start = current_time - limit.window_size.total_seconds()
|
|
299
|
+
|
|
300
|
+
# Remove old requests outside window
|
|
301
|
+
usage.request_times = [t for t in usage.request_times if t > window_start]
|
|
302
|
+
|
|
303
|
+
# Check rate
|
|
304
|
+
requests_in_window = len(usage.request_times)
|
|
305
|
+
max_requests = (limit.rate_limit or 0.0) * limit.window_size.total_seconds()
|
|
306
|
+
|
|
307
|
+
return requests_in_window < max_requests
|
|
308
|
+
|
|
309
|
+
async def allocate(
|
|
310
|
+
self,
|
|
311
|
+
scope: QuotaScope,
|
|
312
|
+
scope_id: str,
|
|
313
|
+
resource_type: ResourceType,
|
|
314
|
+
amount: float,
|
|
315
|
+
) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Allocate resources against quota.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
True if allocation succeeded, False otherwise
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
QuotaExceededError: If hard quota is exceeded
|
|
324
|
+
"""
|
|
325
|
+
# Check quota
|
|
326
|
+
if not await self.check_quota(scope, scope_id, resource_type, amount):
|
|
327
|
+
# Get current usage for error message
|
|
328
|
+
usage = self._usage[scope][scope_id][resource_type]
|
|
329
|
+
limit = self._limits[scope][scope_id][resource_type]
|
|
330
|
+
|
|
331
|
+
if limit.policy == QuotaPolicy.HARD:
|
|
332
|
+
raise QuotaExceededError(
|
|
333
|
+
scope, scope_id, resource_type, amount, limit.limit - usage.current
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
usage.record_violation()
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
# Record allocation
|
|
340
|
+
lock_key = f"{scope.value}:{scope_id}"
|
|
341
|
+
async with self._locks[lock_key]:
|
|
342
|
+
usage = self._usage[scope][scope_id][resource_type]
|
|
343
|
+
usage.add_allocation(amount)
|
|
344
|
+
|
|
345
|
+
return True
|
|
346
|
+
|
|
347
|
+
async def release(
|
|
348
|
+
self,
|
|
349
|
+
scope: QuotaScope,
|
|
350
|
+
scope_id: str,
|
|
351
|
+
resource_type: ResourceType,
|
|
352
|
+
amount: float,
|
|
353
|
+
) -> None:
|
|
354
|
+
"""Release allocated resources."""
|
|
355
|
+
lock_key = f"{scope.value}:{scope_id}"
|
|
356
|
+
if lock_key not in self._locks:
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
async with self._locks[lock_key]:
|
|
360
|
+
if (
|
|
361
|
+
scope_id in self._usage[scope]
|
|
362
|
+
and resource_type in self._usage[scope][scope_id]
|
|
363
|
+
):
|
|
364
|
+
usage = self._usage[scope][scope_id][resource_type]
|
|
365
|
+
usage.remove_allocation(amount)
|
|
366
|
+
|
|
367
|
+
def get_usage(
|
|
368
|
+
self,
|
|
369
|
+
scope: QuotaScope,
|
|
370
|
+
scope_id: str,
|
|
371
|
+
resource_type: Optional[ResourceType] = None,
|
|
372
|
+
) -> Union[QuotaUsage, dict[ResourceType, QuotaUsage]]:
|
|
373
|
+
"""Get current usage for a scope."""
|
|
374
|
+
if scope_id not in self._usage[scope]:
|
|
375
|
+
return {} if resource_type is None else QuotaUsage()
|
|
376
|
+
|
|
377
|
+
if resource_type:
|
|
378
|
+
return self._usage[scope][scope_id].get(resource_type, QuotaUsage())
|
|
379
|
+
else:
|
|
380
|
+
return dict(self._usage[scope][scope_id])
|
|
381
|
+
|
|
382
|
+
def get_metrics(
|
|
383
|
+
self, scope: Optional[QuotaScope] = None, scope_id: Optional[str] = None
|
|
384
|
+
) -> list[QuotaMetrics]:
|
|
385
|
+
"""Get quota metrics."""
|
|
386
|
+
metrics = []
|
|
387
|
+
|
|
388
|
+
scopes = [scope] if scope else list(QuotaScope)
|
|
389
|
+
|
|
390
|
+
for s in scopes:
|
|
391
|
+
scope_ids = [scope_id] if scope_id else list(self._limits[s].keys())
|
|
392
|
+
|
|
393
|
+
for sid in scope_ids:
|
|
394
|
+
if sid not in self._limits[s]:
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
for resource_type, limit in self._limits[s][sid].items():
|
|
398
|
+
usage = self._usage[s][sid].get(resource_type, QuotaUsage())
|
|
399
|
+
|
|
400
|
+
metrics.append(
|
|
401
|
+
QuotaMetrics(
|
|
402
|
+
scope=s,
|
|
403
|
+
scope_id=sid,
|
|
404
|
+
resource_type=resource_type,
|
|
405
|
+
usage=usage,
|
|
406
|
+
limit=limit,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
return metrics
|
|
411
|
+
|
|
412
|
+
def reset_usage(
|
|
413
|
+
self,
|
|
414
|
+
scope: QuotaScope,
|
|
415
|
+
scope_id: str,
|
|
416
|
+
resource_type: Optional[ResourceType] = None,
|
|
417
|
+
) -> None:
|
|
418
|
+
"""Reset usage statistics."""
|
|
419
|
+
if scope_id in self._usage[scope]:
|
|
420
|
+
if resource_type:
|
|
421
|
+
if resource_type in self._usage[scope][scope_id]:
|
|
422
|
+
self._usage[scope][scope_id][resource_type].reset()
|
|
423
|
+
else:
|
|
424
|
+
for usage in self._usage[scope][scope_id].values():
|
|
425
|
+
usage.reset()
|
|
426
|
+
|
|
427
|
+
async def _cleanup_expired(self) -> None:
|
|
428
|
+
"""Clean up expired usage data."""
|
|
429
|
+
current_time = time.time()
|
|
430
|
+
|
|
431
|
+
# Clean up old rate limit data
|
|
432
|
+
for scope in self._usage.values():
|
|
433
|
+
for scope_id_usage in scope.values():
|
|
434
|
+
for usage in scope_id_usage.values():
|
|
435
|
+
# Keep only recent request times
|
|
436
|
+
cutoff = current_time - 3600 # Keep 1 hour
|
|
437
|
+
usage.request_times = [t for t in usage.request_times if t > cutoff]
|
|
438
|
+
|
|
439
|
+
logger.debug("quota_cleanup_completed")
|
|
440
|
+
|
|
441
|
+
async def _cleanup_loop(self) -> None:
|
|
442
|
+
"""Periodic cleanup of old usage data."""
|
|
443
|
+
while self._running:
|
|
444
|
+
try:
|
|
445
|
+
await asyncio.sleep(3600) # Run hourly
|
|
446
|
+
await self._cleanup_expired()
|
|
447
|
+
|
|
448
|
+
except asyncio.CancelledError:
|
|
449
|
+
break
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error("quota_cleanup_error", error=str(e))
|
|
452
|
+
|
|
453
|
+
def apply_quota_policy(
|
|
454
|
+
self,
|
|
455
|
+
policy_name: str,
|
|
456
|
+
quotas: dict[ResourceType, float],
|
|
457
|
+
scope: QuotaScope = QuotaScope.AGENT,
|
|
458
|
+
policy: QuotaPolicy = QuotaPolicy.HARD,
|
|
459
|
+
) -> dict[str, Any]:
|
|
460
|
+
"""Apply a quota policy to multiple scope IDs."""
|
|
461
|
+
# This would be used to apply standard quota templates
|
|
462
|
+
return {
|
|
463
|
+
"policy_name": policy_name,
|
|
464
|
+
"quotas": quotas,
|
|
465
|
+
"scope": scope,
|
|
466
|
+
"policy": policy,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class QuotaEnforcer:
|
|
471
|
+
"""Enforces quotas during resource allocation."""
|
|
472
|
+
|
|
473
|
+
def __init__(self, quota_manager: QuotaManager):
|
|
474
|
+
self.quota_manager = quota_manager
|
|
475
|
+
|
|
476
|
+
async def check_all_quotas(
|
|
477
|
+
self, requests: list[tuple[QuotaScope, str, ResourceType, float]]
|
|
478
|
+
) -> tuple[bool, list[str]]:
|
|
479
|
+
"""
|
|
480
|
+
Check multiple quota requests.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Tuple of (all_allowed, list_of_violations)
|
|
484
|
+
"""
|
|
485
|
+
violations = []
|
|
486
|
+
|
|
487
|
+
for scope, scope_id, resource_type, amount in requests:
|
|
488
|
+
try:
|
|
489
|
+
if not await self.quota_manager.check_quota(
|
|
490
|
+
scope, scope_id, resource_type, amount
|
|
491
|
+
):
|
|
492
|
+
violations.append(
|
|
493
|
+
f"{scope.value} '{scope_id}' exceeds "
|
|
494
|
+
f"{resource_type.name} quota"
|
|
495
|
+
)
|
|
496
|
+
except QuotaExceededError as e:
|
|
497
|
+
violations.append(str(e))
|
|
498
|
+
|
|
499
|
+
return len(violations) == 0, violations
|
|
500
|
+
|
|
501
|
+
async def allocate_with_quotas(
|
|
502
|
+
self, allocations: list[tuple[QuotaScope, str, ResourceType, float]]
|
|
503
|
+
) -> tuple[bool, list[tuple[QuotaScope, str, ResourceType, float]]]:
|
|
504
|
+
"""
|
|
505
|
+
Allocate resources with quota enforcement.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Tuple of (success, list_of_allocated_resources)
|
|
509
|
+
"""
|
|
510
|
+
allocated = []
|
|
511
|
+
|
|
512
|
+
try:
|
|
513
|
+
# First check all quotas
|
|
514
|
+
allowed, violations = await self.check_all_quotas(allocations)
|
|
515
|
+
if not allowed:
|
|
516
|
+
return False, []
|
|
517
|
+
|
|
518
|
+
# Allocate all
|
|
519
|
+
for scope, scope_id, resource_type, amount in allocations:
|
|
520
|
+
await self.quota_manager.allocate(
|
|
521
|
+
scope, scope_id, resource_type, amount
|
|
522
|
+
)
|
|
523
|
+
allocated.append((scope, scope_id, resource_type, amount))
|
|
524
|
+
|
|
525
|
+
return True, allocated
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
# Rollback allocations
|
|
529
|
+
for scope, scope_id, resource_type, amount in allocated:
|
|
530
|
+
await self.quota_manager.release(scope, scope_id, resource_type, amount)
|
|
531
|
+
|
|
532
|
+
logger.error("quota_allocation_failed", error=str(e))
|
|
533
|
+
return False, []
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# Predefined quota policies
|
|
537
|
+
class QuotaPolicies:
|
|
538
|
+
"""Common quota policies."""
|
|
539
|
+
|
|
540
|
+
SMALL_AGENT = {
|
|
541
|
+
ResourceType.CPU: 2.0,
|
|
542
|
+
ResourceType.MEMORY: 512.0,
|
|
543
|
+
ResourceType.IO: 10.0,
|
|
544
|
+
ResourceType.NETWORK: 10.0,
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
MEDIUM_AGENT = {
|
|
548
|
+
ResourceType.CPU: 4.0,
|
|
549
|
+
ResourceType.MEMORY: 2048.0,
|
|
550
|
+
ResourceType.IO: 50.0,
|
|
551
|
+
ResourceType.NETWORK: 50.0,
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
LARGE_AGENT = {
|
|
555
|
+
ResourceType.CPU: 8.0,
|
|
556
|
+
ResourceType.MEMORY: 8192.0,
|
|
557
|
+
ResourceType.IO: 100.0,
|
|
558
|
+
ResourceType.NETWORK: 100.0,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
GPU_AGENT = {
|
|
562
|
+
ResourceType.CPU: 4.0,
|
|
563
|
+
ResourceType.MEMORY: 16384.0,
|
|
564
|
+
ResourceType.GPU: 1.0,
|
|
565
|
+
ResourceType.IO: 100.0,
|
|
566
|
+
ResourceType.NETWORK: 100.0,
|
|
567
|
+
}
|