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,790 @@
|
|
|
1
|
+
"""Resource allocation strategies for PuffinFlow resource management.
|
|
2
|
+
|
|
3
|
+
This module provides various allocation strategies for distributing computational
|
|
4
|
+
resources across agent states, including first-fit, best-fit, priority-based,
|
|
5
|
+
and fair-share allocation algorithms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import heapq
|
|
9
|
+
import time
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import structlog
|
|
18
|
+
|
|
19
|
+
from .pool import ResourcePool
|
|
20
|
+
|
|
21
|
+
# Import resource management components from the canonical source
|
|
22
|
+
from .requirements import (
|
|
23
|
+
ResourceRequirements,
|
|
24
|
+
ResourceType, # Use the canonical mapping from requirements.py
|
|
25
|
+
get_resource_amount,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = structlog.get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AllocationStrategy(Enum):
|
|
32
|
+
"""Enumeration of available resource allocation strategies.
|
|
33
|
+
|
|
34
|
+
Each strategy implements a different approach to distributing limited
|
|
35
|
+
computational resources among competing agent states.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
FIRST_FIT = "first_fit" # Allocate to first available slot
|
|
39
|
+
BEST_FIT = "best_fit" # Minimize resource waste
|
|
40
|
+
WORST_FIT = "worst_fit" # Maximize remaining free space
|
|
41
|
+
PRIORITY = "priority" # Allocate based on state priority
|
|
42
|
+
FAIR_SHARE = "fair_share" # Ensure equitable resource distribution
|
|
43
|
+
ROUND_ROBIN = "round_robin" # Rotate allocations cyclically
|
|
44
|
+
WEIGHTED = "weighted" # Weight allocations by importance
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class AllocationRequest:
|
|
49
|
+
"""Represents a request for computational resource allocation.
|
|
50
|
+
|
|
51
|
+
Encapsulates all information needed to process a resource allocation
|
|
52
|
+
request, including resource requirements, priority, and metadata.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
request_id: str # Unique identifier for this request
|
|
56
|
+
requester_id: str # ID of the requesting agent/state
|
|
57
|
+
requirements: ResourceRequirements # Detailed resource requirements
|
|
58
|
+
priority: int = 0 # Request priority (higher = more important)
|
|
59
|
+
weight: float = 1.0 # Relative importance weight
|
|
60
|
+
metadata: dict[str, Any] = field(
|
|
61
|
+
default_factory=dict
|
|
62
|
+
) # Additional request metadata
|
|
63
|
+
timestamp: datetime = field(
|
|
64
|
+
default_factory=datetime.utcnow
|
|
65
|
+
) # When request was created
|
|
66
|
+
deadline: Optional[datetime] = None # Optional deadline for allocation
|
|
67
|
+
|
|
68
|
+
def __lt__(self, other: "AllocationRequest") -> bool:
|
|
69
|
+
"""Define ordering for priority queue operations.
|
|
70
|
+
|
|
71
|
+
Higher priority values are considered "less than" for max-heap behavior.
|
|
72
|
+
"""
|
|
73
|
+
return self.priority > other.priority
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class AllocationResult:
|
|
78
|
+
"""Contains the outcome of a resource allocation attempt.
|
|
79
|
+
|
|
80
|
+
Provides detailed information about whether allocation succeeded,
|
|
81
|
+
what resources were allocated, and performance metrics.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
request_id: str # ID of the original request
|
|
85
|
+
success: bool # Whether allocation succeeded
|
|
86
|
+
allocated: dict[ResourceType, float] = field(
|
|
87
|
+
default_factory=dict
|
|
88
|
+
) # Resources actually allocated
|
|
89
|
+
reason: Optional[str] = None # Reason for failure (if applicable)
|
|
90
|
+
timestamp: datetime = field(
|
|
91
|
+
default_factory=datetime.utcnow
|
|
92
|
+
) # When allocation was processed
|
|
93
|
+
allocation_time: Optional[float] = None # Time taken to process allocation
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict[str, Any]:
|
|
96
|
+
"""Convert result to dictionary format for serialization."""
|
|
97
|
+
return {
|
|
98
|
+
"request_id": self.request_id,
|
|
99
|
+
"success": self.success,
|
|
100
|
+
"allocated": {rt.name: amount for rt, amount in self.allocated.items()},
|
|
101
|
+
"reason": self.reason,
|
|
102
|
+
"timestamp": self.timestamp.isoformat(),
|
|
103
|
+
"allocation_time": self.allocation_time,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AllocationMetrics:
|
|
108
|
+
"""Tracks and aggregates allocation performance metrics.
|
|
109
|
+
|
|
110
|
+
Maintains statistics about allocation success rates, timing,
|
|
111
|
+
resource utilization, and queue behavior.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(self) -> None:
|
|
115
|
+
"""Initialize metrics tracking."""
|
|
116
|
+
self.total_requests = 0 # Total number of allocation requests
|
|
117
|
+
self.successful_allocations = 0 # Number of successful allocations
|
|
118
|
+
self.failed_allocations = 0 # Number of failed allocations
|
|
119
|
+
self.total_allocation_time = 0.0 # Cumulative time spent on allocations
|
|
120
|
+
self.resource_utilization: dict[ResourceType, float] = defaultdict(
|
|
121
|
+
float
|
|
122
|
+
) # Resource usage by type
|
|
123
|
+
self.queue_lengths: list[int] = [] # Historical queue length snapshots
|
|
124
|
+
self.wait_times: list[float] = [] # Request wait times
|
|
125
|
+
|
|
126
|
+
def record_allocation(
|
|
127
|
+
self, result: AllocationResult, wait_time: float = 0.0
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Record metrics for a completed allocation attempt.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
result: The allocation result to record
|
|
133
|
+
wait_time: How long the request waited in queue
|
|
134
|
+
"""
|
|
135
|
+
self.total_requests += 1
|
|
136
|
+
|
|
137
|
+
if result.success:
|
|
138
|
+
self.successful_allocations += 1
|
|
139
|
+
# Track resource utilization for successful allocations
|
|
140
|
+
for rt, amount in result.allocated.items():
|
|
141
|
+
self.resource_utilization[rt] += amount
|
|
142
|
+
else:
|
|
143
|
+
self.failed_allocations += 1
|
|
144
|
+
|
|
145
|
+
# Record timing metrics
|
|
146
|
+
if result.allocation_time:
|
|
147
|
+
self.total_allocation_time += result.allocation_time
|
|
148
|
+
|
|
149
|
+
if wait_time > 0:
|
|
150
|
+
self.wait_times.append(wait_time)
|
|
151
|
+
|
|
152
|
+
def get_stats(self) -> dict[str, Any]:
|
|
153
|
+
"""Calculate and return comprehensive allocation statistics."""
|
|
154
|
+
success_rate = (
|
|
155
|
+
self.successful_allocations / self.total_requests
|
|
156
|
+
if self.total_requests > 0
|
|
157
|
+
else 0
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
avg_allocation_time = (
|
|
161
|
+
self.total_allocation_time / self.successful_allocations
|
|
162
|
+
if self.successful_allocations > 0
|
|
163
|
+
else 0
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
avg_wait_time = (
|
|
167
|
+
sum(self.wait_times) / len(self.wait_times) if self.wait_times else 0
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"total_requests": self.total_requests,
|
|
172
|
+
"successful_allocations": self.successful_allocations,
|
|
173
|
+
"failed_allocations": self.failed_allocations,
|
|
174
|
+
"success_rate": success_rate,
|
|
175
|
+
"avg_allocation_time": avg_allocation_time,
|
|
176
|
+
"avg_wait_time": avg_wait_time,
|
|
177
|
+
"resource_utilization": dict(self.resource_utilization),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ResourceAllocator(ABC):
|
|
182
|
+
"""Abstract base class for all resource allocation strategies.
|
|
183
|
+
|
|
184
|
+
Defines the common interface and shared functionality for different
|
|
185
|
+
allocation algorithms. Subclasses implement specific allocation logic.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, resource_pool: ResourcePool):
|
|
189
|
+
"""Initialize allocator with a resource pool.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
resource_pool: The pool of available computational resources
|
|
193
|
+
"""
|
|
194
|
+
self.resource_pool = resource_pool
|
|
195
|
+
self.metrics = AllocationMetrics()
|
|
196
|
+
self._pending_requests: list[AllocationRequest] = []
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
200
|
+
"""Allocate resources for a single request.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
request: The resource allocation request
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Result indicating success/failure and allocated resources
|
|
207
|
+
"""
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
@abstractmethod
|
|
211
|
+
def get_allocation_order(
|
|
212
|
+
self, requests: list[AllocationRequest]
|
|
213
|
+
) -> list[AllocationRequest]:
|
|
214
|
+
"""Determine the order for processing multiple allocation requests.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
requests: List of pending allocation requests
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Requests ordered according to the allocation strategy
|
|
221
|
+
"""
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
async def allocate_batch(
|
|
225
|
+
self, requests: list[AllocationRequest]
|
|
226
|
+
) -> list[AllocationResult]:
|
|
227
|
+
"""Allocate resources for multiple requests in optimal order.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
requests: List of allocation requests to process
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List of allocation results in processing order
|
|
234
|
+
"""
|
|
235
|
+
ordered_requests = self.get_allocation_order(requests)
|
|
236
|
+
results = []
|
|
237
|
+
|
|
238
|
+
for request in ordered_requests:
|
|
239
|
+
result = await self.allocate(request)
|
|
240
|
+
results.append(result)
|
|
241
|
+
self.metrics.record_allocation(result)
|
|
242
|
+
|
|
243
|
+
return results
|
|
244
|
+
|
|
245
|
+
def can_allocate(self, requirements: ResourceRequirements) -> bool:
|
|
246
|
+
"""Check if the given resource requirements can be satisfied.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
requirements: Resource requirements to check
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if resources are available, False otherwise
|
|
253
|
+
"""
|
|
254
|
+
# Check each resource type that is requested
|
|
255
|
+
for resource_type in [
|
|
256
|
+
ResourceType.CPU,
|
|
257
|
+
ResourceType.MEMORY,
|
|
258
|
+
ResourceType.IO,
|
|
259
|
+
ResourceType.NETWORK,
|
|
260
|
+
ResourceType.GPU,
|
|
261
|
+
]:
|
|
262
|
+
# Use bitwise AND to check if this resource type is requested
|
|
263
|
+
if requirements.resource_types & resource_type:
|
|
264
|
+
required = get_resource_amount(requirements, resource_type)
|
|
265
|
+
available = self.resource_pool.available.get(resource_type, 0.0)
|
|
266
|
+
|
|
267
|
+
if required > available:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class FirstFitAllocator(ResourceAllocator):
|
|
274
|
+
"""First-fit allocation strategy implementation.
|
|
275
|
+
|
|
276
|
+
Allocates resources to the first request that can be satisfied,
|
|
277
|
+
without considering optimization. Simple and fast but may lead
|
|
278
|
+
to resource fragmentation.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
282
|
+
"""Allocate resources using first-fit strategy.
|
|
283
|
+
|
|
284
|
+
Attempts to immediately satisfy the request with available resources.
|
|
285
|
+
"""
|
|
286
|
+
start_time = time.time()
|
|
287
|
+
|
|
288
|
+
# Attempt non-blocking resource acquisition
|
|
289
|
+
success = await self.resource_pool.acquire(
|
|
290
|
+
request.request_id,
|
|
291
|
+
request.requirements,
|
|
292
|
+
timeout=0, # Non-blocking
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if success:
|
|
296
|
+
# Build dictionary of actually allocated resources
|
|
297
|
+
allocated = {}
|
|
298
|
+
for resource_type in [
|
|
299
|
+
ResourceType.CPU,
|
|
300
|
+
ResourceType.MEMORY,
|
|
301
|
+
ResourceType.IO,
|
|
302
|
+
ResourceType.NETWORK,
|
|
303
|
+
ResourceType.GPU,
|
|
304
|
+
]:
|
|
305
|
+
# Check if this resource type was requested using bitwise AND
|
|
306
|
+
if request.requirements.resource_types & resource_type:
|
|
307
|
+
amount = get_resource_amount(request.requirements, resource_type)
|
|
308
|
+
if amount > 0:
|
|
309
|
+
allocated[resource_type] = amount
|
|
310
|
+
|
|
311
|
+
return AllocationResult(
|
|
312
|
+
request_id=request.request_id,
|
|
313
|
+
success=True,
|
|
314
|
+
allocated=allocated,
|
|
315
|
+
allocation_time=time.time() - start_time,
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
return AllocationResult(
|
|
319
|
+
request_id=request.request_id,
|
|
320
|
+
success=False,
|
|
321
|
+
reason="Insufficient resources",
|
|
322
|
+
allocation_time=time.time() - start_time,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def get_allocation_order(
|
|
326
|
+
self, requests: list[AllocationRequest]
|
|
327
|
+
) -> list[AllocationRequest]:
|
|
328
|
+
"""Order requests by arrival time (FIFO - First In, First Out)."""
|
|
329
|
+
return sorted(requests, key=lambda r: r.timestamp)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class BestFitAllocator(ResourceAllocator):
|
|
333
|
+
"""Best-fit allocation strategy implementation.
|
|
334
|
+
|
|
335
|
+
Chooses allocations that minimize resource waste by finding the
|
|
336
|
+
allocation that leaves the smallest amount of unused resources.
|
|
337
|
+
More complex than first-fit but can improve resource utilization.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
341
|
+
"""Allocate resources using best-fit strategy."""
|
|
342
|
+
start_time = time.time()
|
|
343
|
+
|
|
344
|
+
# Calculate potential resource waste for this allocation
|
|
345
|
+
waste = self._calculate_waste(request.requirements)
|
|
346
|
+
|
|
347
|
+
# Attempt resource acquisition
|
|
348
|
+
success = await self.resource_pool.acquire(
|
|
349
|
+
request.request_id, request.requirements, timeout=0
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if success:
|
|
353
|
+
# Build dictionary of allocated resources
|
|
354
|
+
allocated = {}
|
|
355
|
+
for resource_type in [
|
|
356
|
+
ResourceType.CPU,
|
|
357
|
+
ResourceType.MEMORY,
|
|
358
|
+
ResourceType.IO,
|
|
359
|
+
ResourceType.NETWORK,
|
|
360
|
+
ResourceType.GPU,
|
|
361
|
+
]:
|
|
362
|
+
# Check if this resource type was requested
|
|
363
|
+
if request.requirements.resource_types & resource_type:
|
|
364
|
+
amount = get_resource_amount(request.requirements, resource_type)
|
|
365
|
+
if amount > 0:
|
|
366
|
+
allocated[resource_type] = amount
|
|
367
|
+
|
|
368
|
+
logger.debug(
|
|
369
|
+
"best_fit_allocation", request_id=request.request_id, waste=waste
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return AllocationResult(
|
|
373
|
+
request_id=request.request_id,
|
|
374
|
+
success=True,
|
|
375
|
+
allocated=allocated,
|
|
376
|
+
allocation_time=time.time() - start_time,
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
return AllocationResult(
|
|
380
|
+
request_id=request.request_id,
|
|
381
|
+
success=False,
|
|
382
|
+
reason="Insufficient resources",
|
|
383
|
+
allocation_time=time.time() - start_time,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _calculate_waste(self, requirements: ResourceRequirements) -> float:
|
|
387
|
+
"""Calculate the amount of resource waste this allocation would cause.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
requirements: Resource requirements to evaluate
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Total amount of wasted resources (lower is better)
|
|
394
|
+
"""
|
|
395
|
+
total_waste = 0.0
|
|
396
|
+
|
|
397
|
+
for resource_type in [
|
|
398
|
+
ResourceType.CPU,
|
|
399
|
+
ResourceType.MEMORY,
|
|
400
|
+
ResourceType.IO,
|
|
401
|
+
ResourceType.NETWORK,
|
|
402
|
+
ResourceType.GPU,
|
|
403
|
+
]:
|
|
404
|
+
# Only calculate waste for requested resource types
|
|
405
|
+
if requirements.resource_types & resource_type:
|
|
406
|
+
required = get_resource_amount(requirements, resource_type)
|
|
407
|
+
available = self.resource_pool.available.get(resource_type, 0.0)
|
|
408
|
+
|
|
409
|
+
if available >= required:
|
|
410
|
+
# Waste is the unused portion after allocation
|
|
411
|
+
waste = available - required
|
|
412
|
+
total_waste += waste
|
|
413
|
+
|
|
414
|
+
return total_waste
|
|
415
|
+
|
|
416
|
+
def get_allocation_order(
|
|
417
|
+
self, requests: list[AllocationRequest]
|
|
418
|
+
) -> list[AllocationRequest]:
|
|
419
|
+
"""Order requests by waste (ascending) - least waste first."""
|
|
420
|
+
# Calculate waste for each request and sort by it
|
|
421
|
+
requests_with_waste = [
|
|
422
|
+
(self._calculate_waste(req.requirements), req) for req in requests
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
# Sort by waste amount (ascending - least waste first)
|
|
426
|
+
requests_with_waste.sort(key=lambda x: x[0])
|
|
427
|
+
|
|
428
|
+
return [req for _, req in requests_with_waste]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class WorstFitAllocator(ResourceAllocator):
|
|
432
|
+
"""Worst-fit allocation strategy implementation.
|
|
433
|
+
|
|
434
|
+
Chooses allocations that maximize remaining free space, which can
|
|
435
|
+
help accommodate future large requests but may lead to more fragmentation.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
439
|
+
"""Allocate resources using worst-fit strategy.
|
|
440
|
+
|
|
441
|
+
Uses the same allocation mechanism as first-fit but with different ordering.
|
|
442
|
+
"""
|
|
443
|
+
# Delegate to first-fit allocator for actual allocation
|
|
444
|
+
first_fit = FirstFitAllocator(self.resource_pool)
|
|
445
|
+
return await first_fit.allocate(request)
|
|
446
|
+
|
|
447
|
+
def get_allocation_order(
|
|
448
|
+
self, requests: list[AllocationRequest]
|
|
449
|
+
) -> list[AllocationRequest]:
|
|
450
|
+
"""Order requests by remaining space (descending) - most remaining space first."""
|
|
451
|
+
# Calculate remaining space for each request and sort by it
|
|
452
|
+
requests_with_remaining = [
|
|
453
|
+
(self._calculate_remaining(req.requirements), req) for req in requests
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
# Sort by remaining space (descending - most remaining first)
|
|
457
|
+
requests_with_remaining.sort(key=lambda x: x[0], reverse=True)
|
|
458
|
+
|
|
459
|
+
return [req for _, req in requests_with_remaining]
|
|
460
|
+
|
|
461
|
+
def _calculate_remaining(self, requirements: ResourceRequirements) -> float:
|
|
462
|
+
"""Calculate remaining resources after this allocation.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
requirements: Resource requirements to evaluate
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Total amount of resources that would remain free
|
|
469
|
+
"""
|
|
470
|
+
total_remaining = 0.0
|
|
471
|
+
|
|
472
|
+
for resource_type in [
|
|
473
|
+
ResourceType.CPU,
|
|
474
|
+
ResourceType.MEMORY,
|
|
475
|
+
ResourceType.IO,
|
|
476
|
+
ResourceType.NETWORK,
|
|
477
|
+
ResourceType.GPU,
|
|
478
|
+
]:
|
|
479
|
+
# Only calculate for requested resource types
|
|
480
|
+
if requirements.resource_types & resource_type:
|
|
481
|
+
required = get_resource_amount(requirements, resource_type)
|
|
482
|
+
available = self.resource_pool.available.get(resource_type, 0.0)
|
|
483
|
+
|
|
484
|
+
if available >= required:
|
|
485
|
+
remaining = available - required
|
|
486
|
+
total_remaining += remaining
|
|
487
|
+
|
|
488
|
+
return total_remaining
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class PriorityAllocator(ResourceAllocator):
|
|
492
|
+
"""Priority-based allocation strategy implementation.
|
|
493
|
+
|
|
494
|
+
Maintains a priority queue and always processes the highest-priority
|
|
495
|
+
requests first. Essential for systems with critical vs. non-critical workloads.
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
def __init__(self, resource_pool: ResourcePool):
|
|
499
|
+
"""Initialize priority allocator with a priority queue."""
|
|
500
|
+
super().__init__(resource_pool)
|
|
501
|
+
self._priority_queue: list[AllocationRequest] = []
|
|
502
|
+
|
|
503
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
504
|
+
"""Allocate resources based on priority using a priority queue."""
|
|
505
|
+
start_time = time.time()
|
|
506
|
+
|
|
507
|
+
# Add request to priority queue (heapq maintains min-heap, so we negate priority)
|
|
508
|
+
heapq.heappush(self._priority_queue, request)
|
|
509
|
+
|
|
510
|
+
# Process queue starting with highest priority requests
|
|
511
|
+
processed = []
|
|
512
|
+
while self._priority_queue:
|
|
513
|
+
next_request = heapq.heappop(self._priority_queue)
|
|
514
|
+
|
|
515
|
+
# Check if we can allocate to this request
|
|
516
|
+
if self.can_allocate(next_request.requirements):
|
|
517
|
+
success = await self.resource_pool.acquire(
|
|
518
|
+
next_request.request_id, next_request.requirements, timeout=0
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if success:
|
|
522
|
+
processed.append(next_request)
|
|
523
|
+
|
|
524
|
+
# If this was our original request, return success
|
|
525
|
+
if next_request.request_id == request.request_id:
|
|
526
|
+
# Build allocated resources dictionary
|
|
527
|
+
allocated = {}
|
|
528
|
+
for resource_type in [
|
|
529
|
+
ResourceType.CPU,
|
|
530
|
+
ResourceType.MEMORY,
|
|
531
|
+
ResourceType.IO,
|
|
532
|
+
ResourceType.NETWORK,
|
|
533
|
+
ResourceType.GPU,
|
|
534
|
+
]:
|
|
535
|
+
if request.requirements.resource_types & resource_type:
|
|
536
|
+
amount = get_resource_amount(
|
|
537
|
+
request.requirements, resource_type
|
|
538
|
+
)
|
|
539
|
+
if amount > 0:
|
|
540
|
+
allocated[resource_type] = amount
|
|
541
|
+
|
|
542
|
+
return AllocationResult(
|
|
543
|
+
request_id=request.request_id,
|
|
544
|
+
success=True,
|
|
545
|
+
allocated=allocated,
|
|
546
|
+
allocation_time=time.time() - start_time,
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
# Couldn't acquire resources, put back in queue
|
|
550
|
+
heapq.heappush(self._priority_queue, next_request)
|
|
551
|
+
break
|
|
552
|
+
else:
|
|
553
|
+
# Can't allocate to this request, put it back and stop
|
|
554
|
+
heapq.heappush(self._priority_queue, next_request)
|
|
555
|
+
break
|
|
556
|
+
|
|
557
|
+
# Request couldn't be processed immediately
|
|
558
|
+
return AllocationResult(
|
|
559
|
+
request_id=request.request_id,
|
|
560
|
+
success=False,
|
|
561
|
+
reason="Queued for resources",
|
|
562
|
+
allocation_time=time.time() - start_time,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
def get_allocation_order(
|
|
566
|
+
self, requests: list[AllocationRequest]
|
|
567
|
+
) -> list[AllocationRequest]:
|
|
568
|
+
"""Order requests by priority (highest first)."""
|
|
569
|
+
return sorted(requests, key=lambda r: r.priority, reverse=True)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class FairShareAllocator(ResourceAllocator):
|
|
573
|
+
"""Fair-share allocation strategy implementation.
|
|
574
|
+
|
|
575
|
+
Ensures equitable resource distribution among different requesters
|
|
576
|
+
by tracking usage history and enforcing fair share limits.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
def __init__(self, resource_pool: ResourcePool):
|
|
580
|
+
"""Initialize fair-share allocator with usage tracking."""
|
|
581
|
+
super().__init__(resource_pool)
|
|
582
|
+
self._usage_history: dict[str, float] = defaultdict(
|
|
583
|
+
float
|
|
584
|
+
) # Total resources used by each requester
|
|
585
|
+
self._allocation_counts: dict[str, int] = defaultdict(
|
|
586
|
+
int
|
|
587
|
+
) # Number of allocations per requester
|
|
588
|
+
|
|
589
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
590
|
+
"""Allocate resources with fair-share constraints."""
|
|
591
|
+
start_time = time.time()
|
|
592
|
+
|
|
593
|
+
# Calculate fair share limit for this requester
|
|
594
|
+
fair_share = self._calculate_fair_share(request.requester_id)
|
|
595
|
+
|
|
596
|
+
# Check if request would exceed fair share
|
|
597
|
+
current_usage = self._usage_history[request.requester_id]
|
|
598
|
+
requested_total = self._calculate_resource_total(request.requirements)
|
|
599
|
+
|
|
600
|
+
if current_usage + requested_total > fair_share:
|
|
601
|
+
return AllocationResult(
|
|
602
|
+
request_id=request.request_id,
|
|
603
|
+
success=False,
|
|
604
|
+
reason=f"Exceeds fair share (current: {current_usage}, "
|
|
605
|
+
f"limit: {fair_share})",
|
|
606
|
+
allocation_time=time.time() - start_time,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Attempt allocation within fair share limits
|
|
610
|
+
success = await self.resource_pool.acquire(
|
|
611
|
+
request.request_id, request.requirements, timeout=0
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if success:
|
|
615
|
+
# Update usage tracking
|
|
616
|
+
self._usage_history[request.requester_id] += requested_total
|
|
617
|
+
self._allocation_counts[request.requester_id] += 1
|
|
618
|
+
|
|
619
|
+
# Build allocated resources dictionary
|
|
620
|
+
allocated = {}
|
|
621
|
+
for resource_type in [
|
|
622
|
+
ResourceType.CPU,
|
|
623
|
+
ResourceType.MEMORY,
|
|
624
|
+
ResourceType.IO,
|
|
625
|
+
ResourceType.NETWORK,
|
|
626
|
+
ResourceType.GPU,
|
|
627
|
+
]:
|
|
628
|
+
if request.requirements.resource_types & resource_type:
|
|
629
|
+
amount = get_resource_amount(request.requirements, resource_type)
|
|
630
|
+
if amount > 0:
|
|
631
|
+
allocated[resource_type] = amount
|
|
632
|
+
|
|
633
|
+
return AllocationResult(
|
|
634
|
+
request_id=request.request_id,
|
|
635
|
+
success=True,
|
|
636
|
+
allocated=allocated,
|
|
637
|
+
allocation_time=time.time() - start_time,
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
return AllocationResult(
|
|
641
|
+
request_id=request.request_id,
|
|
642
|
+
success=False,
|
|
643
|
+
reason="Insufficient resources",
|
|
644
|
+
allocation_time=time.time() - start_time,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
def _calculate_fair_share(self, requester_id: str) -> float:
|
|
648
|
+
"""Calculate fair share limit for a specific requester.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
requester_id: ID of the requester
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Fair share resource limit
|
|
655
|
+
"""
|
|
656
|
+
# Simple fair share: divide total resources equally among all requesters
|
|
657
|
+
total_requesters = len(self._usage_history) or 1
|
|
658
|
+
total_resources = sum(self.resource_pool.resources.values())
|
|
659
|
+
|
|
660
|
+
return total_resources / total_requesters
|
|
661
|
+
|
|
662
|
+
def _calculate_resource_total(self, requirements: ResourceRequirements) -> float:
|
|
663
|
+
"""Calculate total resource units requested across all resource types.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
requirements: Resource requirements to sum
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Total resource units requested
|
|
670
|
+
"""
|
|
671
|
+
total = 0.0
|
|
672
|
+
|
|
673
|
+
for resource_type in [
|
|
674
|
+
ResourceType.CPU,
|
|
675
|
+
ResourceType.MEMORY,
|
|
676
|
+
ResourceType.IO,
|
|
677
|
+
ResourceType.NETWORK,
|
|
678
|
+
ResourceType.GPU,
|
|
679
|
+
]:
|
|
680
|
+
if requirements.resource_types & resource_type:
|
|
681
|
+
total += get_resource_amount(requirements, resource_type)
|
|
682
|
+
|
|
683
|
+
return total
|
|
684
|
+
|
|
685
|
+
def get_allocation_order(
|
|
686
|
+
self, requests: list[AllocationRequest]
|
|
687
|
+
) -> list[AllocationRequest]:
|
|
688
|
+
"""Order requests by usage history (least used requesters first)."""
|
|
689
|
+
return sorted(requests, key=lambda r: self._usage_history[r.requester_id])
|
|
690
|
+
|
|
691
|
+
def reset_usage_history(self) -> None:
|
|
692
|
+
"""Reset usage history for a new allocation period."""
|
|
693
|
+
self._usage_history.clear()
|
|
694
|
+
self._allocation_counts.clear()
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class WeightedAllocator(ResourceAllocator):
|
|
698
|
+
"""Weighted allocation strategy implementation.
|
|
699
|
+
|
|
700
|
+
Combines priority and weight factors to determine allocation order,
|
|
701
|
+
allowing for fine-grained control over resource distribution.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
def __init__(self, resource_pool: ResourcePool):
|
|
705
|
+
"""Initialize weighted allocator."""
|
|
706
|
+
super().__init__(resource_pool)
|
|
707
|
+
self._weights: dict[str, float] = {} # Requester-specific weights
|
|
708
|
+
|
|
709
|
+
def set_weight(self, requester_id: str, weight: float) -> None:
|
|
710
|
+
"""Set allocation weight for a specific requester.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
requester_id: ID of the requester
|
|
714
|
+
weight: Weight factor (higher = more important)
|
|
715
|
+
"""
|
|
716
|
+
self._weights[requester_id] = weight
|
|
717
|
+
|
|
718
|
+
async def allocate(self, request: AllocationRequest) -> AllocationResult:
|
|
719
|
+
"""Allocate resources based on weighted priority."""
|
|
720
|
+
time.time()
|
|
721
|
+
|
|
722
|
+
# Get weight for this requester (use request weight as fallback)
|
|
723
|
+
weight = self._weights.get(request.requester_id, request.weight)
|
|
724
|
+
|
|
725
|
+
# Calculate weighted priority
|
|
726
|
+
weighted_priority = request.priority * weight
|
|
727
|
+
|
|
728
|
+
# Create modified request with weighted priority
|
|
729
|
+
weighted_request = AllocationRequest(
|
|
730
|
+
request_id=request.request_id,
|
|
731
|
+
requester_id=request.requester_id,
|
|
732
|
+
requirements=request.requirements,
|
|
733
|
+
priority=int(weighted_priority),
|
|
734
|
+
weight=weight,
|
|
735
|
+
metadata=request.metadata,
|
|
736
|
+
timestamp=request.timestamp,
|
|
737
|
+
deadline=request.deadline,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Use priority allocator with the weighted priority
|
|
741
|
+
priority_allocator = PriorityAllocator(self.resource_pool)
|
|
742
|
+
return await priority_allocator.allocate(weighted_request)
|
|
743
|
+
|
|
744
|
+
def get_allocation_order(
|
|
745
|
+
self, requests: list[AllocationRequest]
|
|
746
|
+
) -> list[AllocationRequest]:
|
|
747
|
+
"""Order requests by weighted priority (highest weighted priority first)."""
|
|
748
|
+
weighted_requests = []
|
|
749
|
+
|
|
750
|
+
for req in requests:
|
|
751
|
+
weight = self._weights.get(req.requester_id, req.weight)
|
|
752
|
+
weighted_priority = req.priority * weight
|
|
753
|
+
weighted_requests.append((weighted_priority, req))
|
|
754
|
+
|
|
755
|
+
# Sort by weighted priority (descending - highest first)
|
|
756
|
+
weighted_requests.sort(key=lambda x: x[0], reverse=True)
|
|
757
|
+
|
|
758
|
+
return [req for _, req in weighted_requests]
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def create_allocator(
|
|
762
|
+
strategy: AllocationStrategy, resource_pool: ResourcePool
|
|
763
|
+
) -> ResourceAllocator:
|
|
764
|
+
"""Factory function for creating resource allocators.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
strategy: The allocation strategy to use
|
|
768
|
+
resource_pool: The pool of available resources
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
Configured allocator instance
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
ValueError: If strategy is not recognized
|
|
775
|
+
"""
|
|
776
|
+
allocators = {
|
|
777
|
+
AllocationStrategy.FIRST_FIT: FirstFitAllocator,
|
|
778
|
+
AllocationStrategy.BEST_FIT: BestFitAllocator,
|
|
779
|
+
AllocationStrategy.WORST_FIT: WorstFitAllocator,
|
|
780
|
+
AllocationStrategy.PRIORITY: PriorityAllocator,
|
|
781
|
+
AllocationStrategy.FAIR_SHARE: FairShareAllocator,
|
|
782
|
+
AllocationStrategy.WEIGHTED: WeightedAllocator,
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
allocator_class = allocators.get(strategy)
|
|
786
|
+
if allocator_class is None:
|
|
787
|
+
# Default to first-fit for unknown strategies
|
|
788
|
+
allocator_class = FirstFitAllocator
|
|
789
|
+
|
|
790
|
+
return allocator_class(resource_pool) # type: ignore[abstract]
|