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.
Files changed (55) hide show
  1. puffinflow/__init__.py +132 -0
  2. puffinflow/core/__init__.py +110 -0
  3. puffinflow/core/agent/__init__.py +320 -0
  4. puffinflow/core/agent/base.py +1635 -0
  5. puffinflow/core/agent/checkpoint.py +50 -0
  6. puffinflow/core/agent/context.py +521 -0
  7. puffinflow/core/agent/decorators/__init__.py +90 -0
  8. puffinflow/core/agent/decorators/builder.py +454 -0
  9. puffinflow/core/agent/decorators/flexible.py +714 -0
  10. puffinflow/core/agent/decorators/inspection.py +144 -0
  11. puffinflow/core/agent/dependencies.py +57 -0
  12. puffinflow/core/agent/scheduling/__init__.py +21 -0
  13. puffinflow/core/agent/scheduling/builder.py +160 -0
  14. puffinflow/core/agent/scheduling/exceptions.py +35 -0
  15. puffinflow/core/agent/scheduling/inputs.py +137 -0
  16. puffinflow/core/agent/scheduling/parser.py +209 -0
  17. puffinflow/core/agent/scheduling/scheduler.py +413 -0
  18. puffinflow/core/agent/state.py +141 -0
  19. puffinflow/core/config.py +62 -0
  20. puffinflow/core/coordination/__init__.py +137 -0
  21. puffinflow/core/coordination/agent_group.py +359 -0
  22. puffinflow/core/coordination/agent_pool.py +629 -0
  23. puffinflow/core/coordination/agent_team.py +577 -0
  24. puffinflow/core/coordination/coordinator.py +720 -0
  25. puffinflow/core/coordination/deadlock.py +1759 -0
  26. puffinflow/core/coordination/fluent_api.py +421 -0
  27. puffinflow/core/coordination/primitives.py +478 -0
  28. puffinflow/core/coordination/rate_limiter.py +520 -0
  29. puffinflow/core/observability/__init__.py +47 -0
  30. puffinflow/core/observability/agent.py +139 -0
  31. puffinflow/core/observability/alerting.py +73 -0
  32. puffinflow/core/observability/config.py +127 -0
  33. puffinflow/core/observability/context.py +88 -0
  34. puffinflow/core/observability/core.py +147 -0
  35. puffinflow/core/observability/decorators.py +105 -0
  36. puffinflow/core/observability/events.py +71 -0
  37. puffinflow/core/observability/interfaces.py +196 -0
  38. puffinflow/core/observability/metrics.py +137 -0
  39. puffinflow/core/observability/tracing.py +209 -0
  40. puffinflow/core/reliability/__init__.py +27 -0
  41. puffinflow/core/reliability/bulkhead.py +96 -0
  42. puffinflow/core/reliability/circuit_breaker.py +149 -0
  43. puffinflow/core/reliability/leak_detector.py +122 -0
  44. puffinflow/core/resources/__init__.py +77 -0
  45. puffinflow/core/resources/allocation.py +790 -0
  46. puffinflow/core/resources/pool.py +645 -0
  47. puffinflow/core/resources/quotas.py +567 -0
  48. puffinflow/core/resources/requirements.py +217 -0
  49. puffinflow/version.py +21 -0
  50. puffinflow-2.dev0.dist-info/METADATA +334 -0
  51. puffinflow-2.dev0.dist-info/RECORD +55 -0
  52. puffinflow-2.dev0.dist-info/WHEEL +5 -0
  53. puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
  54. puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
  55. 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]