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,478 @@
|
|
|
1
|
+
"""Coordination primitives for distributed systems."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from enum import Enum, auto
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PrimitiveType(Enum):
|
|
17
|
+
"""Coordination primitive types"""
|
|
18
|
+
|
|
19
|
+
MUTEX = auto() # Exclusive access
|
|
20
|
+
SEMAPHORE = auto() # Limited concurrent access
|
|
21
|
+
BARRIER = auto() # Synchronization point
|
|
22
|
+
LEASE = auto() # Time-based exclusive access
|
|
23
|
+
LOCK = auto() # Simple lock
|
|
24
|
+
QUOTA = auto() # Resource quota management
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ResourceState(Enum):
|
|
28
|
+
"""Resource states"""
|
|
29
|
+
|
|
30
|
+
AVAILABLE = "available"
|
|
31
|
+
ACQUIRED = "acquired"
|
|
32
|
+
LOCKED = "locked"
|
|
33
|
+
EXPIRED = "expired"
|
|
34
|
+
ERROR = "error"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class CoordinationPrimitive:
|
|
39
|
+
"""Enhanced coordination primitive"""
|
|
40
|
+
|
|
41
|
+
name: str
|
|
42
|
+
type: PrimitiveType
|
|
43
|
+
ttl: float = 30.0
|
|
44
|
+
max_count: int = 1
|
|
45
|
+
wait_timeout: Optional[float] = None
|
|
46
|
+
quota_limit: Optional[float] = None
|
|
47
|
+
|
|
48
|
+
# Internal state
|
|
49
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
|
50
|
+
_condition: asyncio.Condition = field(default_factory=asyncio.Condition)
|
|
51
|
+
_owners: set[str] = field(default_factory=set)
|
|
52
|
+
_acquired_times: dict[str, float] = field(default_factory=dict)
|
|
53
|
+
_quota_usage: dict[str, float] = field(default_factory=dict)
|
|
54
|
+
_wait_count: int = 0
|
|
55
|
+
_state: ResourceState = field(default=ResourceState.AVAILABLE)
|
|
56
|
+
_last_error: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
async def acquire(
|
|
59
|
+
self,
|
|
60
|
+
caller_id: str,
|
|
61
|
+
timeout: Optional[float] = None,
|
|
62
|
+
quota_amount: Optional[float] = None,
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""Acquire the primitive"""
|
|
65
|
+
try:
|
|
66
|
+
async with self._lock:
|
|
67
|
+
# Handle quota type specially
|
|
68
|
+
if self.type == PrimitiveType.QUOTA:
|
|
69
|
+
if quota_amount is None:
|
|
70
|
+
raise ValueError("Quota amount required")
|
|
71
|
+
current_usage = sum(self._quota_usage.values())
|
|
72
|
+
if current_usage + quota_amount <= (self.quota_limit or 0):
|
|
73
|
+
self._quota_usage[caller_id] = (
|
|
74
|
+
self._quota_usage.get(caller_id, 0) + quota_amount
|
|
75
|
+
)
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Check existing ownership
|
|
80
|
+
if caller_id in self._owners:
|
|
81
|
+
self._acquired_times[caller_id] = time.time()
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Handle different primitive types
|
|
85
|
+
if self.type == PrimitiveType.MUTEX:
|
|
86
|
+
if not self._owners:
|
|
87
|
+
self._acquire_for(caller_id)
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
elif self.type == PrimitiveType.SEMAPHORE:
|
|
91
|
+
if len(self._owners) < self.max_count:
|
|
92
|
+
self._acquire_for(caller_id)
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
elif self.type == PrimitiveType.BARRIER:
|
|
96
|
+
# Unify the condition's lock with the primitive's lock on first use.
|
|
97
|
+
if not hasattr(self, "_barrier_lock_unified"):
|
|
98
|
+
self._condition = asyncio.Condition(self._lock)
|
|
99
|
+
self._barrier_lock_unified = True
|
|
100
|
+
|
|
101
|
+
self._acquire_for(caller_id)
|
|
102
|
+
|
|
103
|
+
if len(self._owners) < self.max_count:
|
|
104
|
+
# We must wait for other parties.
|
|
105
|
+
self._wait_count += 1
|
|
106
|
+
try:
|
|
107
|
+
# The `wait()` method will atomically release the lock and block until notified.
|
|
108
|
+
await asyncio.wait_for(
|
|
109
|
+
self._condition.wait(),
|
|
110
|
+
timeout=timeout or self.wait_timeout,
|
|
111
|
+
)
|
|
112
|
+
# Woke up successfully.
|
|
113
|
+
return True
|
|
114
|
+
except asyncio.TimeoutError:
|
|
115
|
+
# Timed out waiting for others.
|
|
116
|
+
self._remove_owner(caller_id)
|
|
117
|
+
return False
|
|
118
|
+
finally:
|
|
119
|
+
self._wait_count -= 1
|
|
120
|
+
else:
|
|
121
|
+
# We are the last party. Notify all waiters and proceed.
|
|
122
|
+
self._condition.notify_all()
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
elif self.type == PrimitiveType.LEASE:
|
|
126
|
+
self._cleanup_expired()
|
|
127
|
+
if not self._owners:
|
|
128
|
+
self._acquire_for(caller_id)
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
self._state = ResourceState.ERROR
|
|
135
|
+
self._last_error = str(e)
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def _acquire_for(self, caller_id: str) -> None:
|
|
139
|
+
"""Internal acquisition helper"""
|
|
140
|
+
self._owners.add(caller_id)
|
|
141
|
+
self._acquired_times[caller_id] = time.time()
|
|
142
|
+
self._state = ResourceState.ACQUIRED
|
|
143
|
+
|
|
144
|
+
def _remove_owner(self, caller_id: str) -> None:
|
|
145
|
+
"""Internal removal helper"""
|
|
146
|
+
self._owners.discard(caller_id)
|
|
147
|
+
self._acquired_times.pop(caller_id, None)
|
|
148
|
+
self._quota_usage.pop(caller_id, None)
|
|
149
|
+
if not self._owners:
|
|
150
|
+
self._state = ResourceState.AVAILABLE
|
|
151
|
+
|
|
152
|
+
def _cleanup_expired(self) -> None:
|
|
153
|
+
"""Clean up expired acquisitions"""
|
|
154
|
+
now = time.time()
|
|
155
|
+
expired = [
|
|
156
|
+
owner
|
|
157
|
+
for owner, acquired in self._acquired_times.items()
|
|
158
|
+
if now - acquired > self.ttl
|
|
159
|
+
]
|
|
160
|
+
for owner in expired:
|
|
161
|
+
self._remove_owner(owner)
|
|
162
|
+
|
|
163
|
+
async def release(self, caller_id: str) -> bool:
|
|
164
|
+
"""Release the primitive"""
|
|
165
|
+
async with self._lock:
|
|
166
|
+
# The acquire method for QUOTA does not add to _owners, so we handle its release separately.
|
|
167
|
+
if self.type == PrimitiveType.QUOTA and caller_id in self._quota_usage:
|
|
168
|
+
self._quota_usage.pop(caller_id)
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
if caller_id in self._owners:
|
|
172
|
+
self._remove_owner(caller_id)
|
|
173
|
+
|
|
174
|
+
if self.type == PrimitiveType.BARRIER and self._wait_count > 0:
|
|
175
|
+
async with self._condition:
|
|
176
|
+
self._condition.notify_all()
|
|
177
|
+
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def get_state(self) -> dict[str, Any]:
|
|
183
|
+
"""Get current state information"""
|
|
184
|
+
return {
|
|
185
|
+
"state": self._state.value,
|
|
186
|
+
"owners": list(self._owners),
|
|
187
|
+
"wait_count": self._wait_count,
|
|
188
|
+
"quota_usage": dict(self._quota_usage),
|
|
189
|
+
"last_error": self._last_error,
|
|
190
|
+
"ttl_remaining": (
|
|
191
|
+
min(
|
|
192
|
+
(self.ttl - (time.time() - acquired))
|
|
193
|
+
for acquired in self._acquired_times.values()
|
|
194
|
+
)
|
|
195
|
+
if self._acquired_times
|
|
196
|
+
else None
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Specialized primitive implementations
|
|
202
|
+
class Mutex(CoordinationPrimitive):
|
|
203
|
+
"""Mutual exclusion lock"""
|
|
204
|
+
|
|
205
|
+
def __init__(self, name: str, ttl: float = 30.0):
|
|
206
|
+
super().__init__(name=name, type=PrimitiveType.MUTEX, ttl=ttl, max_count=1)
|
|
207
|
+
|
|
208
|
+
async def __aenter__(self) -> "Mutex":
|
|
209
|
+
"""Async context manager support"""
|
|
210
|
+
caller_id = str(uuid.uuid4())
|
|
211
|
+
self._context_caller_id = caller_id
|
|
212
|
+
await self.acquire(caller_id)
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
216
|
+
"""Async context manager support"""
|
|
217
|
+
if hasattr(self, "_context_caller_id"):
|
|
218
|
+
await self.release(self._context_caller_id)
|
|
219
|
+
delattr(self, "_context_caller_id")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Semaphore(CoordinationPrimitive):
|
|
223
|
+
"""Counting semaphore"""
|
|
224
|
+
|
|
225
|
+
def __init__(self, name: str, max_count: int = 1, ttl: float = 30.0):
|
|
226
|
+
super().__init__(
|
|
227
|
+
name=name, type=PrimitiveType.SEMAPHORE, ttl=ttl, max_count=max_count
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def available_permits(self) -> int:
|
|
232
|
+
"""Get number of available permits"""
|
|
233
|
+
return self.max_count - len(self._owners)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class Barrier(CoordinationPrimitive):
|
|
237
|
+
"""Synchronization barrier"""
|
|
238
|
+
|
|
239
|
+
def __init__(self, name: str, parties: int, timeout: Optional[float] = None):
|
|
240
|
+
super().__init__(
|
|
241
|
+
name=name,
|
|
242
|
+
type=PrimitiveType.BARRIER,
|
|
243
|
+
max_count=parties,
|
|
244
|
+
wait_timeout=timeout,
|
|
245
|
+
)
|
|
246
|
+
self._parties = parties
|
|
247
|
+
self._generation = 0
|
|
248
|
+
self._condition = asyncio.Condition(self._lock)
|
|
249
|
+
|
|
250
|
+
async def wait(self, caller_id: Optional[str] = None) -> int:
|
|
251
|
+
"""Wait at the barrier"""
|
|
252
|
+
if caller_id is None:
|
|
253
|
+
caller_id = str(uuid.uuid4())
|
|
254
|
+
|
|
255
|
+
async with self._lock:
|
|
256
|
+
generation = self._generation
|
|
257
|
+
|
|
258
|
+
self._owners.add(caller_id)
|
|
259
|
+
self._acquired_times[caller_id] = time.time()
|
|
260
|
+
|
|
261
|
+
if len(self._owners) < self._parties:
|
|
262
|
+
try:
|
|
263
|
+
await asyncio.wait_for(
|
|
264
|
+
self._condition.wait(), # Releases lock
|
|
265
|
+
timeout=self.wait_timeout,
|
|
266
|
+
)
|
|
267
|
+
return generation
|
|
268
|
+
except asyncio.TimeoutError:
|
|
269
|
+
self._owners.discard(caller_id)
|
|
270
|
+
# Notify others who might be waiting on a now-unreachable barrier
|
|
271
|
+
self._condition.notify_all()
|
|
272
|
+
raise
|
|
273
|
+
else:
|
|
274
|
+
# Last party: reset and notify
|
|
275
|
+
self._generation += 1
|
|
276
|
+
self._owners.clear()
|
|
277
|
+
self._acquired_times.clear()
|
|
278
|
+
self._condition.notify_all()
|
|
279
|
+
return generation
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class Lease(CoordinationPrimitive):
|
|
283
|
+
"""Time-based lease"""
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
name: str,
|
|
288
|
+
ttl: float = 30.0,
|
|
289
|
+
auto_renew: bool = False,
|
|
290
|
+
renew_interval: float = 10.0,
|
|
291
|
+
):
|
|
292
|
+
super().__init__(name=name, type=PrimitiveType.LEASE, ttl=ttl)
|
|
293
|
+
self.auto_renew = auto_renew
|
|
294
|
+
self.renew_interval = renew_interval
|
|
295
|
+
self._renew_task: Optional[asyncio.Task] = None
|
|
296
|
+
|
|
297
|
+
async def acquire(
|
|
298
|
+
self,
|
|
299
|
+
caller_id: str,
|
|
300
|
+
timeout: Optional[float] = None,
|
|
301
|
+
quota_amount: Optional[float] = None,
|
|
302
|
+
) -> bool:
|
|
303
|
+
"""Acquire lease with optional auto-renewal"""
|
|
304
|
+
success = await super().acquire(caller_id, timeout, quota_amount)
|
|
305
|
+
|
|
306
|
+
if success and self.auto_renew and not self._renew_task:
|
|
307
|
+
self._renew_task = asyncio.create_task(self._auto_renew_loop(caller_id))
|
|
308
|
+
|
|
309
|
+
return success
|
|
310
|
+
|
|
311
|
+
async def release(self, caller_id: str) -> bool:
|
|
312
|
+
"""Release lease and cancel auto-renewal"""
|
|
313
|
+
if self._renew_task:
|
|
314
|
+
self._renew_task.cancel()
|
|
315
|
+
await asyncio.gather(self._renew_task, return_exceptions=True)
|
|
316
|
+
self._renew_task = None
|
|
317
|
+
|
|
318
|
+
return await super().release(caller_id)
|
|
319
|
+
|
|
320
|
+
async def _auto_renew_loop(self, caller_id: str) -> None:
|
|
321
|
+
"""Auto-renew lease periodically"""
|
|
322
|
+
while True:
|
|
323
|
+
try:
|
|
324
|
+
await asyncio.sleep(self.renew_interval)
|
|
325
|
+
|
|
326
|
+
async with self._lock:
|
|
327
|
+
if caller_id in self._owners:
|
|
328
|
+
self._acquired_times[caller_id] = time.time()
|
|
329
|
+
logger.debug(
|
|
330
|
+
"lease_renewed", lease=self.name, caller_id=caller_id
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
break
|
|
334
|
+
|
|
335
|
+
except asyncio.CancelledError:
|
|
336
|
+
break
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.error("lease_renew_error", lease=self.name, error=str(e))
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class Lock(CoordinationPrimitive):
|
|
343
|
+
"""Simple reentrant lock"""
|
|
344
|
+
|
|
345
|
+
def __init__(self, name: str, ttl: float = 30.0):
|
|
346
|
+
super().__init__(name=name, type=PrimitiveType.LOCK, ttl=ttl)
|
|
347
|
+
self._lock_count: dict[str, int] = {}
|
|
348
|
+
|
|
349
|
+
async def acquire(
|
|
350
|
+
self,
|
|
351
|
+
caller_id: str,
|
|
352
|
+
timeout: Optional[float] = None,
|
|
353
|
+
quota_amount: Optional[float] = None,
|
|
354
|
+
) -> bool:
|
|
355
|
+
"""Acquire lock with reentrancy support"""
|
|
356
|
+
async with self._lock:
|
|
357
|
+
# Check if already owned by caller (reentrant)
|
|
358
|
+
if caller_id in self._owners:
|
|
359
|
+
self._lock_count[caller_id] = self._lock_count.get(caller_id, 1) + 1
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
# Try to acquire
|
|
363
|
+
if not self._owners:
|
|
364
|
+
self._acquire_for(caller_id)
|
|
365
|
+
self._lock_count[caller_id] = 1
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
async def release(self, caller_id: str) -> bool:
|
|
371
|
+
"""Release lock with reentrancy support"""
|
|
372
|
+
async with self._lock:
|
|
373
|
+
if caller_id not in self._owners:
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
# Decrement lock count
|
|
377
|
+
count = self._lock_count.get(caller_id, 1) - 1
|
|
378
|
+
|
|
379
|
+
if count <= 0:
|
|
380
|
+
# Fully release
|
|
381
|
+
self._remove_owner(caller_id)
|
|
382
|
+
self._lock_count.pop(caller_id, None)
|
|
383
|
+
return True
|
|
384
|
+
else:
|
|
385
|
+
# Still locked
|
|
386
|
+
self._lock_count[caller_id] = count
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Quota(CoordinationPrimitive):
|
|
391
|
+
"""Resource quota management"""
|
|
392
|
+
|
|
393
|
+
def __init__(
|
|
394
|
+
self, name: str, limit: float, reset_interval: Optional[timedelta] = None
|
|
395
|
+
):
|
|
396
|
+
super().__init__(name=name, type=PrimitiveType.QUOTA, quota_limit=limit)
|
|
397
|
+
self.reset_interval = reset_interval
|
|
398
|
+
self._last_reset = time.time()
|
|
399
|
+
self._reset_task: Optional[asyncio.Task] = None
|
|
400
|
+
|
|
401
|
+
def _start_reset_task_if_needed(self) -> None:
|
|
402
|
+
"""Start the background reset task if configured and not already running."""
|
|
403
|
+
if self.reset_interval and self._reset_task is None:
|
|
404
|
+
self._reset_task = asyncio.create_task(self._reset_loop())
|
|
405
|
+
|
|
406
|
+
async def consume(self, caller_id: str, amount: float) -> bool:
|
|
407
|
+
"""Consume quota amount"""
|
|
408
|
+
self._start_reset_task_if_needed()
|
|
409
|
+
return await self.acquire(caller_id, quota_amount=amount)
|
|
410
|
+
|
|
411
|
+
async def release_quota(self, caller_id: str, amount: float) -> None:
|
|
412
|
+
"""Release quota (give back)"""
|
|
413
|
+
self._start_reset_task_if_needed()
|
|
414
|
+
async with self._lock:
|
|
415
|
+
if caller_id in self._quota_usage and amount > 0:
|
|
416
|
+
self._quota_usage[caller_id] = max(
|
|
417
|
+
0, self._quota_usage[caller_id] - amount
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def available(self) -> float:
|
|
422
|
+
"""Get available quota"""
|
|
423
|
+
used = sum(self._quota_usage.values())
|
|
424
|
+
return max(0, (self.quota_limit or 0) - used)
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def usage(self) -> dict[str, float]:
|
|
428
|
+
"""Get current usage by caller"""
|
|
429
|
+
return dict(self._quota_usage)
|
|
430
|
+
|
|
431
|
+
async def reset(self) -> None:
|
|
432
|
+
"""Reset all quota usage"""
|
|
433
|
+
self._start_reset_task_if_needed()
|
|
434
|
+
async with self._lock:
|
|
435
|
+
self._quota_usage.clear()
|
|
436
|
+
self._last_reset = time.time()
|
|
437
|
+
logger.info("quota_reset", quota=self.name)
|
|
438
|
+
|
|
439
|
+
async def _reset_loop(self) -> None:
|
|
440
|
+
"""Periodic quota reset"""
|
|
441
|
+
while True:
|
|
442
|
+
try:
|
|
443
|
+
if self.reset_interval:
|
|
444
|
+
await asyncio.sleep(self.reset_interval.total_seconds())
|
|
445
|
+
await self.reset()
|
|
446
|
+
else:
|
|
447
|
+
break
|
|
448
|
+
except asyncio.CancelledError:
|
|
449
|
+
break
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error("quota_reset_error", quota=self.name, error=str(e))
|
|
452
|
+
|
|
453
|
+
def __del__(self) -> None:
|
|
454
|
+
"""Cleanup reset task"""
|
|
455
|
+
if self._reset_task and not self._reset_task.done():
|
|
456
|
+
self._reset_task.cancel()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# Factory function
|
|
460
|
+
def create_primitive(
|
|
461
|
+
primitive_type: PrimitiveType, name: str, **kwargs: Any
|
|
462
|
+
) -> CoordinationPrimitive:
|
|
463
|
+
"""Create a coordination primitive by type"""
|
|
464
|
+
primitives = {
|
|
465
|
+
PrimitiveType.MUTEX: Mutex,
|
|
466
|
+
PrimitiveType.SEMAPHORE: Semaphore,
|
|
467
|
+
PrimitiveType.BARRIER: Barrier,
|
|
468
|
+
PrimitiveType.LEASE: Lease,
|
|
469
|
+
PrimitiveType.LOCK: Lock,
|
|
470
|
+
PrimitiveType.QUOTA: Quota,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
primitive_class = primitives.get(primitive_type, CoordinationPrimitive)
|
|
474
|
+
|
|
475
|
+
if primitive_class == CoordinationPrimitive:
|
|
476
|
+
return CoordinationPrimitive(name=name, type=primitive_type, **kwargs)
|
|
477
|
+
else:
|
|
478
|
+
return primitive_class(name=name, **kwargs) # type: ignore[no-any-return]
|