service-capacity-modeling 0.3.74__py3-none-any.whl → 0.3.75__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.

Potentially problematic release.


This version of service-capacity-modeling might be problematic. Click here for more details.

@@ -264,14 +264,14 @@ class Drive(ExcludeUnsetModel):
264
264
  return max(self.block_size_kib, self.group_size_kib)
265
265
 
266
266
  @property
267
- def max_size_gib(self):
267
+ def max_size_gib(self) -> int:
268
268
  if self.max_scale_size_gib != 0:
269
269
  return self.max_scale_size_gib
270
270
  else:
271
271
  return self.size_gib
272
272
 
273
273
  @property
274
- def max_io_per_s(self):
274
+ def max_io_per_s(self) -> int:
275
275
  if self.max_scale_io_per_s != 0:
276
276
  return self.max_scale_io_per_s
277
277
  else:
@@ -779,23 +779,48 @@ class BufferComponent(str, Enum):
779
779
  compute = "compute"
780
780
  # [Data Shape] a.k.a. "Dataset" related buffers, e.g. Disk and Memory
781
781
  storage = "storage"
782
-
783
782
  # Resource specific component
784
783
  cpu = "cpu"
785
784
  network = "network"
786
785
  disk = "disk"
787
786
  memory = "memory"
788
787
 
788
+ @staticmethod
789
+ def is_generic(component: str) -> bool:
790
+ return component in {BufferComponent.compute, BufferComponent.storage}
791
+
792
+ @staticmethod
793
+ def is_specific(component: str) -> bool:
794
+ return not BufferComponent.is_generic(component)
795
+
789
796
 
790
797
  class BufferIntent(str, Enum):
791
798
  # Most buffers show "desired" buffer, this is the default
792
799
  desired = "desired"
793
800
  # ratio on top of existing buffers to ensure exists. Generally combined
794
801
  # with a different desired buffer to ensure we don't just scale needlessly
802
+ # This means we can scale up or down as as long as we meet the desired buffer.
795
803
  scale = "scale"
796
- # Ignore model preferences, just preserve existing buffers
804
+
805
+ # DEPRECATED: Use scale_up/scale_down instead
806
+ # Ignores model preferences, just preserve existing buffers
807
+ # We rarely actually want to do this since it can cause severe over provisioning
797
808
  preserve = "preserve"
798
809
 
810
+ # Scale up if necessary to meet the desired buffer.
811
+ # If the existing resource is over-provisioned, do not reduce the requirement.
812
+ # If under-provisioned, the requirement can be increased to meet the desired buffer.
813
+ # Example: need 20 cores but have 10 → scale up to 20 cores.
814
+ # Example 2: need 20 cores but have 40 → do not scale down and require at
815
+ # least 40 cores
816
+ scale_up = "scale_up"
817
+ # Scale down if necessary to meet the desired buffer.
818
+ # If the existing resource is under-provisioned, do not increase the requirement.
819
+ # If over-provisioned, the requirement can be decreased to meet the desired buffer.
820
+ # Example: need 20 cores but have 10 → maintain buffer and do not scale up.
821
+ # Example 2: need 20 cores but have 40 → scale down to 20 cores.
822
+ scale_down = "scale_down"
823
+
799
824
 
800
825
  class Buffer(ExcludeUnsetModel):
801
826
  # The value of the buffer expressed as a ratio over "normal" load e.g. 1.5x
@@ -819,7 +844,6 @@ class Buffers(ExcludeUnsetModel):
819
844
  "compute": Buffer(ratio: 1.5),
820
845
  }
821
846
  )
822
-
823
847
  And then models layer in their buffers, for example if a workload
824
848
  requires 10 CPU cores, but the operator of that workload likes to build in
825
849
  2x buffer for background work (20 cores provisioned), they would express that
@@ -1,3 +1,4 @@
1
+ # pylint: disable=too-many-lines
1
2
  import logging
2
3
  import math
3
4
  import random
@@ -6,8 +7,12 @@ from typing import Callable
6
7
  from typing import Dict
7
8
  from typing import List
8
9
  from typing import Optional
10
+ from typing import Set
9
11
  from typing import Tuple
10
12
 
13
+ from pydantic import BaseModel
14
+ from pydantic import Field
15
+
11
16
  from service_capacity_modeling.hardware import shapes
12
17
  from service_capacity_modeling.interface import AVG_ITEM_SIZE_BYTES
13
18
  from service_capacity_modeling.interface import Buffer
@@ -63,6 +68,23 @@ def _QOS(tier: int) -> float:
63
68
  return 1
64
69
 
65
70
 
71
+ def combine_buffer_ratios(left: Optional[float], right: Optional[float]) -> float:
72
+ """
73
+ Strategy for how two buffers for the same component are combined.
74
+ - Multiply two buffers by multiplying if both are not None
75
+ """
76
+
77
+ if left is None and right is None:
78
+ raise ValueError("Cannot combine buffer ratios when both values are None")
79
+ if left is None:
80
+ assert right is not None # MyPy
81
+ return right
82
+ if right is None:
83
+ assert left is not None # MyPy
84
+ return left
85
+ return left * right
86
+
87
+
66
88
  def _sqrt_staffed_cores(rps: float, latency_s: float, qos: float) -> int:
67
89
  # Square root staffing
68
90
  # s = a + Q*sqrt(a)
@@ -153,18 +175,31 @@ def normalize_cores(
153
175
  target_shape: Instance,
154
176
  reference_shape: Optional[Instance] = None,
155
177
  ) -> int:
156
- """Calculates equivalent cores on a target shape relative to a reference
178
+ """Calculates equivalent CPU on a target shape relative to a reference
157
179
 
158
180
  Takes into account relative core frequency and IPC factor from the hardware
159
181
  description to give a rough estimate of how many equivalent cores you need
160
182
  in a target_shape to have the core_count number of cores on the reference_shape
161
183
  """
184
+ # Normalize the core count the same as CPUs
185
+ return _normalize_cpu(
186
+ cpu_count=core_count,
187
+ target_shape=target_shape,
188
+ reference_shape=reference_shape,
189
+ )
190
+
191
+
192
+ def _normalize_cpu(
193
+ cpu_count: float,
194
+ target_shape: Instance,
195
+ reference_shape: Optional[Instance] = None,
196
+ ) -> int:
162
197
  if reference_shape is None:
163
198
  reference_shape = default_reference_shape
164
199
 
165
200
  target_speed = target_shape.cpu_ghz * target_shape.cpu_ipc_scale
166
201
  reference_speed = reference_shape.cpu_ghz * reference_shape.cpu_ipc_scale
167
- return max(1, math.ceil(core_count / (target_speed / reference_speed)))
202
+ return max(1, math.ceil(cpu_count / (target_speed / reference_speed)))
168
203
 
169
204
 
170
205
  def _reserved_headroom(
@@ -218,8 +253,6 @@ def cpu_headroom_target(instance: Instance, buffers: Optional[Buffers] = None) -
218
253
  # When someone asks for the key, return any buffers that
219
254
  # influence the component in the value
220
255
  _default_buffer_fallbacks: Dict[str, List[str]] = {
221
- BufferComponent.compute: [BufferComponent.cpu],
222
- BufferComponent.storage: [BufferComponent.disk],
223
256
  BufferComponent.cpu: [BufferComponent.compute],
224
257
  BufferComponent.network: [BufferComponent.compute],
225
258
  BufferComponent.memory: [BufferComponent.storage],
@@ -227,6 +260,44 @@ _default_buffer_fallbacks: Dict[str, List[str]] = {
227
260
  }
228
261
 
229
262
 
263
+ def _expand_components(
264
+ components: List[str],
265
+ component_fallbacks: Optional[Dict[str, List[str]]] = None,
266
+ ) -> Set[str]:
267
+ """Expand and dedupe components to include their fallbacks
268
+
269
+ Args:
270
+ components: List of component names to expand
271
+ component_fallbacks: Optional fallback mapping (uses default if None)
272
+
273
+ Returns:
274
+ Set of expanded component names including fallbacks
275
+ """
276
+
277
+ # Semantically it does not make sense to fetch buffers for the generic category
278
+ generic_components = [c for c in components if BufferComponent.is_generic(c)]
279
+ if generic_components:
280
+ all_specific_components = [
281
+ c for c in BufferComponent if BufferComponent.is_specific(c)
282
+ ]
283
+ raise ValueError(
284
+ f"Only specific components allowed. Generic components found: "
285
+ f"{', '.join(str(c) for c in generic_components)}. "
286
+ f"Use specific components instead: "
287
+ f"{', '.join(str(c) for c in all_specific_components)}"
288
+ )
289
+
290
+ if component_fallbacks is None:
291
+ component_fallbacks = _default_buffer_fallbacks
292
+
293
+ expanded_components = set(components)
294
+ for component in components:
295
+ expanded_components = expanded_components | set(
296
+ component_fallbacks.get(component, [])
297
+ )
298
+ return expanded_components
299
+
300
+
230
301
  def buffer_for_components(
231
302
  buffers: Buffers,
232
303
  components: List[str],
@@ -245,14 +316,7 @@ def buffer_for_components(
245
316
  components: the components that ultimately matched after applying
246
317
  source: All the component buffers that made up the composite ratio
247
318
  """
248
- if component_fallbacks is None:
249
- component_fallbacks = _default_buffer_fallbacks
250
-
251
- unique_components = set(components)
252
- for component in components:
253
- unique_components = unique_components | set(
254
- component_fallbacks.get(component, [])
255
- )
319
+ expanded_components = _expand_components(components, component_fallbacks)
256
320
 
257
321
  desired = {k: v.model_copy() for k, v in buffers.desired.items()}
258
322
  if current_capacity:
@@ -266,14 +330,14 @@ def buffer_for_components(
266
330
  ratio = 1.0
267
331
  sources = {}
268
332
  for name, buffer in desired.items():
269
- if any(i in unique_components for i in buffer.components):
333
+ if expanded_components.intersection(buffer.components):
270
334
  sources[name] = buffer
271
- ratio *= buffer.ratio
335
+ ratio = combine_buffer_ratios(ratio, buffer.ratio)
272
336
  if not sources:
273
337
  ratio = buffers.default.ratio
274
338
 
275
339
  return Buffer(
276
- ratio=ratio, components=sorted(list(unique_components)), sources=sources
340
+ ratio=ratio, components=sorted(list(expanded_components)), sources=sources
277
341
  )
278
342
 
279
343
 
@@ -483,7 +547,7 @@ def compute_stateful_zone( # pylint: disable=too-many-positional-arguments
483
547
  # When initially provisioniong we don't want to attach more than
484
548
  # 1/3 the maximum volume size in one node (preferring more nodes
485
549
  # with smaller volumes)
486
- max_size = drive.max_size_gib / 3
550
+ max_size = math.ceil(drive.max_size_gib / 3)
487
551
  if ebs_gib > max_size > 0:
488
552
  ratio = ebs_gib / max_size
489
553
  count = max(cluster_size(math.ceil(count * ratio)), min_count)
@@ -717,180 +781,188 @@ def merge_plan(
717
781
  )
718
782
 
719
783
 
720
- def derived_buffer_for_component(buffer: Dict[str, Buffer], components: List[str]):
721
- scale = 0.0
722
- preserve = False
723
-
724
- if not buffer:
725
- return scale, preserve
726
-
727
- for bfr in buffer.values():
728
- if any(component in components for component in bfr.components):
729
- if bfr.intent == BufferIntent.scale:
730
- scale = max(scale, bfr.ratio)
784
+ class DerivedBuffers(BaseModel):
785
+ scale: float = Field(default=1, gt=0)
786
+ preserve: bool = False
787
+ # When present, this is the maximum ratio of the current usage
788
+ ceiling: Optional[float] = Field(
789
+ default=None,
790
+ gt=0,
791
+ )
792
+ # When present, this is the minimum ratio of the current usage
793
+ floor: Optional[float] = Field(default=None, gt=0)
794
+
795
+ @staticmethod
796
+ def for_components(
797
+ buffer: Dict[str, Buffer],
798
+ components: List[str],
799
+ component_fallbacks: Optional[Dict[str, List[str]]] = None,
800
+ ):
801
+ expanded_components = _expand_components(components, component_fallbacks)
802
+
803
+ scale = 1.0
804
+ preserve = False
805
+ ceiling = None
806
+ floor = None
807
+
808
+ for bfr in buffer.values():
809
+ if not expanded_components.intersection(bfr.components):
810
+ continue
811
+
812
+ if bfr.intent in [
813
+ BufferIntent.scale,
814
+ BufferIntent.scale_up,
815
+ BufferIntent.scale_down,
816
+ ]:
817
+ scale = combine_buffer_ratios(scale, bfr.ratio)
818
+ if bfr.intent == BufferIntent.scale_up:
819
+ floor = 1 # Create a floor of 1.0x the current usage
820
+ if bfr.intent == BufferIntent.scale_down:
821
+ ceiling = 1 # Create a ceiling of 1.0x the current usage
731
822
  if bfr.intent == BufferIntent.preserve:
732
823
  preserve = True
733
824
 
734
- return scale, preserve
735
-
736
-
737
- def get_cores_from_current_capacity(
738
- current_capacity: CurrentClusterCapacity, buffers: Buffers, instance: Instance
739
- ):
740
- # compute cores required per zone
741
- cpu_success_buffer = (1 - cpu_headroom_target(instance, buffers)) * 100
742
- current_cpu_utilization = current_capacity.cpu_utilization.mid
743
-
744
- if current_capacity.cluster_instance is None:
745
- cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
746
- else:
747
- cluster_instance = current_capacity.cluster_instance
748
-
749
- current_cores = cluster_instance.cpu * current_capacity.cluster_instance_count.mid
750
-
751
- scale, preserve = derived_buffer_for_component(buffers.derived, ["compute", "cpu"])
752
- # Scale and preserve for the same component should not be passed together.
753
- # If user passes it, then scale will be preferred over preserve.
754
- if scale > 0:
755
- # if the new cpu core is less than the current,
756
- # then take no action and return the current cpu cores
757
- new_cpu_utilization = current_cpu_utilization * scale
758
- core_scale_up_factor = max(1.0, new_cpu_utilization / cpu_success_buffer)
759
- return math.ceil(current_cores * core_scale_up_factor)
760
-
761
- if preserve:
762
- return current_cores
763
-
764
- return int(current_cores * (current_cpu_utilization / cpu_success_buffer))
765
-
766
-
767
- def get_memory_from_current_capacity(
768
- current_capacity: CurrentClusterCapacity, buffers: Buffers
769
- ):
770
- # compute memory required per zone
771
- current_memory_utilization = (
772
- current_capacity.memory_utilization_gib.mid
773
- * current_capacity.cluster_instance_count.mid
774
- )
775
-
776
- if current_capacity.cluster_instance is None:
777
- cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
778
- else:
779
- cluster_instance = current_capacity.cluster_instance
780
-
781
- zonal_ram_allocated = (
782
- cluster_instance.ram_gib * current_capacity.cluster_instance_count.mid
783
- )
784
-
785
- # These are the desired buffers
786
- memory_buffer = buffer_for_components(
787
- buffers=buffers, components=[BufferComponent.memory]
788
- )
789
-
790
- scale, preserve = derived_buffer_for_component(
791
- buffers.derived, ["memory", "storage"]
792
- )
793
- # Scale and preserve for the same component should not be passed together.
794
- # If user passes it, then scale will be preferred over preserve.
795
- if scale > 0:
796
- # if the new required memory is less than the current,
797
- # then take no action and return the current ram
798
- return max(
799
- current_memory_utilization * scale * memory_buffer.ratio,
800
- zonal_ram_allocated,
825
+ return DerivedBuffers(
826
+ scale=scale, preserve=preserve, ceiling=ceiling, floor=floor
801
827
  )
802
828
 
803
- if preserve:
804
- return zonal_ram_allocated
805
-
806
- return current_memory_utilization * memory_buffer.ratio
807
-
808
-
809
- def get_network_from_current_capacity(
810
- current_capacity: CurrentClusterCapacity, buffers: Buffers
811
- ):
812
- # compute network required per zone
813
- current_network_utilization = (
814
- current_capacity.network_utilization_mbps.mid
815
- * current_capacity.cluster_instance_count.mid
816
- )
817
-
818
- if current_capacity.cluster_instance is None:
819
- cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
820
- else:
821
- cluster_instance = current_capacity.cluster_instance
822
-
823
- zonal_network_allocated = (
824
- cluster_instance.net_mbps * current_capacity.cluster_instance_count.mid
825
- )
826
-
827
- # These are the desired buffers
828
- network_buffer = buffer_for_components(
829
- buffers=buffers, components=[BufferComponent.network]
830
- )
829
+ def calculate_requirement(
830
+ self,
831
+ current_usage: float,
832
+ existing_capacity: float,
833
+ desired_buffer_ratio: float = 1.0,
834
+ ) -> float:
835
+ if self.preserve:
836
+ return existing_capacity
837
+
838
+ requirement = self.scale * current_usage * desired_buffer_ratio
839
+ if self.ceiling is not None:
840
+ requirement = min(requirement, self.ceiling * existing_capacity)
841
+ if self.floor is not None:
842
+ requirement = max(requirement, self.floor * existing_capacity)
843
+
844
+ return requirement
845
+
846
+
847
+ class RequirementFromCurrentCapacity(BaseModel):
848
+ current_capacity: CurrentClusterCapacity
849
+ buffers: Buffers
850
+
851
+ @property
852
+ def current_instance(self) -> Instance:
853
+ if self.current_capacity.cluster_instance is not None:
854
+ return self.current_capacity.cluster_instance
855
+ return shapes.instance(self.current_capacity.cluster_instance_name)
856
+
857
+ def cpu(self, instance_candidate: Instance) -> int:
858
+ current_cpu_util = self.current_capacity.cpu_utilization.mid / 100
859
+ current_total_cpu = (
860
+ self.current_instance.cpu * self.current_capacity.cluster_instance_count.mid
861
+ )
831
862
 
832
- scale, preserve = derived_buffer_for_component(
833
- buffers.derived, ["compute", "network"]
834
- )
835
- # Scale and preserve for the same component should not be passed together.
836
- # If user passes it, then scale will be preferred over preserve.
837
- if scale > 0:
838
- # if the new required network is less than the current,
839
- # then take no action and return the current bandwidth
840
- return max(
841
- current_network_utilization * scale * network_buffer.ratio,
842
- zonal_network_allocated,
863
+ derived_buffers = DerivedBuffers.for_components(
864
+ self.buffers.derived, [BufferComponent.cpu]
843
865
  )
844
866
 
845
- if preserve:
846
- return zonal_network_allocated
867
+ # The ideal CPU% that accomodates the headroom + desired buffer, sometimes
868
+ # referred to as the "success buffer"
869
+ target_cpu_util = 1 - cpu_headroom_target(instance_candidate, self.buffers)
870
+ # current_util / target_util ratio indicates CPU scaling direction:
871
+ # > 1: scale up, < 1: scale down, = 1: no change needed
872
+ used_cpu = (current_cpu_util / target_cpu_util) * current_total_cpu
873
+ return math.ceil(
874
+ # Desired buffer is omitted because the cpu_headroom already
875
+ # includes it
876
+ derived_buffers.calculate_requirement(
877
+ current_usage=used_cpu,
878
+ existing_capacity=current_total_cpu,
879
+ )
880
+ )
847
881
 
848
- return current_network_utilization * network_buffer.ratio
882
+ @property
883
+ def mem_gib(self) -> float:
884
+ current_memory_utilization = (
885
+ self.current_capacity.memory_utilization_gib.mid
886
+ * self.current_capacity.cluster_instance_count.mid
887
+ )
888
+ zonal_ram_allocated = (
889
+ self.current_instance.ram_gib
890
+ * self.current_capacity.cluster_instance_count.mid
891
+ )
849
892
 
893
+ desired_buffer = buffer_for_components(
894
+ buffers=self.buffers, components=[BufferComponent.memory]
895
+ )
896
+ derived_buffer = DerivedBuffers.for_components(
897
+ self.buffers.derived, [BufferComponent.memory]
898
+ )
850
899
 
851
- def get_disk_from_current_capacity(
852
- current_capacity: CurrentClusterCapacity, buffers: Buffers
853
- ):
854
- # compute disk required per zone
855
- current_disk_utilization = (
856
- current_capacity.disk_utilization_gib.mid
857
- * current_capacity.cluster_instance_count.mid
858
- )
900
+ return derived_buffer.calculate_requirement(
901
+ current_usage=current_memory_utilization,
902
+ existing_capacity=zonal_ram_allocated,
903
+ desired_buffer_ratio=desired_buffer.ratio,
904
+ )
859
905
 
860
- if current_capacity.cluster_instance is None:
861
- cluster_instance = shapes.instance(current_capacity.cluster_instance_name)
862
- else:
863
- cluster_instance = current_capacity.cluster_instance
906
+ @property
907
+ def disk_gib(self) -> int:
908
+ current_cluster_disk_util_gib = (
909
+ self.current_capacity.disk_utilization_gib.mid
910
+ * self.current_capacity.cluster_instance_count.mid
911
+ )
912
+ current_node_disk_gib = (
913
+ self.current_instance.drive.max_size_gib
914
+ if self.current_instance.drive is not None
915
+ else (
916
+ self.current_capacity.cluster_drive.size_gib
917
+ if self.current_capacity.cluster_drive is not None
918
+ else 0
919
+ )
920
+ )
864
921
 
865
- if cluster_instance.drive is not None:
866
- instance_disk_allocated = cluster_instance.drive.max_size_gib
867
- else:
868
- assert current_capacity.cluster_drive is not None, "Drive should not be None"
869
- instance_disk_allocated = current_capacity.cluster_drive.size_gib
922
+ zonal_disk_allocated = (
923
+ current_node_disk_gib * self.current_capacity.cluster_instance_count.mid
924
+ )
925
+ # These are the desired buffers
926
+ disk_buffer = buffer_for_components(
927
+ buffers=self.buffers, components=[BufferComponent.disk]
928
+ )
870
929
 
871
- zonal_disk_allocated = (
872
- instance_disk_allocated * current_capacity.cluster_instance_count.mid
873
- )
930
+ derived_buffer = DerivedBuffers.for_components(
931
+ self.buffers.derived, [BufferComponent.disk]
932
+ )
933
+ required_disk = derived_buffer.calculate_requirement(
934
+ current_usage=current_cluster_disk_util_gib,
935
+ existing_capacity=zonal_disk_allocated,
936
+ desired_buffer_ratio=disk_buffer.ratio,
937
+ )
938
+ return math.ceil(required_disk)
874
939
 
875
- # These are the desired buffers
876
- disk_buffer = buffer_for_components(
877
- buffers=buffers, components=[BufferComponent.disk]
878
- )
940
+ @property
941
+ def network_mbps(self) -> int:
942
+ current_network_utilization = (
943
+ self.current_capacity.network_utilization_mbps.mid
944
+ * self.current_capacity.cluster_instance_count.mid
945
+ )
946
+ zonal_network_allocated = (
947
+ self.current_instance.net_mbps
948
+ * self.current_capacity.cluster_instance_count.mid
949
+ )
879
950
 
880
- scale, preserve = derived_buffer_for_component(buffers.derived, ["storage", "disk"])
881
- # Scale and preserve for the same component should not be passed together.
882
- # If user passes it, then scale will be preferred over preserve.
883
- if scale > 0:
884
- # if the new required disk is less than the current,
885
- # then take no action and return the current disk
886
- return max(
887
- current_disk_utilization * scale * disk_buffer.ratio, zonal_disk_allocated
951
+ # These are the desired buffers
952
+ network_buffer = buffer_for_components(
953
+ buffers=self.buffers, components=[BufferComponent.network]
954
+ )
955
+ derived_buffer = DerivedBuffers.for_components(
956
+ self.buffers.derived, [BufferComponent.network]
888
957
  )
889
- if preserve:
890
- # preserve the current disk size for the zone
891
- return zonal_disk_allocated
892
958
 
893
- return current_disk_utilization * disk_buffer.ratio
959
+ return math.ceil(
960
+ derived_buffer.calculate_requirement(
961
+ current_usage=current_network_utilization,
962
+ existing_capacity=zonal_network_allocated,
963
+ desired_buffer_ratio=network_buffer.ratio,
964
+ )
965
+ )
894
966
 
895
967
 
896
968
  def zonal_requirements_from_current(
@@ -901,20 +973,25 @@ def zonal_requirements_from_current(
901
973
  ) -> CapacityRequirement:
902
974
  if current_cluster is not None and current_cluster.zonal[0] is not None:
903
975
  current_capacity: CurrentClusterCapacity = current_cluster.zonal[0]
904
- needed_cores = normalize_cores(
905
- get_cores_from_current_capacity(current_capacity, buffers, instance),
976
+
977
+ # Adjust the CPUs (vCPU + cores) based on generation / instance type
978
+ requirement = RequirementFromCurrentCapacity(
979
+ current_capacity=current_capacity,
980
+ buffers=buffers,
981
+ )
982
+ normalized_cpu = _normalize_cpu(
983
+ requirement.cpu(instance),
906
984
  instance,
907
985
  reference_shape,
908
986
  )
909
- needed_network_mbps = get_network_from_current_capacity(
910
- current_capacity, buffers
911
- )
912
- needed_memory_gib = get_memory_from_current_capacity(current_capacity, buffers)
913
- needed_disk_gib = get_disk_from_current_capacity(current_capacity, buffers)
987
+
988
+ needed_network_mbps = requirement.network_mbps
989
+ needed_disk_gib = requirement.disk_gib
990
+ needed_memory_gib = requirement.mem_gib
914
991
 
915
992
  return CapacityRequirement(
916
993
  requirement_type="zonal-capacity",
917
- cpu_cores=certain_int(needed_cores),
994
+ cpu_cores=certain_int(normalized_cpu),
918
995
  mem_gib=certain_float(needed_memory_gib),
919
996
  disk_gib=certain_float(needed_disk_gib),
920
997
  network_mbps=certain_float(needed_network_mbps),
@@ -35,7 +35,7 @@ from service_capacity_modeling.interface import ServiceCapacity
35
35
  from service_capacity_modeling.models import CapacityModel
36
36
  from service_capacity_modeling.models.common import buffer_for_components
37
37
  from service_capacity_modeling.models.common import compute_stateful_zone
38
- from service_capacity_modeling.models.common import derived_buffer_for_component
38
+ from service_capacity_modeling.models.common import DerivedBuffers
39
39
  from service_capacity_modeling.models.common import get_effective_disk_per_node_gib
40
40
  from service_capacity_modeling.models.common import network_services
41
41
  from service_capacity_modeling.models.common import normalize_cores
@@ -181,7 +181,9 @@ def _zonal_requirement_for_new_cluster(
181
181
  )
182
182
 
183
183
 
184
- def _estimate_cassandra_requirement( # pylint: disable=too-many-positional-arguments
184
+ # pylint: disable=too-many-locals
185
+ # pylint: disable=too-many-positional-arguments
186
+ def _estimate_cassandra_requirement(
185
187
  instance: Instance,
186
188
  desires: CapacityDesires,
187
189
  working_set: float,
@@ -205,19 +207,22 @@ def _estimate_cassandra_requirement( # pylint: disable=too-many-positional-argu
205
207
  # If the cluster is already provisioned
206
208
  if current_capacity and desires.current_clusters is not None:
207
209
  capacity_requirement = zonal_requirements_from_current(
208
- desires.current_clusters, desires.buffers, instance, reference_shape
210
+ desires.current_clusters,
211
+ desires.buffers,
212
+ instance,
213
+ reference_shape,
209
214
  )
210
- disk_scale, _ = derived_buffer_for_component(
211
- desires.buffers.derived, ["storage", "disk"]
215
+ disk_derived_buffer = DerivedBuffers.for_components(
216
+ desires.buffers.derived, [BufferComponent.disk]
212
217
  )
213
218
  disk_used_gib = (
214
219
  current_capacity.disk_utilization_gib.mid
215
220
  * current_capacity.cluster_instance_count.mid
216
- * (disk_scale or 1)
217
- )
218
- _, memory_preserve = derived_buffer_for_component(
219
- desires.buffers.derived, ["storage", "memory"]
221
+ * disk_derived_buffer.scale
220
222
  )
223
+ memory_preserve = DerivedBuffers.for_components(
224
+ desires.buffers.derived, [BufferComponent.memory]
225
+ ).preserve
221
226
  else:
222
227
  # If the cluster is not yet provisioned
223
228
  capacity_requirement = _zonal_requirement_for_new_cluster(
@@ -33,13 +33,10 @@ from service_capacity_modeling.interface import Requirements
33
33
  from service_capacity_modeling.models import CapacityModel
34
34
  from service_capacity_modeling.models.common import buffer_for_components
35
35
  from service_capacity_modeling.models.common import compute_stateful_zone
36
- from service_capacity_modeling.models.common import get_cores_from_current_capacity
37
- from service_capacity_modeling.models.common import get_disk_from_current_capacity
38
36
  from service_capacity_modeling.models.common import get_effective_disk_per_node_gib
39
- from service_capacity_modeling.models.common import get_memory_from_current_capacity
40
- from service_capacity_modeling.models.common import get_network_from_current_capacity
41
37
  from service_capacity_modeling.models.common import network_services
42
38
  from service_capacity_modeling.models.common import normalize_cores
39
+ from service_capacity_modeling.models.common import RequirementFromCurrentCapacity
43
40
  from service_capacity_modeling.models.common import simple_network_mbps
44
41
  from service_capacity_modeling.models.common import sqrt_staffed_cores
45
42
  from service_capacity_modeling.models.common import working_set_from_drive_and_slo
@@ -104,25 +101,20 @@ def calculate_vitals_for_capacity_planner(
104
101
  )
105
102
  if not current_capacity:
106
103
  return needed_cores, needed_network_mbps, needed_memory_gib, needed_disk_gib
104
+ requirements = RequirementFromCurrentCapacity(
105
+ current_capacity=current_capacity,
106
+ buffers=desires.buffers,
107
+ )
107
108
  needed_cores = normalize_cores(
108
- core_count=get_cores_from_current_capacity(
109
- current_capacity, desires.buffers, instance
110
- ),
109
+ core_count=requirements.cpu(instance_candidate=instance),
111
110
  target_shape=instance,
112
111
  reference_shape=current_capacity.cluster_instance,
113
112
  )
114
- needed_network_mbps = get_network_from_current_capacity(
115
- current_capacity, desires.buffers
116
- )
117
- needed_memory_gib = get_memory_from_current_capacity(
118
- current_capacity, desires.buffers
113
+ needed_network_mbps = requirements.network_mbps
114
+ needed_disk_gib = (
115
+ requirements.disk_gib if current_capacity.cluster_drive is not None else 0.0
119
116
  )
120
- if current_capacity.cluster_drive is None:
121
- needed_disk_gib = 0.0
122
- else:
123
- needed_disk_gib = get_disk_from_current_capacity(
124
- current_capacity, desires.buffers
125
- )
117
+ needed_memory_gib = requirements.mem_gib
126
118
  return needed_cores, needed_network_mbps, needed_memory_gib, needed_disk_gib
127
119
 
128
120
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: service-capacity-modeling
3
- Version: 0.3.74
3
+ Version: 0.3.75
4
4
  Summary: Contains utilities for modeling capacity for pluggable workloads
5
5
  Author: Joseph Lynch
6
6
  Author-email: josephl@netflix.com
@@ -1,6 +1,6 @@
1
1
  service_capacity_modeling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  service_capacity_modeling/capacity_planner.py,sha256=B6e0esOAvV6qMkEeLIO9rEveTftRG_Ut_d9gYgSIM0w,31914
3
- service_capacity_modeling/interface.py,sha256=-nwCW67LBoxR9I_FZxkMmB0n4VS21gqrdfDCskZ0w7E,37493
3
+ service_capacity_modeling/interface.py,sha256=WLCVqrRgH4Vz3YhzZAct0QMXrBOow7v5ipZLoJ7AfSc,38826
4
4
  service_capacity_modeling/stats.py,sha256=8HIPwVnmvbauBwXhn6vbNYO7-CzWPuymnq0eX7ZA1_w,5849
5
5
  service_capacity_modeling/hardware/__init__.py,sha256=kzIHnIymwnf4qQYDpfIChIAxTF8b87XtnBg1TwF_J9E,8974
6
6
  service_capacity_modeling/hardware/profiles/__init__.py,sha256=7-y3JbCBkgzaAjFla2RIymREcImdZ51HTl3yn3vzoGw,1602
@@ -46,19 +46,19 @@ service_capacity_modeling/hardware/profiles/shapes/aws/manual_drives.json,sha256
46
46
  service_capacity_modeling/hardware/profiles/shapes/aws/manual_instances.json,sha256=-_jxyQgmwKe5JnbfhMD9xDCq0sy7z2fdZn7Fu76IUkk,12457
47
47
  service_capacity_modeling/hardware/profiles/shapes/aws/manual_services.json,sha256=h63675KKmu5IrI3BORDN8fiAqLjAyYHArErKbC7-T30,776
48
48
  service_capacity_modeling/models/__init__.py,sha256=XK7rTBW8ZXQY5L9Uy2FwjuFN_KBW3hKw7IrhG1piajs,13567
49
- service_capacity_modeling/models/common.py,sha256=oF1WVhd6kXFvKNs7zzkms25U1Si_iqCn48TptyxWd0E,34128
49
+ service_capacity_modeling/models/common.py,sha256=ntBx5Rku4UfJiKwZ0tkv75gXBzUlGT6hOUTLvENlElA,36534
50
50
  service_capacity_modeling/models/headroom_strategy.py,sha256=QIkP_K_tK2EGAjloaGfXeAPH5M0UDCN8FlAtwV9xxTA,651
51
51
  service_capacity_modeling/models/utils.py,sha256=WosEEg4o1_WSbTb5mL-M1v8JuWJgvS2oWvnDS3qNz3k,2662
52
52
  service_capacity_modeling/models/org/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  service_capacity_modeling/models/org/netflix/__init__.py,sha256=m7IaQbo85NEbDvfoPJREIznpzg0YHTCrKP5C1GvnOYM,2378
54
54
  service_capacity_modeling/models/org/netflix/aurora.py,sha256=Mi9zd48k64GkKIjAs3J1S2qThguNvyWIy2dUmhwrVhc,12883
55
- service_capacity_modeling/models/org/netflix/cassandra.py,sha256=QdSurxiv9XiCK7z6Omn6hjZf0rHleDfMvn3-JSpr5rA,38548
55
+ service_capacity_modeling/models/org/netflix/cassandra.py,sha256=MnTD7X3-mm05LtM_CjWatIVyeMCfPqVsxszTDb8s7ao,38632
56
56
  service_capacity_modeling/models/org/netflix/counter.py,sha256=hOVRRCgCPU-A5TdLKQXc_mWTQpkKOWRNjOeECdDP7kA,9205
57
57
  service_capacity_modeling/models/org/netflix/crdb.py,sha256=ELIbxwfNsJcEkNGW7qtz0SEzt3Vj6wj8QL5QQeebIlo,20635
58
58
  service_capacity_modeling/models/org/netflix/ddb.py,sha256=GDoXVIpDDY6xDB0dsiaz7RAPPj-qffTrM9N6w5-5ndg,26311
59
59
  service_capacity_modeling/models/org/netflix/elasticsearch.py,sha256=AfyqfC4Y_QDyvYLBbeq8_ReM9q54RUNrZkOsSBjBgIc,25085
60
60
  service_capacity_modeling/models/org/netflix/entity.py,sha256=M0vzwhf8UAbVxnXspAkN4GEbq3rix6yoky6W2oDG6a0,8648
61
- service_capacity_modeling/models/org/netflix/evcache.py,sha256=70lgaRgNwJH84o6JVoUDplkCi4v-WzEX3nxVagoJjDc,25775
61
+ service_capacity_modeling/models/org/netflix/evcache.py,sha256=mLSoecrXwwfrt9ZRu1LZ2po8lD50orQAFnpvW0YTmI8,25396
62
62
  service_capacity_modeling/models/org/netflix/graphkv.py,sha256=iS5QDDv9_hNY6nIgdL-umB439qP7-jN-n6_Tl6d-ZSo,8557
63
63
  service_capacity_modeling/models/org/netflix/iso_date_math.py,sha256=CPGHLmbGeNqkcYcmCkLKhPZcAU-yTJ2HjvuXdnNyCYc,996
64
64
  service_capacity_modeling/models/org/netflix/kafka.py,sha256=MDHaht5cWsOJ113uMl6nQ7nllSATrlBCQ-TXLkqMWEk,25466
@@ -75,9 +75,9 @@ service_capacity_modeling/tools/auto_shape.py,sha256=Pe9a7vbFxqIy8eL8ssENTu9FNnF
75
75
  service_capacity_modeling/tools/fetch_pricing.py,sha256=JkgJPTE0SVj8sdGQvo0HN-Hdv3nfA2tu7C_Arad5aX8,3762
76
76
  service_capacity_modeling/tools/generate_missing.py,sha256=XqUs54CPfli4XtK0rEiFKqDvpwCiMAD8wrl7fAxpYHs,3062
77
77
  service_capacity_modeling/tools/instance_families.py,sha256=5Y4_aJ5ML-JPzwaWcS_caUeZ28CmUVoqjYYLaRl01vg,9148
78
- service_capacity_modeling-0.3.74.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
79
- service_capacity_modeling-0.3.74.dist-info/METADATA,sha256=Q32PjFEqL_3-U-JUgIMQeW8hz4CtAIqw3m2H39gndYU,10214
80
- service_capacity_modeling-0.3.74.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
81
- service_capacity_modeling-0.3.74.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
82
- service_capacity_modeling-0.3.74.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
83
- service_capacity_modeling-0.3.74.dist-info/RECORD,,
78
+ service_capacity_modeling-0.3.75.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
79
+ service_capacity_modeling-0.3.75.dist-info/METADATA,sha256=r_DDZjFo54y1QWSRY7HfJ4wWwypgxT31-oKXeCBLoMA,10214
80
+ service_capacity_modeling-0.3.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
81
+ service_capacity_modeling-0.3.75.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
82
+ service_capacity_modeling-0.3.75.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
83
+ service_capacity_modeling-0.3.75.dist-info/RECORD,,