1#![allow(dead_code)]
2use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct ResourceId(String);
14
15impl ResourceId {
16 pub fn new(name: impl Into<String>) -> Self {
18 Self(name.into())
19 }
20
21 #[must_use]
23 pub fn name(&self) -> &str {
24 &self.0
25 }
26}
27
28impl std::fmt::Display for ResourceId {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(f, "{}", self.0)
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct ResourceDescriptor {
37 pub id: ResourceId,
39 pub capacity: u64,
41 pub allocated: u64,
43 pub label: String,
45 pub unit: String,
47}
48
49impl ResourceDescriptor {
50 pub fn new(
52 id: ResourceId,
53 capacity: u64,
54 label: impl Into<String>,
55 unit: impl Into<String>,
56 ) -> Self {
57 Self {
58 id,
59 capacity,
60 allocated: 0,
61 label: label.into(),
62 unit: unit.into(),
63 }
64 }
65
66 #[must_use]
68 pub fn available(&self) -> u64 {
69 self.capacity.saturating_sub(self.allocated)
70 }
71
72 #[allow(clippy::cast_precision_loss)]
74 #[must_use]
75 pub fn utilisation(&self) -> f64 {
76 if self.capacity == 0 {
77 return 0.0;
78 }
79 self.allocated as f64 / self.capacity as f64
80 }
81
82 #[must_use]
84 pub fn can_allocate(&self, amount: u64) -> bool {
85 self.available() >= amount
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct AllocationToken {
92 pub token_id: u64,
94 pub resource_id: ResourceId,
96 pub amount: u64,
98}
99
100#[derive(Debug, Clone)]
102pub struct ResourceRequest {
103 pub resource_id: ResourceId,
105 pub amount: u64,
107 pub priority: u32,
109 pub requester: String,
111}
112
113impl ResourceRequest {
114 #[must_use]
116 pub fn new(resource_id: ResourceId, amount: u64) -> Self {
117 Self {
118 resource_id,
119 amount,
120 priority: 0,
121 requester: String::new(),
122 }
123 }
124
125 #[must_use]
127 pub fn with_priority(mut self, priority: u32) -> Self {
128 self.priority = priority;
129 self
130 }
131
132 pub fn with_requester(mut self, requester: impl Into<String>) -> Self {
134 self.requester = requester.into();
135 self
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum PoolError {
142 ResourceNotFound(String),
144 InsufficientCapacity {
146 resource: String,
148 requested: u64,
150 available: u64,
152 },
153 InvalidToken(u64),
155}
156
157impl std::fmt::Display for PoolError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 Self::ResourceNotFound(id) => write!(f, "resource not found: {id}"),
161 Self::InsufficientCapacity {
162 resource,
163 requested,
164 available,
165 } => {
166 write!(f, "insufficient capacity for '{resource}': requested {requested}, available {available}")
167 }
168 Self::InvalidToken(id) => write!(f, "invalid allocation token: {id}"),
169 }
170 }
171}
172
173#[derive(Debug)]
175pub struct ResourcePool {
176 resources: HashMap<ResourceId, ResourceDescriptor>,
178 allocations: HashMap<u64, AllocationToken>,
180 next_token_id: u64,
182}
183
184impl ResourcePool {
185 #[must_use]
187 pub fn new() -> Self {
188 Self {
189 resources: HashMap::new(),
190 allocations: HashMap::new(),
191 next_token_id: 1,
192 }
193 }
194
195 pub fn register(&mut self, descriptor: ResourceDescriptor) {
197 self.resources.insert(descriptor.id.clone(), descriptor);
198 }
199
200 pub fn allocate(&mut self, request: &ResourceRequest) -> Result<AllocationToken, PoolError> {
206 let resource = self
207 .resources
208 .get_mut(&request.resource_id)
209 .ok_or_else(|| PoolError::ResourceNotFound(request.resource_id.name().to_string()))?;
210
211 if !resource.can_allocate(request.amount) {
212 return Err(PoolError::InsufficientCapacity {
213 resource: resource.id.name().to_string(),
214 requested: request.amount,
215 available: resource.available(),
216 });
217 }
218
219 resource.allocated += request.amount;
220
221 let token = AllocationToken {
222 token_id: self.next_token_id,
223 resource_id: request.resource_id.clone(),
224 amount: request.amount,
225 };
226 self.next_token_id += 1;
227 self.allocations.insert(token.token_id, token.clone());
228 Ok(token)
229 }
230
231 pub fn release(&mut self, token_id: u64) -> Result<(), PoolError> {
237 let token = self
238 .allocations
239 .remove(&token_id)
240 .ok_or(PoolError::InvalidToken(token_id))?;
241
242 if let Some(resource) = self.resources.get_mut(&token.resource_id) {
243 resource.allocated = resource.allocated.saturating_sub(token.amount);
244 }
245
246 Ok(())
247 }
248
249 #[must_use]
251 pub fn snapshot(&self) -> Vec<ResourceDescriptor> {
252 self.resources.values().cloned().collect()
253 }
254
255 #[must_use]
257 pub fn get_resource(&self, id: &ResourceId) -> Option<&ResourceDescriptor> {
258 self.resources.get(id)
259 }
260
261 #[must_use]
263 pub fn active_allocations(&self) -> usize {
264 self.allocations.len()
265 }
266
267 #[must_use]
269 pub fn resource_count(&self) -> usize {
270 self.resources.len()
271 }
272
273 #[must_use]
275 pub fn can_satisfy(&self, request: &ResourceRequest) -> bool {
276 self.resources
277 .get(&request.resource_id)
278 .is_some_and(|r| r.can_allocate(request.amount))
279 }
280
281 pub fn reset_all(&mut self) {
283 self.allocations.clear();
284 for resource in self.resources.values_mut() {
285 resource.allocated = 0;
286 }
287 }
288}
289
290impl Default for ResourcePool {
291 fn default() -> Self {
292 Self::new()
293 }
294}
295
296#[derive(Debug, Clone)]
298pub struct PoolStats {
299 pub total_capacity: u64,
301 pub total_allocated: u64,
303 pub resource_count: usize,
305 pub active_allocations: usize,
307 pub average_utilisation: f64,
309}
310
311impl ResourcePool {
312 #[allow(clippy::cast_precision_loss)]
314 #[must_use]
315 pub fn stats(&self) -> PoolStats {
316 let total_capacity: u64 = self.resources.values().map(|r| r.capacity).sum();
317 let total_allocated: u64 = self.resources.values().map(|r| r.allocated).sum();
318 let resource_count = self.resources.len();
319 let active_allocations = self.allocations.len();
320 let average_utilisation = if resource_count == 0 {
321 0.0
322 } else {
323 let sum: f64 = self
324 .resources
325 .values()
326 .map(ResourceDescriptor::utilisation)
327 .sum();
328 sum / resource_count as f64
329 };
330
331 PoolStats {
332 total_capacity,
333 total_allocated,
334 resource_count,
335 active_allocations,
336 average_utilisation,
337 }
338 }
339}
340
341#[derive(Debug, Clone)]
347pub enum ScalingPolicy {
348 Fixed,
350 Step {
353 scale_up_threshold: f64,
355 scale_down_threshold: f64,
357 step_size: u64,
359 min_capacity: u64,
361 max_capacity: u64,
363 },
364 TargetTracking {
366 target_utilisation: f64,
368 min_capacity: u64,
370 max_capacity: u64,
372 },
373}
374
375#[derive(Debug, Clone, PartialEq, Eq)]
377pub enum ScalingAction {
378 NoChange,
380 ScaleUp(u64),
382 ScaleDown(u64),
384}
385
386impl std::fmt::Display for ScalingAction {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 match self {
389 Self::NoChange => write!(f, "no change"),
390 Self::ScaleUp(n) => write!(f, "scale up by {n}"),
391 Self::ScaleDown(n) => write!(f, "scale down by {n}"),
392 }
393 }
394}
395
396#[derive(Debug, Clone)]
398pub struct ScalingEvent {
399 pub resource_id: ResourceId,
401 pub action: ScalingAction,
403 pub old_capacity: u64,
405 pub new_capacity: u64,
407 pub utilisation: f64,
409 pub timestamp_ms: u64,
411}
412
413#[derive(Debug)]
415pub struct ResourceScaler {
416 policies: HashMap<ResourceId, ScalingPolicy>,
418 history: Vec<ScalingEvent>,
420 max_history: usize,
422 cooldown_ms: u64,
424 last_scale_time: HashMap<ResourceId, u64>,
426}
427
428impl ResourceScaler {
429 #[must_use]
431 pub fn new() -> Self {
432 Self {
433 policies: HashMap::new(),
434 history: Vec::new(),
435 max_history: 1000,
436 cooldown_ms: 60_000, last_scale_time: HashMap::new(),
438 }
439 }
440
441 #[must_use]
443 pub fn with_cooldown_ms(mut self, ms: u64) -> Self {
444 self.cooldown_ms = ms;
445 self
446 }
447
448 pub fn set_policy(&mut self, resource_id: ResourceId, policy: ScalingPolicy) {
450 self.policies.insert(resource_id, policy);
451 }
452
453 #[must_use]
455 #[allow(
456 clippy::cast_precision_loss,
457 clippy::cast_possible_truncation,
458 clippy::cast_sign_loss
459 )]
460 pub fn evaluate(&self, resource: &ResourceDescriptor) -> ScalingAction {
461 let policy = match self.policies.get(&resource.id) {
462 Some(p) => p,
463 None => return ScalingAction::NoChange,
464 };
465
466 let util = resource.utilisation();
467
468 match policy {
469 ScalingPolicy::Fixed => ScalingAction::NoChange,
470 ScalingPolicy::Step {
471 scale_up_threshold,
472 scale_down_threshold,
473 step_size,
474 min_capacity,
475 max_capacity,
476 } => {
477 if util >= *scale_up_threshold && resource.capacity < *max_capacity {
478 let new_cap = (resource.capacity + step_size).min(*max_capacity);
479 let delta = new_cap - resource.capacity;
480 if delta > 0 {
481 ScalingAction::ScaleUp(delta)
482 } else {
483 ScalingAction::NoChange
484 }
485 } else if util <= *scale_down_threshold && resource.capacity > *min_capacity {
486 let new_cap = resource
487 .capacity
488 .saturating_sub(*step_size)
489 .max(*min_capacity);
490 let new_cap = new_cap.max(resource.allocated);
492 let delta = resource.capacity - new_cap;
493 if delta > 0 {
494 ScalingAction::ScaleDown(delta)
495 } else {
496 ScalingAction::NoChange
497 }
498 } else {
499 ScalingAction::NoChange
500 }
501 }
502 ScalingPolicy::TargetTracking {
503 target_utilisation,
504 min_capacity,
505 max_capacity,
506 } => {
507 if resource.capacity == 0 || *target_utilisation <= 0.0 {
508 return ScalingAction::NoChange;
509 }
510
511 let desired = (resource.allocated as f64 / target_utilisation).ceil() as u64;
512 let desired = desired.clamp(*min_capacity, *max_capacity);
513
514 if desired > resource.capacity {
515 ScalingAction::ScaleUp(desired - resource.capacity)
516 } else if desired < resource.capacity {
517 let delta = resource.capacity - desired;
518 if desired >= resource.allocated {
520 ScalingAction::ScaleDown(delta)
521 } else {
522 ScalingAction::NoChange
523 }
524 } else {
525 ScalingAction::NoChange
526 }
527 }
528 }
529 }
530
531 pub fn apply(
535 &mut self,
536 pool: &mut ResourcePool,
537 resource_id: &ResourceId,
538 now_ms: u64,
539 ) -> Option<ScalingEvent> {
540 if let Some(&last_time) = self.last_scale_time.get(resource_id) {
542 if now_ms.saturating_sub(last_time) < self.cooldown_ms {
543 return None;
544 }
545 }
546
547 let resource = pool.get_resource(resource_id)?.clone();
548 let action = self.evaluate(&resource);
549
550 if action == ScalingAction::NoChange {
551 return None;
552 }
553
554 let old_capacity = resource.capacity;
555 let new_capacity = match &action {
556 ScalingAction::ScaleUp(delta) => old_capacity + delta,
557 ScalingAction::ScaleDown(delta) => old_capacity.saturating_sub(*delta),
558 ScalingAction::NoChange => return None,
559 };
560
561 if let Some(res) = pool.resources.get_mut(resource_id) {
563 res.capacity = new_capacity;
564 }
565
566 let event = ScalingEvent {
567 resource_id: resource_id.clone(),
568 action,
569 old_capacity,
570 new_capacity,
571 utilisation: resource.utilisation(),
572 timestamp_ms: now_ms,
573 };
574
575 self.last_scale_time.insert(resource_id.clone(), now_ms);
576 self.history.push(event.clone());
577 if self.history.len() > self.max_history {
578 self.history.remove(0);
579 }
580
581 Some(event)
582 }
583
584 #[must_use]
586 pub fn history(&self) -> &[ScalingEvent] {
587 &self.history
588 }
589
590 #[must_use]
592 pub fn history_for(&self, resource_id: &ResourceId) -> Vec<&ScalingEvent> {
593 self.history
594 .iter()
595 .filter(|e| &e.resource_id == resource_id)
596 .collect()
597 }
598
599 #[must_use]
601 pub fn policy_count(&self) -> usize {
602 self.policies.len()
603 }
604}
605
606impl Default for ResourceScaler {
607 fn default() -> Self {
608 Self::new()
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615
616 fn cpu_resource() -> ResourceDescriptor {
617 ResourceDescriptor::new(ResourceId::new("cpu"), 8, "CPU Cores", "cores")
618 }
619
620 fn gpu_resource() -> ResourceDescriptor {
621 ResourceDescriptor::new(ResourceId::new("gpu"), 2, "GPU Devices", "devices")
622 }
623
624 #[test]
625 fn test_resource_descriptor_available() {
626 let mut r = cpu_resource();
627 assert_eq!(r.available(), 8);
628 r.allocated = 3;
629 assert_eq!(r.available(), 5);
630 }
631
632 #[test]
633 fn test_resource_descriptor_utilisation() {
634 let mut r = cpu_resource();
635 assert!((r.utilisation() - 0.0).abs() < f64::EPSILON);
636 r.allocated = 4;
637 assert!((r.utilisation() - 0.5).abs() < f64::EPSILON);
638 }
639
640 #[test]
641 fn test_resource_descriptor_zero_capacity() {
642 let r = ResourceDescriptor::new(ResourceId::new("empty"), 0, "Empty", "units");
643 assert!((r.utilisation() - 0.0).abs() < f64::EPSILON);
644 assert!(!r.can_allocate(1));
645 }
646
647 #[test]
648 fn test_pool_register_and_count() {
649 let mut pool = ResourcePool::new();
650 assert_eq!(pool.resource_count(), 0);
651 pool.register(cpu_resource());
652 assert_eq!(pool.resource_count(), 1);
653 pool.register(gpu_resource());
654 assert_eq!(pool.resource_count(), 2);
655 }
656
657 #[test]
658 fn test_pool_allocate_success() {
659 let mut pool = ResourcePool::new();
660 pool.register(cpu_resource());
661 let req = ResourceRequest::new(ResourceId::new("cpu"), 4);
662 let token = pool.allocate(&req).expect("should succeed in test");
663 assert_eq!(token.amount, 4);
664 assert_eq!(pool.active_allocations(), 1);
665 let r = pool
666 .get_resource(&ResourceId::new("cpu"))
667 .expect("should succeed in test");
668 assert_eq!(r.allocated, 4);
669 }
670
671 #[test]
672 fn test_pool_allocate_insufficient() {
673 let mut pool = ResourcePool::new();
674 pool.register(cpu_resource());
675 let req = ResourceRequest::new(ResourceId::new("cpu"), 100);
676 let err = pool.allocate(&req).unwrap_err();
677 assert!(matches!(err, PoolError::InsufficientCapacity { .. }));
678 }
679
680 #[test]
681 fn test_pool_allocate_unknown_resource() {
682 let mut pool = ResourcePool::new();
683 let req = ResourceRequest::new(ResourceId::new("missing"), 1);
684 let err = pool.allocate(&req).unwrap_err();
685 assert!(matches!(err, PoolError::ResourceNotFound(_)));
686 }
687
688 #[test]
689 fn test_pool_release_success() {
690 let mut pool = ResourcePool::new();
691 pool.register(cpu_resource());
692 let req = ResourceRequest::new(ResourceId::new("cpu"), 4);
693 let token = pool.allocate(&req).expect("should succeed in test");
694 pool.release(token.token_id)
695 .expect("should succeed in test");
696 assert_eq!(pool.active_allocations(), 0);
697 let r = pool
698 .get_resource(&ResourceId::new("cpu"))
699 .expect("should succeed in test");
700 assert_eq!(r.allocated, 0);
701 }
702
703 #[test]
704 fn test_pool_release_invalid_token() {
705 let mut pool = ResourcePool::new();
706 let err = pool.release(9999).unwrap_err();
707 assert!(matches!(err, PoolError::InvalidToken(9999)));
708 }
709
710 #[test]
711 fn test_pool_can_satisfy() {
712 let mut pool = ResourcePool::new();
713 pool.register(cpu_resource());
714 let req_ok = ResourceRequest::new(ResourceId::new("cpu"), 4);
715 assert!(pool.can_satisfy(&req_ok));
716 let req_too_much = ResourceRequest::new(ResourceId::new("cpu"), 100);
717 assert!(!pool.can_satisfy(&req_too_much));
718 }
719
720 #[test]
721 fn test_pool_reset_all() {
722 let mut pool = ResourcePool::new();
723 pool.register(cpu_resource());
724 pool.register(gpu_resource());
725 let req1 = ResourceRequest::new(ResourceId::new("cpu"), 4);
726 let req2 = ResourceRequest::new(ResourceId::new("gpu"), 1);
727 let _t1 = pool.allocate(&req1).expect("should succeed in test");
728 let _t2 = pool.allocate(&req2).expect("should succeed in test");
729 assert_eq!(pool.active_allocations(), 2);
730 pool.reset_all();
731 assert_eq!(pool.active_allocations(), 0);
732 for r in pool.snapshot() {
733 assert_eq!(r.allocated, 0);
734 }
735 }
736
737 #[test]
738 fn test_pool_stats() {
739 let mut pool = ResourcePool::new();
740 pool.register(cpu_resource());
741 pool.register(gpu_resource());
742 let req = ResourceRequest::new(ResourceId::new("cpu"), 4);
743 let _t = pool.allocate(&req).expect("should succeed in test");
744
745 let stats = pool.stats();
746 assert_eq!(stats.total_capacity, 10); assert_eq!(stats.total_allocated, 4);
748 assert_eq!(stats.resource_count, 2);
749 assert_eq!(stats.active_allocations, 1);
750 assert!(stats.average_utilisation > 0.0);
751 }
752
753 #[test]
754 fn test_resource_request_builder() {
755 let req = ResourceRequest::new(ResourceId::new("cpu"), 2)
756 .with_priority(10)
757 .with_requester("task-1");
758 assert_eq!(req.priority, 10);
759 assert_eq!(req.requester, "task-1");
760 }
761
762 #[test]
763 fn test_multiple_allocations_same_resource() {
764 let mut pool = ResourcePool::new();
765 pool.register(cpu_resource());
766 let req = ResourceRequest::new(ResourceId::new("cpu"), 3);
767 let t1 = pool.allocate(&req).expect("should succeed in test");
768 let t2 = pool.allocate(&req).expect("should succeed in test");
769 assert_eq!(pool.active_allocations(), 2);
770 let r = pool
771 .get_resource(&ResourceId::new("cpu"))
772 .expect("should succeed in test");
773 assert_eq!(r.allocated, 6);
774
775 let req_too_much = ResourceRequest::new(ResourceId::new("cpu"), 3);
777 assert!(pool.allocate(&req_too_much).is_err());
778
779 pool.release(t1.token_id).expect("should succeed in test");
780 let _t3 = pool
782 .allocate(&req_too_much)
783 .expect("should succeed in test");
784 assert_eq!(pool.active_allocations(), 2);
785 pool.release(t2.token_id).expect("should succeed in test");
786 }
787
788 #[test]
791 fn test_scaler_fixed_policy() {
792 let scaler = ResourceScaler::new();
793 let resource = cpu_resource();
794 assert_eq!(scaler.evaluate(&resource), ScalingAction::NoChange);
795 }
796
797 #[test]
798 fn test_scaler_step_scale_up() {
799 let mut scaler = ResourceScaler::new();
800 scaler.set_policy(
801 ResourceId::new("cpu"),
802 ScalingPolicy::Step {
803 scale_up_threshold: 0.8,
804 scale_down_threshold: 0.2,
805 step_size: 4,
806 min_capacity: 4,
807 max_capacity: 32,
808 },
809 );
810
811 let mut resource = cpu_resource(); resource.allocated = 7; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleUp(4));
815 }
816
817 #[test]
818 fn test_scaler_step_scale_down() {
819 let mut scaler = ResourceScaler::new();
820 scaler.set_policy(
821 ResourceId::new("cpu"),
822 ScalingPolicy::Step {
823 scale_up_threshold: 0.8,
824 scale_down_threshold: 0.2,
825 step_size: 4,
826 min_capacity: 4,
827 max_capacity: 32,
828 },
829 );
830
831 let mut resource = cpu_resource(); resource.allocated = 1; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleDown(4));
835 }
836
837 #[test]
838 fn test_scaler_step_capped_at_max() {
839 let mut scaler = ResourceScaler::new();
840 scaler.set_policy(
841 ResourceId::new("cpu"),
842 ScalingPolicy::Step {
843 scale_up_threshold: 0.8,
844 scale_down_threshold: 0.2,
845 step_size: 100,
846 min_capacity: 4,
847 max_capacity: 10,
848 },
849 );
850
851 let mut resource = cpu_resource(); resource.allocated = 7; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleUp(2));
856 }
857
858 #[test]
859 fn test_scaler_step_capped_at_min() {
860 let mut scaler = ResourceScaler::new();
861 scaler.set_policy(
862 ResourceId::new("cpu"),
863 ScalingPolicy::Step {
864 scale_up_threshold: 0.8,
865 scale_down_threshold: 0.2,
866 step_size: 100,
867 min_capacity: 6,
868 max_capacity: 32,
869 },
870 );
871
872 let mut resource = cpu_resource(); resource.allocated = 0; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleDown(2));
877 }
878
879 #[test]
880 fn test_scaler_target_tracking_scale_up() {
881 let mut scaler = ResourceScaler::new();
882 scaler.set_policy(
883 ResourceId::new("cpu"),
884 ScalingPolicy::TargetTracking {
885 target_utilisation: 0.5,
886 min_capacity: 4,
887 max_capacity: 32,
888 },
889 );
890
891 let mut resource = cpu_resource(); resource.allocated = 7; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleUp(6));
895 }
896
897 #[test]
898 fn test_scaler_target_tracking_scale_down() {
899 let mut scaler = ResourceScaler::new();
900 scaler.set_policy(
901 ResourceId::new("cpu"),
902 ScalingPolicy::TargetTracking {
903 target_utilisation: 0.5,
904 min_capacity: 4,
905 max_capacity: 32,
906 },
907 );
908
909 let mut resource = ResourceDescriptor::new(ResourceId::new("cpu"), 16, "CPU", "cores");
910 resource.allocated = 2; assert_eq!(scaler.evaluate(&resource), ScalingAction::ScaleDown(12));
913 }
914
915 #[test]
916 fn test_scaler_target_tracking_at_target() {
917 let mut scaler = ResourceScaler::new();
918 scaler.set_policy(
919 ResourceId::new("cpu"),
920 ScalingPolicy::TargetTracking {
921 target_utilisation: 0.5,
922 min_capacity: 4,
923 max_capacity: 32,
924 },
925 );
926
927 let mut resource = cpu_resource(); resource.allocated = 4; assert_eq!(scaler.evaluate(&resource), ScalingAction::NoChange);
931 }
932
933 #[test]
934 fn test_scaler_apply_to_pool() {
935 let mut pool = ResourcePool::new();
936 pool.register(cpu_resource()); let req = ResourceRequest::new(ResourceId::new("cpu"), 7);
940 let _t = pool.allocate(&req).expect("allocate");
941
942 let mut scaler = ResourceScaler::new().with_cooldown_ms(0);
943 scaler.set_policy(
944 ResourceId::new("cpu"),
945 ScalingPolicy::Step {
946 scale_up_threshold: 0.8,
947 scale_down_threshold: 0.2,
948 step_size: 4,
949 min_capacity: 4,
950 max_capacity: 32,
951 },
952 );
953
954 let event = scaler.apply(&mut pool, &ResourceId::new("cpu"), 1000);
955 assert!(event.is_some());
956 let event = event.expect("event");
957 assert_eq!(event.old_capacity, 8);
958 assert_eq!(event.new_capacity, 12);
959
960 let resource = pool
961 .get_resource(&ResourceId::new("cpu"))
962 .expect("resource");
963 assert_eq!(resource.capacity, 12);
964 }
965
966 #[test]
967 fn test_scaler_cooldown() {
968 let mut pool = ResourcePool::new();
969 pool.register(cpu_resource());
970 let req = ResourceRequest::new(ResourceId::new("cpu"), 7);
971 let _t = pool.allocate(&req).expect("allocate");
972
973 let mut scaler = ResourceScaler::new().with_cooldown_ms(60_000);
974 scaler.set_policy(
975 ResourceId::new("cpu"),
976 ScalingPolicy::Step {
977 scale_up_threshold: 0.8,
978 scale_down_threshold: 0.2,
979 step_size: 4,
980 min_capacity: 4,
981 max_capacity: 32,
982 },
983 );
984
985 let event = scaler.apply(&mut pool, &ResourceId::new("cpu"), 1000);
987 assert!(event.is_some());
988
989 let event = scaler.apply(&mut pool, &ResourceId::new("cpu"), 2000);
991 assert!(event.is_none());
992
993 let event = scaler.apply(&mut pool, &ResourceId::new("cpu"), 70_000);
995 let _ = event;
998 }
999
1000 #[test]
1001 fn test_scaler_history() {
1002 let mut pool = ResourcePool::new();
1003 pool.register(cpu_resource());
1004 let req = ResourceRequest::new(ResourceId::new("cpu"), 7);
1005 let _t = pool.allocate(&req).expect("allocate");
1006
1007 let mut scaler = ResourceScaler::new().with_cooldown_ms(0);
1008 scaler.set_policy(
1009 ResourceId::new("cpu"),
1010 ScalingPolicy::Step {
1011 scale_up_threshold: 0.8,
1012 scale_down_threshold: 0.2,
1013 step_size: 4,
1014 min_capacity: 4,
1015 max_capacity: 32,
1016 },
1017 );
1018
1019 scaler.apply(&mut pool, &ResourceId::new("cpu"), 1000);
1020 assert_eq!(scaler.history().len(), 1);
1021
1022 let cpu_history = scaler.history_for(&ResourceId::new("cpu"));
1023 assert_eq!(cpu_history.len(), 1);
1024 assert_eq!(cpu_history[0].old_capacity, 8);
1025 }
1026
1027 #[test]
1028 fn test_scaling_action_display() {
1029 assert_eq!(ScalingAction::NoChange.to_string(), "no change");
1030 assert_eq!(ScalingAction::ScaleUp(4).to_string(), "scale up by 4");
1031 assert_eq!(ScalingAction::ScaleDown(2).to_string(), "scale down by 2");
1032 }
1033
1034 #[test]
1035 fn test_scaler_policy_count() {
1036 let mut scaler = ResourceScaler::new();
1037 assert_eq!(scaler.policy_count(), 0);
1038 scaler.set_policy(ResourceId::new("cpu"), ScalingPolicy::Fixed);
1039 assert_eq!(scaler.policy_count(), 1);
1040 }
1041
1042 #[test]
1043 fn test_scaler_unknown_resource() {
1044 let mut scaler = ResourceScaler::new();
1045 let mut pool = ResourcePool::new();
1046 pool.register(cpu_resource());
1047
1048 let event = scaler.apply(&mut pool, &ResourceId::new("gpu"), 1000);
1050 assert!(event.is_none());
1051 }
1052}