diff --git a/crates/stackable-operator/src/builder/pod/resources.rs b/crates/stackable-operator/src/builder/pod/resources.rs index 040231457..6ed6ef218 100644 --- a/crates/stackable-operator/src/builder/pod/resources.rs +++ b/crates/stackable-operator/src/builder/pod/resources.rs @@ -7,8 +7,7 @@ use tracing::warn; use crate::{ commons::resources::ResourceRequirementsType, - cpu::{self, CpuQuantity}, - memory::{self, MemoryQuantity}, + quantity::{CpuQuantity, MemoryQuantity, ParseQuantityError}, }; const RESOURCE_DENYLIST: &[&str] = &["cpu", "memory"]; @@ -54,7 +53,7 @@ impl ResourceRequirementsBuilder<(), CL, MR, ML> { self, request: impl Into, factor: f32, - ) -> cpu::Result> { + ) -> Result, ParseQuantityError> { let request = CpuQuantity::from_str(&request.into())?; let limit = request * factor; @@ -124,7 +123,7 @@ impl ResourceRequirementsBuilder { self, request: impl Into, factor: f32, - ) -> memory::Result> { + ) -> Result, ParseQuantityError> { let request = MemoryQuantity::from_str(&request.into())?; let limit = request * factor; diff --git a/crates/stackable-operator/src/commons/resources.rs b/crates/stackable-operator/src/commons/resources.rs index fbaaf9f1e..f08682724 100644 --- a/crates/stackable-operator/src/commons/resources.rs +++ b/crates/stackable-operator/src/commons/resources.rs @@ -67,30 +67,34 @@ //! shared_storage: PvcConfig, //! } //! ``` +use std::{collections::BTreeMap, fmt::Debug}; -use crate::{ - config::{ - fragment::{Fragment, FromFragment}, - merge::Merge, - }, - cpu::CpuQuantity, - memory::MemoryQuantity, -}; use educe::Educe; -use k8s_openapi::api::core::v1::{ - Container, PersistentVolumeClaim, PersistentVolumeClaimSpec, PodSpec, ResourceRequirements, - VolumeResourceRequirements, +use k8s_openapi::{ + api::core::v1::{ + Container, PersistentVolumeClaim, PersistentVolumeClaimSpec, PodSpec, ResourceRequirements, + VolumeResourceRequirements, + }, + apimachinery::pkg::{ + api::resource::Quantity, + apis::meta::v1::{LabelSelector, ObjectMeta}, + }, }; -use k8s_openapi::apimachinery::pkg::api::resource::Quantity; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Snafu}; -use std::{collections::BTreeMap, fmt::Debug}; use strum::Display; -pub const LIMIT_REQUEST_RATIO_CPU: f32 = 5.0; -pub const LIMIT_REQUEST_RATIO_MEMORY: f32 = 1.0; +use crate::{ + config::{ + fragment::{Fragment, FromFragment}, + merge::Merge, + }, + quantity::{CpuQuantity, MemoryQuantity, ParseQuantityError}, +}; + +pub const LIMIT_REQUEST_RATIO_CPU: f64 = 5.0; +pub const LIMIT_REQUEST_RATIO_MEMORY: f64 = 1.0; type Result = std::result::Result; @@ -109,20 +113,20 @@ pub enum Error { LimitToRequestRatioExceeded { container_name: String, resource_key: String, - allowed_ration: f32, + allowed_ration: f64, }, #[snafu(display("failed to convert Quantity to CpuQuantity for cpu limit"))] - CpuLimit { source: crate::cpu::Error }, + CpuLimit { source: ParseQuantityError }, #[snafu(display("failed to convert Quantity to CpuQuantity for cpu request"))] - CpuRequest { source: crate::cpu::Error }, + CpuRequest { source: ParseQuantityError }, #[snafu(display("failed to convert Quantity to MemoryQuantity for memory limit"))] - MemoryLimit { source: crate::memory::Error }, + MemoryLimit { source: ParseQuantityError }, #[snafu(display("failed to convert Quantity to MemoryQuantity for memory request"))] - MemoryRequest { source: crate::memory::Error }, + MemoryRequest { source: ParseQuantityError }, } /// Resource usage is configured here, this includes CPU usage, memory usage and disk storage @@ -429,7 +433,7 @@ pub trait ResourceRequirementsExt { &self, resource: &ComputeResource, // We did choose a f32 instead of a usize here, as LimitRange ratios can be a floating point (Quantity - e.g. 1500m) - ratio: f32, + ratio: f64, ) -> Result<()>; } @@ -459,7 +463,7 @@ impl ResourceRequirementsExt for Container { Ok(()) } - fn check_limit_to_request_ratio(&self, resource: &ComputeResource, ratio: f32) -> Result<()> { + fn check_limit_to_request_ratio(&self, resource: &ComputeResource, ratio: f64) -> Result<()> { let limit = self .resources .as_ref() @@ -511,7 +515,7 @@ impl ResourceRequirementsExt for PodSpec { Ok(()) } - fn check_limit_to_request_ratio(&self, resource: &ComputeResource, ratio: f32) -> Result<()> { + fn check_limit_to_request_ratio(&self, resource: &ComputeResource, ratio: f64) -> Result<()> { for container in &self.containers { container.check_limit_to_request_ratio(resource, ratio)?; } diff --git a/crates/stackable-operator/src/cpu.rs b/crates/stackable-operator/src/cpu.rs deleted file mode 100644 index 45bb421c9..000000000 --- a/crates/stackable-operator/src/cpu.rs +++ /dev/null @@ -1,307 +0,0 @@ -use std::{ - fmt::Display, - iter::Sum, - ops::{Add, AddAssign, Div, Mul, MulAssign}, - str::FromStr, -}; - -use k8s_openapi::apimachinery::pkg::api::resource::Quantity; -use serde::{de::Visitor, Deserialize, Serialize}; -use snafu::{ResultExt, Snafu}; - -pub type Result = std::result::Result; - -#[derive(Debug, PartialEq, Snafu)] -pub enum Error { - #[snafu(display("unsupported precision {value:?}. Kubernetes doesn't allow you to specify CPU resources with a precision finer than 1m. Because of this, it's useful to specify CPU units less than 1.0 or 1000m using the milliCPU form; for example, 5m rather than 0.005"))] - UnsupportedCpuQuantityPrecision { value: String }, - - #[snafu(display("invalid cpu integer quantity {value:?}"))] - InvalidCpuIntQuantity { - source: std::num::ParseIntError, - value: String, - }, - - #[snafu(display("invalid cpu float quantity {value:?}"))] - InvalidCpuFloatQuantity { - source: std::num::ParseFloatError, - value: String, - }, -} - -/// A representation of CPU quantities with milli precision. -/// Supports conversion from [`Quantity`]. -/// -/// A CPU quantity cannot have a precision finer than 'm' (millis) in Kubernetes. -/// So we use that as our internal representation (see: -/// ``). -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct CpuQuantity { - millis: usize, -} - -impl CpuQuantity { - pub const fn from_millis(millis: usize) -> Self { - Self { millis } - } - - pub fn as_cpu_count(&self) -> f32 { - self.millis as f32 / 1000. - } - - pub const fn as_milli_cpus(&self) -> usize { - self.millis - } -} - -impl Serialize for CpuQuantity { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for CpuQuantity { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct CpuQuantityVisitor; - - impl<'de> Visitor<'de> for CpuQuantityVisitor { - type Value = CpuQuantity; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a valid CPU quantity") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - CpuQuantity::from_str(v).map_err(serde::de::Error::custom) - } - } - - deserializer.deserialize_str(CpuQuantityVisitor) - } -} - -impl Display for CpuQuantity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.millis < 1000 { - true => write!(f, "{}m", self.millis), - false => write!(f, "{}", self.as_cpu_count()), - } - } -} - -impl FromStr for CpuQuantity { - type Err = Error; - - /// Only two formats can be parsed: - /// - /// - {usize}m - /// - {f32} - /// - /// For the float, only milli-precision is supported. Using more precise - /// values will trigger an error, and using any other unit than 'm' or None - /// will also trigger an error. - fn from_str(q: &str) -> Result { - let start_of_unit = q.find(|c: char| c != '.' && !c.is_numeric()); - if let Some(start_of_unit) = start_of_unit { - let (value, unit) = q.split_at(start_of_unit); - if unit != "m" { - return UnsupportedCpuQuantityPrecisionSnafu { - value: q.to_owned(), - } - .fail(); - } - let cpu_millis: usize = value.parse().context(InvalidCpuIntQuantitySnafu { - value: q.to_owned(), - })?; - Ok(Self::from_millis(cpu_millis)) - } else { - let cpus = q.parse::().context(InvalidCpuFloatQuantitySnafu { - value: q.to_owned(), - })?; - let millis_float = cpus * 1000.; - if millis_float != millis_float.round() { - return UnsupportedCpuQuantityPrecisionSnafu { - value: q.to_owned(), - } - .fail(); - } - Ok(Self::from_millis(millis_float as usize)) - } - } -} - -impl From for Quantity { - fn from(quantity: CpuQuantity) -> Self { - Self::from(&quantity) - } -} - -impl From<&CpuQuantity> for Quantity { - fn from(quantity: &CpuQuantity) -> Self { - Quantity(format!("{}", quantity.as_cpu_count())) - } -} - -impl TryFrom<&Quantity> for CpuQuantity { - type Error = Error; - - fn try_from(q: &Quantity) -> Result { - Self::from_str(&q.0) - } -} - -impl TryFrom for CpuQuantity { - type Error = Error; - - fn try_from(q: Quantity) -> Result { - Self::try_from(&q) - } -} - -impl Add for CpuQuantity { - type Output = CpuQuantity; - - fn add(self, rhs: CpuQuantity) -> Self::Output { - CpuQuantity::from_millis(self.millis + rhs.millis) - } -} - -impl AddAssign for CpuQuantity { - fn add_assign(&mut self, rhs: CpuQuantity) { - self.millis += rhs.millis; - } -} - -impl Mul for CpuQuantity { - type Output = CpuQuantity; - - fn mul(self, rhs: usize) -> Self::Output { - Self { - millis: self.millis * rhs, - } - } -} - -impl MulAssign for CpuQuantity { - fn mul_assign(&mut self, rhs: usize) { - self.millis *= rhs; - } -} - -impl Mul for CpuQuantity { - type Output = CpuQuantity; - - fn mul(self, rhs: f32) -> Self::Output { - Self { - millis: (self.millis as f32 * rhs) as usize, - } - } -} - -impl Div for CpuQuantity { - type Output = f32; - - fn div(self, rhs: CpuQuantity) -> Self::Output { - self.millis as f32 / rhs.millis as f32 - } -} - -impl MulAssign for CpuQuantity { - fn mul_assign(&mut self, rhs: f32) { - self.millis = (self.millis as f32 * rhs) as usize; - } -} - -impl Sum for CpuQuantity { - fn sum>(iter: I) -> Self { - iter.fold(CpuQuantity { millis: 0 }, CpuQuantity::add) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::*; - - #[rstest] - #[case("1", 1000)] - #[case("1000m", 1000)] - #[case("500m", 500)] - #[case("2.5", 2500)] - #[case("0.2", 200)] - #[case("0.02", 20)] - #[case("0.002", 2)] - fn from_str_pass(#[case] input: &str, #[case] expected: usize) { - let got = CpuQuantity::from_str(input).unwrap(); - assert_eq!(got.as_milli_cpus(), expected); - } - - #[rstest] - #[case("1.11111")] - #[case("1000.1m")] - #[case("500k")] - #[case("0.0002")] - fn from_str_fail(#[case] input: &str) { - let result = CpuQuantity::from_str(input); - assert!(result.is_err()); - } - - #[rstest] - #[case(CpuQuantity::from_millis(10000), "10")] - #[case(CpuQuantity::from_millis(1500), "1.5")] - #[case(CpuQuantity::from_millis(999), "999m")] - #[case(CpuQuantity::from_millis(500), "500m")] - #[case(CpuQuantity::from_millis(100), "100m")] - #[case(CpuQuantity::from_millis(2000), "2")] - #[case(CpuQuantity::from_millis(1000), "1")] - fn to_string(#[case] cpu: CpuQuantity, #[case] expected: &str) { - assert_eq!(cpu.to_string(), expected) - } - - #[rstest] - #[case(CpuQuantity::from_millis(10000), "cpu: '10'\n")] - #[case(CpuQuantity::from_millis(1500), "cpu: '1.5'\n")] - #[case(CpuQuantity::from_millis(999), "cpu: 999m\n")] - #[case(CpuQuantity::from_millis(500), "cpu: 500m\n")] - #[case(CpuQuantity::from_millis(100), "cpu: 100m\n")] - #[case(CpuQuantity::from_millis(2000), "cpu: '2'\n")] - #[case(CpuQuantity::from_millis(1000), "cpu: '1'\n")] - fn serialize(#[case] cpu: CpuQuantity, #[case] expected: &str) { - #[derive(Serialize)] - struct Cpu { - cpu: CpuQuantity, - } - - let cpu = Cpu { cpu }; - let output = serde_yaml::to_string(&cpu).unwrap(); - - assert_eq!(output, expected); - } - - #[rstest] - #[case("cpu: '10'", CpuQuantity::from_millis(10000))] - #[case("cpu: '1.5'", CpuQuantity::from_millis(1500))] - #[case("cpu: 999m", CpuQuantity::from_millis(999))] - #[case("cpu: 500m", CpuQuantity::from_millis(500))] - #[case("cpu: 100m", CpuQuantity::from_millis(100))] - #[case("cpu: 2", CpuQuantity::from_millis(2000))] - #[case("cpu: 1", CpuQuantity::from_millis(1000))] - fn deserialize(#[case] input: &str, #[case] expected: CpuQuantity) { - #[derive(Deserialize)] - struct Cpu { - cpu: CpuQuantity, - } - - let cpu: Cpu = serde_yaml::from_str(input).unwrap(); - assert_eq!(cpu.cpu, expected); - } -} diff --git a/crates/stackable-operator/src/lib.rs b/crates/stackable-operator/src/lib.rs index 50567fb88..77ae3c8fd 100644 --- a/crates/stackable-operator/src/lib.rs +++ b/crates/stackable-operator/src/lib.rs @@ -4,17 +4,16 @@ pub mod client; pub mod cluster_resources; pub mod commons; pub mod config; -pub mod cpu; pub mod crd; pub mod helm; pub mod iter; pub mod kvp; pub mod logging; -pub mod memory; pub mod namespace; pub mod pod_utils; pub mod product_config_utils; pub mod product_logging; +pub mod quantity; pub mod role_utils; pub mod status; pub mod time; diff --git a/crates/stackable-operator/src/memory.rs b/crates/stackable-operator/src/memory.rs deleted file mode 100644 index 8dfef7bee..000000000 --- a/crates/stackable-operator/src/memory.rs +++ /dev/null @@ -1,712 +0,0 @@ -//! Utilities for converting Kubernetes quantities to Java heap settings. -//! Since Java heap sizes are a subset of Kubernetes quantities, the conversion -//! might lose precision or fail completely. In addition: -//! -//! - decimal quantities are not supported ("2G" is invalid) -//! - units are case sensitive ("2gi" is invalid) -//! - exponential notation is not supported. -//! -//! For details on Kubernetes quantities see: - -use k8s_openapi::apimachinery::pkg::api::resource::Quantity; -use serde::{de::Visitor, Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; - -use std::{ - fmt::Display, - iter::Sum, - ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}, - str::FromStr, -}; - -pub type Result = std::result::Result; - -#[derive(Debug, PartialEq, Snafu)] -pub enum Error { - #[snafu(display("cannot convert quantity {value:?} to Java heap"))] - CannotConvertToJavaHeap { value: String }, - - #[snafu(display( - "cannot convert quantity {value:?} to Java heap value with unit {target_unit:?}" - ))] - CannotConvertToJavaHeapValue { value: String, target_unit: String }, - - #[snafu(display("cannot scale down from kilobytes"))] - CannotScaleDownMemoryUnit, - - #[snafu(display("invalid quantity {value:?}"))] - InvalidQuantity { - source: std::num::ParseFloatError, - value: String, - }, - - #[snafu(display("invalid quantity unit {value:?}"))] - InvalidQuantityUnit { value: String }, - - #[snafu(display("no quantity unit provided for {value:?}"))] - NoQuantityUnit { value: String }, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] -pub enum BinaryMultiple { - Kibi, - Mebi, - Gibi, - Tebi, - Pebi, - Exbi, -} - -impl BinaryMultiple { - pub fn to_java_memory_unit(&self) -> String { - match self { - BinaryMultiple::Kibi => "k".to_string(), - BinaryMultiple::Mebi => "m".to_string(), - BinaryMultiple::Gibi => "g".to_string(), - BinaryMultiple::Tebi => "t".to_string(), - BinaryMultiple::Pebi => "p".to_string(), - BinaryMultiple::Exbi => "e".to_string(), - } - } - - /// The exponential scale factor used when converting a `BinaryMultiple` - /// to another one. - const fn exponential_scale_factor(&self) -> i32 { - match self { - BinaryMultiple::Kibi => 1, - BinaryMultiple::Mebi => 2, - BinaryMultiple::Gibi => 3, - BinaryMultiple::Tebi => 4, - BinaryMultiple::Pebi => 5, - BinaryMultiple::Exbi => 6, - } - } - - pub const fn get_smallest() -> Self { - Self::Kibi - } -} - -impl FromStr for BinaryMultiple { - type Err = Error; - - fn from_str(q: &str) -> Result { - match q { - "Ki" => Ok(BinaryMultiple::Kibi), - "Mi" => Ok(BinaryMultiple::Mebi), - "Gi" => Ok(BinaryMultiple::Gibi), - "Ti" => Ok(BinaryMultiple::Tebi), - "Pi" => Ok(BinaryMultiple::Pebi), - "Ei" => Ok(BinaryMultiple::Exbi), - _ => Err(Error::InvalidQuantityUnit { - value: q.to_string(), - }), - } - } -} - -impl Display for BinaryMultiple { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let out = match self { - BinaryMultiple::Kibi => "Ki", - BinaryMultiple::Mebi => "Mi", - BinaryMultiple::Gibi => "Gi", - BinaryMultiple::Tebi => "Ti", - BinaryMultiple::Pebi => "Pi", - BinaryMultiple::Exbi => "Ei", - }; - - out.fmt(f) - } -} - -/// Convert a (memory) [`Quantity`] to Java heap settings. -/// -/// Quantities are usually passed on to container resources while Java heap -/// sizes need to be scaled accordingly. This implements a very simple heuristic -/// to ensure that: -/// -/// - the quantity unit has been mapped to a java supported heap unit. Java only -/// supports up to Gibibytes while K8S quantities can be expressed in Exbibytes. -/// - the heap size has a non-zero value. -/// -/// Fails if it can't enforce the above restrictions. -#[deprecated( - since = "0.33.0", - note = "use \"-Xmx\" + MemoryQuantity::try_from(quantity).scale_to(unit).format_for_java()" -)] -pub fn to_java_heap(q: &Quantity, factor: f32) -> Result { - let scaled = (q.0.parse::()? * factor).scale_for_java(); - if scaled.value < 1.0 { - Err(Error::CannotConvertToJavaHeap { - value: q.0.to_owned(), - }) - } else { - Ok(format!( - "-Xmx{:.0}{}", - scaled.value, - scaled.unit.to_java_memory_unit() - )) - } -} - -/// Convert a (memory) [`Quantity`] to a raw Java heap value of the desired `target_unit`. -/// -/// Quantities are usually passed on to container resources while Java heap -/// sizes need to be scaled accordingly. The raw heap value is converted to the -/// specified `target_unit` (this conversion is done even if specified a unit -/// greater that Gibibytes. It is not recommended to scale to anything bigger -/// than Gibibytes. This implements a very simple heuristic to ensure that: -/// -/// - the quantity unit has been mapped to a java supported heap unit. Java only -/// supports up to Gibibytes while K8S quantities can be expressed in Exbibytes. -/// - the heap size has a non-zero value. -/// -/// Fails if it can't enforce the above restrictions. -#[deprecated( - since = "0.33.0", - note = "use (MemoryQuantity::try_from(quantity).scale_to(target_unit) * factor)" -)] -pub fn to_java_heap_value(q: &Quantity, factor: f32, target_unit: BinaryMultiple) -> Result { - let scaled = (q.0.parse::()? * factor) - .scale_for_java() - .scale_to(target_unit); - - if scaled.value < 1.0 { - Err(Error::CannotConvertToJavaHeapValue { - value: q.0.to_owned(), - target_unit: target_unit.to_string(), - }) - } else { - Ok(scaled.value as u32) - } -} - -/// Parsed representation of a K8s memory/storage resource limit. -#[derive(Clone, Copy, Debug)] -pub struct MemoryQuantity { - pub value: f32, - pub unit: BinaryMultiple, -} - -impl MemoryQuantity { - pub const fn from_gibi(gibi: f32) -> Self { - Self { - value: gibi, - unit: BinaryMultiple::Gibi, - } - } - - pub const fn from_mebi(mebi: f32) -> Self { - Self { - value: mebi, - unit: BinaryMultiple::Mebi, - } - } - - /// Scales down the unit to GB if it is TB or bigger. - /// Leaves the quantity unchanged otherwise. - fn scale_to_at_most_gb(&self) -> Self { - match self.unit { - BinaryMultiple::Kibi => *self, - BinaryMultiple::Mebi => *self, - BinaryMultiple::Gibi => *self, - BinaryMultiple::Tebi => self.scale_to(BinaryMultiple::Gibi), - BinaryMultiple::Pebi => self.scale_to(BinaryMultiple::Gibi), - BinaryMultiple::Exbi => self.scale_to(BinaryMultiple::Gibi), - } - } - - /// Scale down the unit by one order of magnitude, i.e. GB to MB. - fn scale_down_unit(&self) -> Result { - match self.unit { - BinaryMultiple::Kibi => Err(Error::CannotScaleDownMemoryUnit), - BinaryMultiple::Mebi => Ok(self.scale_to(BinaryMultiple::Kibi)), - BinaryMultiple::Gibi => Ok(self.scale_to(BinaryMultiple::Mebi)), - BinaryMultiple::Tebi => Ok(self.scale_to(BinaryMultiple::Gibi)), - BinaryMultiple::Pebi => Ok(self.scale_to(BinaryMultiple::Tebi)), - BinaryMultiple::Exbi => Ok(self.scale_to(BinaryMultiple::Pebi)), - } - } - - /// Floors the value of this MemoryQuantity. - pub fn floor(&self) -> Self { - Self { - value: self.value.floor(), - unit: self.unit, - } - } - - /// Ceils the value of this MemoryQuantity. - pub fn ceil(&self) -> Self { - Self { - value: self.value.ceil(), - unit: self.unit, - } - } - - /// If the MemoryQuantity value is smaller than 1 (starts with a zero), convert it to a smaller - /// unit until the non fractional part of the value is not zero anymore. - /// This can fail if the quantity is smaller than 1kB. - fn ensure_no_zero(&self) -> Result { - if self.value < 1. { - self.scale_down_unit()?.ensure_no_zero() - } else { - Ok(*self) - } - } - - /// Ensure that the value of this MemoryQuantity is a natural number (not a float). - /// This is done by picking smaller units until the fractional part is smaller than the tolerated - /// rounding loss, and then rounding down. - /// This can fail if the tolerated rounding loss is less than 1kB. - fn ensure_integer(&self, tolerated_rounding_loss: MemoryQuantity) -> Result { - let fraction_memory = MemoryQuantity { - value: self.value.fract(), - unit: self.unit, - }; - if fraction_memory < tolerated_rounding_loss { - Ok(self.floor()) - } else { - self.scale_down_unit()? - .ensure_integer(tolerated_rounding_loss) - } - } - - /// Returns a value like '1355m' or '2g'. Always returns natural numbers with either 'k', 'm' or 'g', - /// even if the values is multiple Terabytes or more. - /// The original quantity may be rounded down to achive a compact, natural number representation. - /// This rounding may cause the quantity to shrink by up to 20MB. - /// Useful to set memory quantities as JVM paramters. - pub fn format_for_java(&self) -> Result { - let m = self - .scale_to_at_most_gb() // Java Heap only supports specifying kb, mb or gb - .ensure_no_zero()? // We don't want 0.9 or 0.2 - .ensure_integer(MemoryQuantity::from_mebi(20.))?; // Java only accepts integers not floats - Ok(format!("{}{}", m.value, m.unit.to_java_memory_unit())) - } - - /// Scales the unit to a value supported by Java and may even scale - /// further down, in an attempt to avoid having zero sizes or losing too - /// much precision. - fn scale_for_java(&self) -> Self { - let (norm_value, norm_unit) = match self.unit { - BinaryMultiple::Kibi => (self.value, self.unit), - BinaryMultiple::Mebi => (self.value, self.unit), - BinaryMultiple::Gibi => (self.value, self.unit), - BinaryMultiple::Tebi => (self.value * 1024.0, BinaryMultiple::Gibi), - BinaryMultiple::Pebi => (self.value * 1024.0 * 1024.0, BinaryMultiple::Gibi), - BinaryMultiple::Exbi => (self.value * 1024.0 * 1024.0 * 1024.0, BinaryMultiple::Gibi), - }; - - const EPS: f32 = 0.2; - let (scaled_value, scaled_unit) = if norm_value < 1.0 || norm_value.fract() > EPS { - match norm_unit { - BinaryMultiple::Mebi => (norm_value * 1024.0, BinaryMultiple::Kibi), - BinaryMultiple::Gibi => (norm_value * 1024.0, BinaryMultiple::Mebi), - _ => (norm_value, norm_unit), - } - } else { - (norm_value, norm_unit) - }; - - MemoryQuantity { - value: scaled_value, - unit: scaled_unit, - } - } - - /// Scale up or down to the desired `BinaryMultiple`. Returns a new `Memory` and does - /// not change itself. - pub fn scale_to(&self, binary_multiple: BinaryMultiple) -> Self { - let from_exponent: i32 = self.unit.exponential_scale_factor(); - let to_exponent: i32 = binary_multiple.exponential_scale_factor(); - - let exponent_diff = from_exponent - to_exponent; - - MemoryQuantity { - value: self.value * 1024f32.powi(exponent_diff), - unit: binary_multiple, - } - } -} - -impl Serialize for MemoryQuantity { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for MemoryQuantity { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct MemoryQuantityVisitor; - - impl<'de> Visitor<'de> for MemoryQuantityVisitor { - type Value = MemoryQuantity; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a valid memory quantity") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - MemoryQuantity::from_str(v).map_err(serde::de::Error::custom) - } - } - - deserializer.deserialize_str(MemoryQuantityVisitor) - } -} - -impl FromStr for MemoryQuantity { - type Err = Error; - - fn from_str(q: &str) -> Result { - let start_of_unit = - q.find(|c: char| c != '.' && !c.is_numeric()) - .context(NoQuantityUnitSnafu { - value: q.to_owned(), - })?; - let (value, unit) = q.split_at(start_of_unit); - Ok(MemoryQuantity { - value: value.parse::().context(InvalidQuantitySnafu { - value: q.to_owned(), - })?, - unit: unit.parse()?, - }) - } -} - -impl Display for MemoryQuantity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.value, self.unit) - } -} - -impl Mul for MemoryQuantity { - type Output = MemoryQuantity; - - fn mul(self, factor: f32) -> Self { - MemoryQuantity { - value: self.value * factor, - unit: self.unit, - } - } -} - -impl Div for MemoryQuantity { - type Output = Self; - - fn div(self, rhs: f32) -> Self::Output { - self * (1. / rhs) - } -} - -impl Div for MemoryQuantity { - type Output = f32; - - fn div(self, rhs: MemoryQuantity) -> Self::Output { - let rhs = rhs.scale_to(self.unit); - self.value / rhs.value - } -} - -impl Sub for MemoryQuantity { - type Output = MemoryQuantity; - - fn sub(self, rhs: MemoryQuantity) -> Self::Output { - MemoryQuantity { - value: self.value - rhs.scale_to(self.unit).value, - unit: self.unit, - } - } -} - -impl SubAssign for MemoryQuantity { - fn sub_assign(&mut self, rhs: MemoryQuantity) { - let rhs = rhs.scale_to(self.unit); - self.value -= rhs.value; - } -} - -impl Add for MemoryQuantity { - type Output = MemoryQuantity; - - fn add(self, rhs: MemoryQuantity) -> Self::Output { - MemoryQuantity { - value: self.value + rhs.scale_to(self.unit).value, - unit: self.unit, - } - } -} - -impl Sum for MemoryQuantity { - fn sum>(iter: I) -> Self { - iter.fold( - MemoryQuantity { - value: 0.0, - unit: BinaryMultiple::Kibi, - }, - MemoryQuantity::add, - ) - } -} - -impl AddAssign for MemoryQuantity { - fn add_assign(&mut self, rhs: MemoryQuantity) { - let rhs = rhs.scale_to(self.unit); - self.value += rhs.value; - } -} - -impl PartialOrd for MemoryQuantity { - fn partial_cmp(&self, other: &Self) -> Option { - let this_val = self.scale_to(BinaryMultiple::get_smallest()).value; - let other_val = other.scale_to(BinaryMultiple::get_smallest()).value; - this_val.partial_cmp(&other_val) - } -} - -impl PartialEq for MemoryQuantity { - fn eq(&self, other: &Self) -> bool { - let this_val = self.scale_to(BinaryMultiple::get_smallest()).value; - let other_val = other.scale_to(BinaryMultiple::get_smallest()).value; - this_val == other_val - } -} - -impl Eq for MemoryQuantity {} - -impl TryFrom for MemoryQuantity { - type Error = Error; - - fn try_from(quantity: Quantity) -> Result { - Self::try_from(&quantity) - } -} - -impl TryFrom<&Quantity> for MemoryQuantity { - type Error = Error; - - fn try_from(quantity: &Quantity) -> Result { - quantity.0.parse() - } -} - -impl From for Quantity { - fn from(quantity: MemoryQuantity) -> Self { - Self::from(&quantity) - } -} - -impl From<&MemoryQuantity> for Quantity { - fn from(quantity: &MemoryQuantity) -> Self { - Quantity(format!("{}", quantity)) - } -} - -#[cfg(test)] -mod tests { - use k8s_openapi::apimachinery::pkg::api::resource::Quantity; - - use super::*; - use rstest::rstest; - - #[rstest] - #[case("256Ki", MemoryQuantity { value: 256.0, unit: BinaryMultiple::Kibi })] - #[case("49041204Ki", MemoryQuantity { value: 49041204.0, unit: BinaryMultiple::Kibi })] - #[case("8Mi", MemoryQuantity { value: 8.0, unit: BinaryMultiple::Mebi })] - #[case("1.5Gi", MemoryQuantity { value: 1.5, unit: BinaryMultiple::Gibi })] - #[case("0.8Ti", MemoryQuantity { value: 0.8, unit: BinaryMultiple::Tebi })] - #[case("3.2Pi", MemoryQuantity { value: 3.2, unit: BinaryMultiple::Pebi })] - #[case("0.2Ei", MemoryQuantity { value: 0.2, unit: BinaryMultiple::Exbi })] - fn from_str_pass(#[case] input: &str, #[case] expected: MemoryQuantity) { - let got = MemoryQuantity::from_str(input).unwrap(); - assert_eq!(got, expected); - } - - #[rstest] - #[case("256Ki")] - #[case("1.6Mi")] - #[case("1.2Gi")] - #[case("1.6Gi")] - #[case("1Gi")] - fn try_from_quantity(#[case] input: String) { - let quantity = MemoryQuantity::try_from(Quantity(input.clone())).unwrap(); - let actual = format!("{quantity}"); - assert_eq!(input, actual); - } - - #[rstest] - #[case("256Ki", "256k")] - #[case("1.6Mi", "1m")] - #[case("1.2Gi", "1228m")] - #[case("1.6Gi", "1638m")] - #[case("1Gi", "1g")] - fn format_java(#[case] input: String, #[case] expected: String) { - let m = MemoryQuantity::try_from(Quantity(input)).unwrap(); - let actual = m.format_for_java().unwrap(); - assert_eq!(expected, actual); - } - - #[rstest] - #[case(2000f32, BinaryMultiple::Kibi, BinaryMultiple::Kibi, 2000f32)] - #[case(2000f32, BinaryMultiple::Kibi, BinaryMultiple::Mebi, 2000f32/1024f32)] - #[case(2000f32, BinaryMultiple::Kibi, BinaryMultiple::Gibi, 2000f32/1024f32/1024f32)] - #[case(2000f32, BinaryMultiple::Kibi, BinaryMultiple::Tebi, 2000f32/1024f32/1024f32/1024f32)] - #[case(2000f32, BinaryMultiple::Kibi, BinaryMultiple::Pebi, 2000f32/1024f32/1024f32/1024f32/1024f32)] - #[case(2000f32, BinaryMultiple::Pebi, BinaryMultiple::Mebi, 2000f32*1024f32*1024f32*1024f32)] - #[case(2000f32, BinaryMultiple::Pebi, BinaryMultiple::Kibi, 2000f32*1024f32*1024f32*1024f32*1024f32)] - #[case(2000f32, BinaryMultiple::Exbi, BinaryMultiple::Pebi, 2000f32*1024f32)] - fn scale_to( - #[case] value: f32, - #[case] unit: BinaryMultiple, - #[case] target_unit: BinaryMultiple, - #[case] expected: f32, - ) { - let memory = MemoryQuantity { value, unit }; - let scaled_memory = memory.scale_to(target_unit); - assert_eq!(scaled_memory.value, expected); - } - - #[rstest] - #[case("256Ki", 1.0, BinaryMultiple::Kibi, 256)] - #[case("256Ki", 0.8, BinaryMultiple::Kibi, 204)] - #[case("2Mi", 0.8, BinaryMultiple::Kibi, 1638)] - #[case("1.5Gi", 0.8, BinaryMultiple::Mebi, 1228)] - #[case("2Gi", 0.8, BinaryMultiple::Mebi, 1638)] - #[case("2Ti", 0.8, BinaryMultiple::Mebi, 1677721)] - #[case("2Ti", 0.8, BinaryMultiple::Gibi, 1638)] - #[case("2Ti", 1.0, BinaryMultiple::Gibi, 2048)] - #[case("2048Ki", 1.0, BinaryMultiple::Mebi, 2)] - #[case("2000Ki", 1.0, BinaryMultiple::Mebi, 1)] - #[case("4000Mi", 1.0, BinaryMultiple::Gibi, 3)] - #[case("4000Mi", 0.8, BinaryMultiple::Gibi, 3)] - fn to_java_heap_value_pass( - #[case] q: &str, - #[case] factor: f32, - #[case] target_unit: BinaryMultiple, - #[case] heap: u32, - ) { - #[allow(deprecated)] // allow use of the deprecated 'to_java_heap' function to test it - let actual = to_java_heap_value(&Quantity(q.to_owned()), factor, target_unit).unwrap(); - assert_eq!(actual, heap); - } - - #[rstest] - #[case("1000Ki", 0.8, BinaryMultiple::Gibi)] - #[case("1000Ki", 0.8, BinaryMultiple::Mebi)] - #[case("1000Mi", 0.8, BinaryMultiple::Gibi)] - #[case("1000Mi", 1.0, BinaryMultiple::Gibi)] - #[case("1023Mi", 1.0, BinaryMultiple::Gibi)] - #[case("1024Mi", 0.8, BinaryMultiple::Gibi)] - fn to_java_heap_value_fail( - #[case] q: &str, - #[case] factor: f32, - #[case] target_unit: BinaryMultiple, - ) { - #[allow(deprecated)] // allow use of the deprecated 'to_java_heap' function to test it - let result = to_java_heap_value(&Quantity(q.to_owned()), factor, target_unit); - assert!(result.is_err()); - } - - #[rstest] - #[case("1000Ki", "500Ki", "500Ki")] - #[case("1Mi", "512Ki", "512Ki")] - #[case("2Mi", "512Ki", "1536Ki")] - #[case("2048Ki", "1Mi", "1024Ki")] - fn subtraction(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { - let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); - let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); - let expected = MemoryQuantity::try_from(Quantity(res.to_owned())).unwrap(); - let actual = lhs - rhs; - assert_eq!(expected, actual); - - let mut actual = lhs; - actual -= rhs; - assert_eq!(expected, actual); - } - - #[rstest] - #[case("1000Ki", "500Ki", "1500Ki")] - #[case("1Mi", "512Ki", "1536Ki")] - #[case("2Mi", "512Ki", "2560Ki")] - #[case("2048Ki", "1Mi", "3072Ki")] - fn addition(#[case] lhs: &str, #[case] rhs: &str, #[case] res: &str) { - let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); - let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); - let expected = MemoryQuantity::try_from(Quantity(res.to_owned())).unwrap(); - let actual = lhs + rhs; - assert_eq!(expected, actual); - - let mut actual = MemoryQuantity::from_mebi(0.0); - actual += lhs; - actual += rhs; - assert_eq!(expected, actual); - } - - #[rstest] - #[case("100Ki", "100Ki", false)] - #[case("100Ki", "100Ki", false)] - #[case("100Ki", "100Ki", false)] - #[case("101Ki", "100Ki", true)] - #[case("100Ki", "101Ki", false)] - #[case("1Mi", "100Ki", true)] - #[case("2000Ki", "1Mi", true)] - fn partial_ord(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { - let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); - let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); - assert_eq!(lhs > rhs, res) - } - - #[rstest] - #[case("100Ki", "100Ki", true)] - #[case("100Ki", "200Ki", false)] - #[case("1Mi", "1024Ki", true)] - #[case("1024Ki", "1Mi", true)] - fn partial_eq(#[case] lhs: &str, #[case] rhs: &str, #[case] res: bool) { - let lhs = MemoryQuantity::try_from(Quantity(lhs.to_owned())).unwrap(); - let rhs = MemoryQuantity::try_from(Quantity(rhs.to_owned())).unwrap(); - assert_eq!(lhs == rhs, res) - } - - #[rstest] - #[case(MemoryQuantity::from_mebi(1536.0), "memory: 1536Mi\n")] - #[case(MemoryQuantity::from_mebi(100.0), "memory: 100Mi\n")] - #[case(MemoryQuantity::from_gibi(10.0), "memory: 10Gi\n")] - #[case(MemoryQuantity::from_gibi(1.0), "memory: 1Gi\n")] - fn serialize(#[case] memory: MemoryQuantity, #[case] expected: &str) { - #[derive(Serialize)] - struct Memory { - memory: MemoryQuantity, - } - - let memory = Memory { memory }; - let output = serde_yaml::to_string(&memory).unwrap(); - - assert_eq!(output, expected); - } - - #[rstest] - #[case("memory: 1536Mi", MemoryQuantity::from_mebi(1536.0))] - #[case("memory: 100Mi", MemoryQuantity::from_mebi(100.0))] - #[case("memory: 10Gi", MemoryQuantity::from_gibi(10.0))] - #[case("memory: 1Gi", MemoryQuantity::from_gibi(1.0))] - fn deserialize(#[case] input: &str, #[case] expected: MemoryQuantity) { - #[derive(Deserialize)] - struct Memory { - memory: MemoryQuantity, - } - - let memory: Memory = serde_yaml::from_str(input).unwrap(); - assert_eq!(memory.memory, expected); - } -} diff --git a/crates/stackable-operator/src/product_logging/framework.rs b/crates/stackable-operator/src/product_logging/framework.rs index 5acfa2deb..48fc066f9 100644 --- a/crates/stackable-operator/src/product_logging/framework.rs +++ b/crates/stackable-operator/src/product_logging/framework.rs @@ -12,7 +12,7 @@ use crate::{ apimachinery::pkg::api::resource::Quantity, }, kube::Resource, - memory::{BinaryMultiple, MemoryQuantity}, + quantity::{BinaryMultiple, MemoryQuantity, Suffix}, role_utils::RoleGroupRef, }; @@ -69,21 +69,15 @@ pub enum LoggingError { /// pod::PodBuilder, /// meta::ObjectMetaBuilder, /// }, -/// memory::{ +/// quantity::{ /// BinaryMultiple, /// MemoryQuantity, /// }, /// }; /// # use stackable_operator::product_logging; /// -/// const MAX_INIT_CONTAINER_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { -/// value: 1.0, -/// unit: BinaryMultiple::Mebi, -/// }; -/// const MAX_MAIN_CONTAINER_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity { -/// value: 10.0, -/// unit: BinaryMultiple::Mebi, -/// }; +/// const MAX_INIT_CONTAINER_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity::from_mebi(1.0); +/// const MAX_MAIN_CONTAINER_LOG_FILES_SIZE: MemoryQuantity = MemoryQuantity::from_mebi(10.0); /// /// PodBuilder::new() /// .metadata(ObjectMetaBuilder::default().build()) @@ -105,13 +99,14 @@ pub fn calculate_log_volume_size_limit(max_log_files_size: &[MemoryQuantity]) -> .iter() .cloned() .sum::() - .scale_to(BinaryMultiple::Mebi) + .scale_to(Suffix::BinaryMultiple(BinaryMultiple::Mebi)) // According to the reasons mentioned in the function documentation, the multiplier must be // greater than 2. Manual tests with ZooKeeper 3.8 in an OpenShift cluster showed that 3 is // absolutely sufficient. - .mul(3.0) + .mul(3.0f32) // Avoid bulky numbers due to the floating-point arithmetic. .ceil(); + log_volume_size_limit.into() } @@ -1528,10 +1523,10 @@ mod tests { use super::*; use crate::product_logging::spec::{AppenderConfig, LoggerConfig}; use rstest::rstest; - use std::collections::BTreeMap; + use std::{collections::BTreeMap, str::FromStr}; #[rstest] - #[case("0Mi", &[])] + #[case("0", &[])] #[case("3Mi", &["1Mi"])] #[case("5Mi", &["1.5Mi"])] #[case("1Mi", &["100Ki"])] @@ -1542,7 +1537,7 @@ mod tests { ) { let input = max_log_files_sizes .iter() - .map(|v| MemoryQuantity::try_from(Quantity(v.to_string())).unwrap()) + .map(|v| MemoryQuantity::from_str(v).unwrap()) .collect::>(); let calculated_log_volume_size_limit = calculate_log_volume_size_limit(&input); assert_eq!( diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs new file mode 100644 index 000000000..4a4ca4a81 --- /dev/null +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -0,0 +1,106 @@ +use std::{ + fmt::Display, + iter::Sum, + ops::{Add, Deref}, + str::FromStr, +}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; +use snafu::Snafu; + +use crate::quantity::{ + macros::forward_quantity_impls, DecimalExponent, DecimalMultiple, Quantity, Suffix, +}; + +#[derive(Debug, Snafu)] +pub struct ParseSuffixError { + input: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub enum CpuSuffix { + DecimalMultiple(DecimalMultiple), + DecimalExponent(DecimalExponent), +} + +impl FromStr for CpuSuffix { + type Err = ParseSuffixError; + + fn from_str(input: &str) -> Result { + if let Ok(decimal_multiple) = DecimalMultiple::from_str(input) { + return Ok(Self::DecimalMultiple(decimal_multiple)); + } + + if input.starts_with(['e', 'E']) { + if let Ok(decimal_exponent) = f64::from_str(&input[1..]) { + return Ok(Self::DecimalExponent(DecimalExponent::from( + decimal_exponent, + ))); + } + } + + ParseSuffixSnafu { input }.fail() + } +} + +impl Display for CpuSuffix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl Default for CpuSuffix { + fn default() -> Self { + CpuSuffix::DecimalMultiple(DecimalMultiple::Empty) + } +} + +impl Suffix for CpuSuffix { + fn factor(&self) -> f64 { + todo!() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct CpuQuantity(Quantity); + +impl Deref for CpuQuantity { + type Target = Quantity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for K8sQuantity { + fn from(value: CpuQuantity) -> Self { + K8sQuantity(value.to_string()) + } +} + +impl Sum for CpuQuantity { + fn sum>(iter: I) -> Self { + iter.fold( + CpuQuantity(Quantity { + value: 0.0, + suffix: CpuSuffix::DecimalMultiple(DecimalMultiple::Empty), + }), + CpuQuantity::add, + ) + } +} + +forward_quantity_impls!(CpuQuantity, K8sQuantity, usize, f32, f64); + +impl CpuQuantity { + pub fn from_millis(value: u32) -> Self { + CpuQuantity(Quantity { + suffix: CpuSuffix::DecimalMultiple(DecimalMultiple::Milli), + value: value.into(), + }) + } + + pub fn scale_to(self, suffix: CpuSuffix) -> Self { + Self(self.0.scale_to(suffix)) + } +} diff --git a/crates/stackable-operator/src/quantity/macros.rs b/crates/stackable-operator/src/quantity/macros.rs new file mode 100644 index 000000000..18e0cae04 --- /dev/null +++ b/crates/stackable-operator/src/quantity/macros.rs @@ -0,0 +1,102 @@ +macro_rules! forward_quantity_impls { + ($for:ty, $kq:ty, $($on:ty),+) => { + $crate::quantity::macros::forward_from_impls!($for, $kq); + $crate::quantity::macros::forward_op_impls!($for, $($on),*); + }; +} + +macro_rules! forward_from_impls { + ($for:ty, $kq:ty) => { + impl ::std::str::FromStr for $for { + type Err = $crate::quantity::ParseQuantityError; + + fn from_str(input: &str) -> Result { + let quantity = $crate::quantity::Quantity::from_str(input)?; + Ok(Self(quantity)) + } + } + + impl From<$crate::quantity::Quantity> for $for { + fn from(quantity: $crate::quantity::Quantity) -> Self { + Self(quantity) + } + } + + impl TryFrom<$kq> for $for { + type Error = $crate::quantity::ParseQuantityError; + + fn try_from(value: $kq) -> Result { + Ok(Self($crate::quantity::Quantity::try_from(value)?)) + } + } + + impl TryFrom<&$kq> for $for { + type Error = $crate::quantity::ParseQuantityError; + + fn try_from(value: &$kq) -> Result { + Ok(Self(Quantity::try_from(value)?)) + } + } + }; +} + +macro_rules! forward_op_impls { + ($for:ty, $($on:ty),+) => { + impl ::std::ops::Add for $for { + type Output = $for; + + fn add(self, rhs: $for) -> Self::Output { + Self(self.0 + rhs.0) + } + } + + impl ::std::ops::AddAssign for $for { + fn add_assign(&mut self, rhs: $for) { + self.0 += rhs.0 + } + } + + impl ::std::ops::Sub for $for { + type Output = $for; + + fn sub(self, rhs: $for) -> Self::Output { + Self(self.0 - rhs.0) + } + } + + impl ::std::ops::SubAssign for $for { + fn sub_assign(&mut self, rhs: $for) { + self.0 -= rhs.0 + } + } + + impl ::std::ops::Div for $for { + type Output = f64; + + fn div(self, rhs: $for) -> Self::Output { + self.0 / rhs.0 + } + } + + $( + impl ::std::ops::Mul<$on> for $for { + type Output = $for; + + fn mul(self, rhs: $on) -> Self::Output { + Self(self.0 * rhs) + } + } + + impl ::std::ops::MulAssign<$on> for $for { + fn mul_assign(&mut self, rhs: $on) { + self.0 *= rhs + } + } + )* + }; +} + +// HACK: Make the macros only available in this crate. +pub(crate) use forward_from_impls; +pub(crate) use forward_op_impls; +pub(crate) use forward_quantity_impls; diff --git a/crates/stackable-operator/src/quantity/memory.rs b/crates/stackable-operator/src/quantity/memory.rs new file mode 100644 index 000000000..e60401da4 --- /dev/null +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -0,0 +1,74 @@ +use std::{ + iter::Sum, + ops::{Add, Deref}, +}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; + +use crate::quantity::{macros::forward_quantity_impls, BinaryMultiple, Quantity, Suffix}; + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct MemoryQuantity(Quantity); + +impl Deref for MemoryQuantity { + type Target = Quantity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for K8sQuantity { + fn from(value: MemoryQuantity) -> Self { + K8sQuantity(value.to_string()) + } +} + +impl Sum for MemoryQuantity { + fn sum>(iter: I) -> Self { + iter.fold( + MemoryQuantity(Quantity { + value: 0.0, + suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi), + }), + MemoryQuantity::add, + ) + } +} + +forward_quantity_impls!(MemoryQuantity, K8sQuantity, usize, f32, f64); + +impl MemoryQuantity { + pub const fn from_gibi(value: f64) -> Self { + MemoryQuantity(Quantity { + suffix: Suffix::BinaryMultiple(BinaryMultiple::Gibi), + value, + }) + } + + pub const fn from_mebi(value: f64) -> Self { + MemoryQuantity(Quantity { + suffix: Suffix::BinaryMultiple(BinaryMultiple::Mebi), + value, + }) + } + + pub fn scale_to(self, suffix: Suffix) -> Self { + Self(self.0.scale_to(suffix)) + } + + pub fn ceil(self) -> Self { + Self(Quantity { + value: self.value.ceil(), + suffix: self.suffix, + }) + } +} + +pub trait JavaHeap { + // TODO (@Techassi): Add proper error type + /// Formats the [`MemoryQuantity`] so that it can be used as a Java heap value. + /// + /// This function can fail, because the [`Quantity`] has to be scaled down to at most + fn to_java_heap_string(&self) -> Result; +} diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs new file mode 100644 index 000000000..a869a54c6 --- /dev/null +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -0,0 +1,420 @@ +//! This module contains types and functions to parse and handle Kubernetes quantities. + +use std::{ + fmt::{Display, Write}, + num::ParseFloatError, + str::FromStr, +}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; +use snafu::{ensure, ResultExt as _, Snafu}; + +mod cpu; +mod macros; +mod memory; +mod ops; +mod suffix; + +pub use cpu::*; +pub use memory::*; +pub use suffix::*; + +#[derive(Debug, PartialEq, Snafu)] +pub enum ParseQuantityError +where + E: std::error::Error + 'static, +{ + #[snafu(display("input is either empty or contains non-ascii characters"))] + InvalidFormat, + + #[snafu(display("failed to parse floating point number"))] + InvalidFloat { source: ParseFloatError }, + + #[snafu(display("failed to parse suffix"))] + InvalidSuffix { source: E }, +} + +// pub struct CpuQuant(Quantity1); + +// pub struct Quantity1 +// where +// T: SuffixTrait, +// { +// value: f64, +// suffix: T, +// } + +// pub trait SuffixTrait: FromStr + Default { +// fn factor(&self) -> f64; +// } + +/// Quantity is a representation of a number with a suffix / format. +/// +/// This type makes it possible to parse Kubernetes quantity strings like '12Ki', '2M, '1.5e2', or +/// '0'. This is done by storing the parsed data as two separate values: the `value` and the +/// `suffix`. The parsing is implemented according to the serialization format laid out in the +/// Kubernetes [source code][quantity-format]. Roughly, the format looks like this: +/// +/// ```plain +/// quantity ::= +/// suffix ::= | | +/// binaryMultiple ::= Ki | Mi | Gi | Ti | Pi | Ei +/// decimalMultiple ::= m | "" | k | M | G | T | P | E +/// decimalExponent ::= "e" | "E" +/// ``` +/// +/// Generally speaking, this implementation very closely resembles the original upstream Go +/// implementation of the Kubernetes project. However there are a few differences which boil down +/// to being easier to use / implement using Rust and safety. These differences in addition to +/// general notes on the implementation are detailed below: +/// +/// #### Suffixes +/// +/// It should be noted that the decimal multiple contains `""` (an empty string / no suffix). This +/// is why one might think that the suffix is optional. Strictly speaking, it is not optional, but +/// a missing / empty suffix maps to a decimal multiple with a scaling factor of 1000^0. The +/// following section goes into more detail about the scaling factors. +/// +/// Instead of marking the `suffix` field as optional by using [`Option`], it instead maps the empty +/// suffix to the [`DecimalMultiple::Empty`] variant. This eases implementing safe mathematical (like +/// scaling up/down, addition or division) operations on [`Quantity`]. +/// +/// The [`Suffix`] enum represents the three different supported suffixes. Each suffix uses a +/// specific base and exponent for it's scaling factor: +/// +/// - The [`BinaryMultiple`] uses a base of 2 and exponents of 10s. +/// - The [`DecimalMultiple`] uses a base of 10 and exponents of 3s. +/// - The [`DecimalExponent`] uses a base of 10 and exponents defined using the +/// [scientific notation][sci-notation]. +/// +/// #### Mathematical operations +/// +/// Similar to to upstream implementation, math operations can change the suffix / format. +/// Additionally, it is necessary to change the suffix of the right-hand-side in binary operations +/// before doing the actual operation (like addition). +/// +/// - **Example 1:** `0Ki + 1Mi` - In this example, the lhs has the value **0**. The exact suffix is +/// irrelevant, but note that it might be different from the suffix of the rhs. Since the value is +/// zero, we can safely update the suffix of the lhs to `Mi` and continue by adding **1** to the +/// lhs. The final result is then `1Mi`. +/// - **Example 2:** `1024Ki + 1Mi` - Here, the lhs is not zero, so we cannot safely update the +/// suffix. Instead, we need to scale the rhs to the appropriate suffix, `Ki` in this example. +/// Afterwards the addition of both values can be done. The final result is `2048Ki`. If needed, +/// this can be scaled to `Mi`, resulting in `2Mi` as expected. +/// +/// #### Precision +/// +/// The upstream implementation uses infinite-precision arithmetic to be able to store very large +/// values, up to 2^63-1. This implementation **does not** use infinite-precision arithmetic. The +/// biggest value which can be safely expresses is [`f64::MAX`]. This value is deemed plenty for +/// now, but there is always the possibility of using infinite-precision implementation as well. +/// +/// [quantity-format]: https://github.com/kubernetes/apimachinery/blob/3e8e52d6a1259ada73f63c1c7d1fad39d4ba9fb4/pkg/api/resource/quantity.go#L39-L59 +/// [sci-notation]: https://en.wikipedia.org/wiki/Scientific_notation#E_notation +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct Quantity +where + S: Suffix, +{ + // FIXME (@Techassi): Support arbitrary-precision numbers + /// The numeric value of the quantity. + /// + /// This field holds data parsed from `` according to the spec. We especially opt + /// to not use arbitrary-precision arithmetic like the Go implementation, as we don't see the + /// need to support these huge numbers. + value: f64, + + /// The suffix of the quantity. + /// + /// This field holds data parsed from `` according to the spec. + suffix: S, +} + +impl FromStr for Quantity +where + S: Suffix, +{ + type Err = ParseQuantityError; + + fn from_str(input: &str) -> Result { + ensure!(!input.is_empty() && input.is_ascii(), InvalidFormatSnafu); + + if input == "0" { + return Ok(Self { + suffix: S::default(), + value: 0.0, + }); + } + + match input.find(|c: char| c != '.' && !c.is_ascii_digit()) { + Some(suffix_index) => { + let parts = input.split_at(suffix_index); + let value = f64::from_str(parts.0).context(InvalidFloatSnafu)?; + let suffix = S::from_str(parts.1).context(InvalidSuffixSnafu)?; + + Ok(Self { suffix, value }) + } + None => { + let value = f64::from_str(input).context(InvalidFloatSnafu)?; + + Ok(Self { + suffix: S::default(), + value, + }) + } + } + } +} + +impl Display for Quantity +where + S: Suffix, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.value == 0.0 { + return f.write_char('0'); + } + + write!( + f, + "{value}{suffix}", + value = self.value, + suffix = self.suffix + ) + } +} + +impl From> for K8sQuantity +where + S: Suffix, +{ + fn from(value: Quantity) -> Self { + K8sQuantity(value.to_string()) + } +} + +impl From<&Quantity> for K8sQuantity +where + S: Suffix, +{ + fn from(value: &Quantity) -> Self { + K8sQuantity(value.to_string()) + } +} + +impl TryFrom for Quantity +where + S: Suffix, +{ + type Error = ParseQuantityError; + + fn try_from(value: K8sQuantity) -> Result { + Quantity::from_str(&value.0) + } +} + +impl TryFrom<&K8sQuantity> for Quantity +where + S: Suffix, +{ + type Error = ParseQuantityError; + + fn try_from(value: &K8sQuantity) -> Result { + Quantity::from_str(&value.0) + } +} + +impl Quantity +where + S: Suffix, +{ + /// Optionally scales up or down to the provided `suffix`. + /// + /// No scaling is performed in the following cases: + /// + /// - the suffixes already match + /// - the value is 0 + pub fn scale_to(self, suffix: S) -> Self { + match (self.value, &self.suffix) { + (0.0, _) => self, + (_, s) if *s == suffix => self, + (v, s) => { + let factor = s.factor() / suffix.factor(); + + Self { + value: v * factor, + suffix, + } + } + } + } + + // pub fn scale_to_non_zero(self) -> Self { + // if !self.value.between(-1.0, 1.0) { + // return self; + // } + + // let mut this = self; + + // while let Some(suffix) = this.suffix.scale_down() { + // this = self.scale_to(suffix); + // if this.value.between(-1.0, 1.0) { + // continue; + // } else { + // return this; + // } + // } + + // Self { + // value: 1.0, + // suffix: this.suffix, + // } + // } + + /// Either sets the suffix of `self` to `rhs` or scales `rhs` if `self` has a value other than + /// zero. + /// + /// This function is currently used for the [`std::ops::Add`] and [`std::ops::Sub`] + /// implementations. + pub fn set_suffix_or_scale_rhs(self, rhs: Self) -> (Self, Self) { + if self.value == 0.0 { + ( + Self { + suffix: rhs.suffix, + ..self + }, + rhs, + ) + } else { + (self, rhs.scale_to(self.suffix)) + } + } +} + +trait FloatExt: PartialOrd + Sized { + fn between(self, start: Self, end: Self) -> bool { + self > start && self < end + } +} + +impl FloatExt for f64 {} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + // See https://github.com/kubernetes/apimachinery/blob/3e8e52d6a1259ada73f63c1c7d1fad39d4ba9fb4/pkg/api/resource/quantity_test.go#L276-L287 + #[rustfmt::skip] + #[rstest] + #[case("0", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Empty))] + #[case("0n", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Nano))] + #[case("0u", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Micro))] + #[case("0m", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Milli))] + #[case("0Ki", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Kibi))] + #[case("0k", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Kilo))] + #[case("0Mi", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Mebi))] + #[case("0M", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Mega))] + #[case("0Gi", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Gibi))] + #[case("0G", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Giga))] + #[case("0Ti", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Tebi))] + #[case("0T", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Tera))] + #[case("0Pi", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Pebi))] + #[case("0P", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Peta))] + #[case("0Ei", 0.0, Suffix::BinaryMultiple(BinaryMultiple::Exbi))] + #[case("0E", 0.0, Suffix::DecimalMultiple(DecimalMultiple::Exa))] + fn parse_zero_quantity(#[case] input: &str, #[case] expected_value: f64, #[case] expected_suffix: Suffix) { + let parsed = Quantity::from_str(input).unwrap(); + + assert_eq!(parsed.suffix, expected_suffix); + assert_eq!(parsed.value, expected_value); + } + + // See https://github.com/kubernetes/apimachinery/blob/3e8e52d6a1259ada73f63c1c7d1fad39d4ba9fb4/pkg/api/resource/quantity_test.go#L289 + #[rustfmt::skip] + #[rstest] + #[case("12.34", 12.34)] + #[case("12", 12.0)] + #[case("1", 1.0)] + fn parse_quantity_without_suffix(#[case] input: &str, #[case] expected_value: f64) { + let parsed = Quantity::from_str(input).unwrap(); + assert_eq!(parsed.value, expected_value); + } + + #[rstest] + #[case("0Mi", Some("0"))] + #[case("256Ki", None)] + #[case("1.5Gi", None)] + #[case("0.8Ti", None)] + #[case("3.2Pi", None)] + #[case("0.2Ei", None)] + #[case("8Mi", None)] + #[case("0", None)] + fn binary_to_string_pass(#[case] input: &str, #[case] output: Option<&str>) { + let parsed = Quantity::from_str(input).unwrap(); + assert_eq!(output.unwrap_or(input), parsed.to_string()); + } + + #[rstest] + #[case("1Mi", BinaryMultiple::Kibi, "1024Ki")] + #[case("1024Ki", BinaryMultiple::Mebi, "1Mi")] + #[case("1Mi", BinaryMultiple::Mebi, "1Mi")] + fn binary_to_binary_scale_pass( + #[case] input: &str, + #[case] scale_to: BinaryMultiple, + #[case] output: &str, + ) { + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::BinaryMultiple(scale_to)); + + assert_eq!(parsed.to_string(), output); + } + + #[rstest] + #[case("1Mi", DecimalMultiple::Kilo, "1048.576k")] + #[case("1Mi", DecimalMultiple::Mega, "1.048576M")] + fn binary_to_decimal_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalMultiple, + #[case] output: &str, + ) { + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalMultiple(scale_to)); + + assert_eq!(parsed.to_string(), output); + } + + #[rstest] + #[case("1M", DecimalMultiple::Kilo, "1000k")] + #[case("1000k", DecimalMultiple::Mega, "1M")] + #[case("1M", DecimalMultiple::Mega, "1M")] + fn decimal_to_decimal_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalMultiple, + #[case] output: &str, + ) { + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalMultiple(scale_to)); + + assert_eq!(parsed.to_string(), output); + } + + #[rstest] + #[case("1e3", DecimalExponent::from(0.0), "1000e0")] + #[case("1000e0", DecimalExponent::from(3.0), "1e3")] + #[case("1e3", DecimalExponent::from(3.0), "1e3")] + fn decimal_exponent_to_decimal_exponent_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalExponent, + #[case] output: &str, + ) { + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalExponent(scale_to)); + + assert_eq!(parsed.to_string(), output); + } +} diff --git a/crates/stackable-operator/src/quantity/ops.rs b/crates/stackable-operator/src/quantity/ops.rs new file mode 100644 index 000000000..38fe532e0 --- /dev/null +++ b/crates/stackable-operator/src/quantity/ops.rs @@ -0,0 +1,101 @@ +use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub, SubAssign}; + +use crate::quantity::Quantity; + +impl Add for Quantity { + type Output = Quantity; + + fn add(self, rhs: Quantity) -> Self::Output { + let (this, rhs) = self.set_suffix_or_scale_rhs(rhs); + + Self { + value: this.value + rhs.value, + suffix: this.suffix, + } + } +} + +impl AddAssign for Quantity { + fn add_assign(&mut self, rhs: Quantity) { + *self = self.add(rhs) + } +} + +impl Sub for Quantity { + type Output = Quantity; + + fn sub(self, rhs: Quantity) -> Self::Output { + let (this, rhs) = self.set_suffix_or_scale_rhs(rhs); + + Self { + value: this.value - rhs.value, + suffix: this.suffix, + } + } +} + +impl SubAssign for Quantity { + fn sub_assign(&mut self, rhs: Self) { + *self = self.sub(rhs) + } +} + +impl Mul for Quantity { + type Output = Quantity; + + fn mul(self, rhs: usize) -> Self::Output { + Self { + value: self.value * rhs as f64, + ..self + } + } +} + +impl MulAssign for Quantity { + fn mul_assign(&mut self, rhs: usize) { + *self = self.mul(rhs) + } +} + +impl Mul for Quantity { + type Output = Quantity; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + value: self.value * rhs as f64, + ..self + } + } +} + +impl MulAssign for Quantity { + fn mul_assign(&mut self, rhs: f32) { + *self = self.mul(rhs) + } +} + +impl Mul for Quantity { + type Output = Quantity; + + fn mul(self, rhs: f64) -> Self::Output { + Self { + value: self.value * rhs, + ..self + } + } +} + +impl MulAssign for Quantity { + fn mul_assign(&mut self, rhs: f64) { + *self = self.mul(rhs) + } +} + +impl Div for Quantity { + type Output = f64; + + fn div(self, rhs: Self) -> Self::Output { + let rhs = rhs.scale_to(self.suffix); + self.value / rhs.value + } +} diff --git a/crates/stackable-operator/src/quantity/suffix.rs b/crates/stackable-operator/src/quantity/suffix.rs new file mode 100644 index 000000000..8a4aa75e1 --- /dev/null +++ b/crates/stackable-operator/src/quantity/suffix.rs @@ -0,0 +1,231 @@ +use std::{fmt::Display, ops::Deref, str::FromStr}; + +pub trait Suffix: + FromStr + + Clone + + Copy + + Default + + Display + + PartialEq + + PartialOrd +{ + fn factor(&self) -> f64; +} + +/// Supported byte-multiples based on powers of 2. +/// +/// These units are defined in IEC 80000-13 and are supported by other standards bodies like NIST. +/// The following list contains examples using the official units which Kubernetes adopted with +/// slight changes (mentioned in parentheses). +/// +/// ```plain +/// - 1024^1, KiB (Ki), Kibibyte +/// - 1024^2, MiB (Mi), Mebibyte +/// - 1024^3, GiB (Gi), Gibibyte +/// - 1024^4, TiB (Ti), Tebibyte +/// - 1024^5, PiB (Pi), Pebibyte +/// - 1024^6, EiB (Ei), Exbibyte +/// ``` +/// +/// All units bigger than Exbibyte are not a valid suffix according to the [Kubernetes serialization +/// format][k8s-serialization-format]. +/// +/// ### See +/// +/// - +/// - +/// +/// [k8s-serialization-format]: https://github.com/kubernetes/apimachinery/blob/8c60292e48e46c4faa1e92acb232ce6adb37512c/pkg/api/resource/quantity.go#L37-L59 +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, strum::Display, strum::EnumString)] +pub enum BinaryMultiple { + #[strum(serialize = "Ki")] + Kibi, + + #[strum(serialize = "Mi")] + Mebi, + + #[strum(serialize = "Gi")] + Gibi, + + #[strum(serialize = "Ti")] + Tebi, + + #[strum(serialize = "Pi")] + Pebi, + + #[strum(serialize = "Ei")] + Exbi, +} + +impl BinaryMultiple { + /// Returns the factor based on powers of 2. + pub fn factor(&self) -> f64 { + match self { + BinaryMultiple::Kibi => 2f64.powi(10), + BinaryMultiple::Mebi => 2f64.powi(20), + BinaryMultiple::Gibi => 2f64.powi(30), + BinaryMultiple::Tebi => 2f64.powi(40), + BinaryMultiple::Pebi => 2f64.powi(50), + BinaryMultiple::Exbi => 2f64.powi(60), + } + } + + pub fn scale_down(self) -> Option { + match self { + BinaryMultiple::Kibi => None, + BinaryMultiple::Mebi => Some(BinaryMultiple::Kibi), + BinaryMultiple::Gibi => Some(BinaryMultiple::Mebi), + BinaryMultiple::Tebi => Some(BinaryMultiple::Gibi), + BinaryMultiple::Pebi => Some(BinaryMultiple::Tebi), + BinaryMultiple::Exbi => Some(BinaryMultiple::Pebi), + } + } +} + +/// Supported byte-multiples based on powers of 10. +/// +/// These units are recommended by the International Electrotechnical Commission (IEC). The +/// following list contains examples using the official SI units and the units used by Kubernetes +/// (mentioned in parentheses). Units used by Kubernetes are a shortened version of the SI units. +/// +/// It should also be noted that there is an inconsistency in the format Kubernetes uses. Kilobytes +/// should use 'K' instead of 'k'. +/// +/// ```plain +/// - 1000^-1, (m): millibyte (Kubernetes only) +/// - 1000^ 0, B ( ): byte (no suffix) +/// - 1000^ 1, kB (k): kilobyte +/// - 1000^ 2, MB (M): Megabyte +/// - 1000^ 3, GB (G): Gigabyte +/// - 1000^ 4, TB (T): Terabyte +/// - 1000^ 5, PB (P): Petabyte +/// - 1000^ 6, EB (E): Exabyte +/// ``` +/// +/// All units bigger than Exabyte are not a valid suffix according to the [Kubernetes serialization +/// format][k8s-serialization-format]. +/// +/// ### See +/// +/// - +/// - +/// +/// [k8s-serialization-format]: https://github.com/kubernetes/apimachinery/blob/8c60292e48e46c4faa1e92acb232ce6adb37512c/pkg/api/resource/quantity.go#L37-L59 +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, strum::Display, strum::EnumString)] +pub enum DecimalMultiple { + #[strum(serialize = "n")] + Nano, + + #[strum(serialize = "u")] + Micro, + + #[strum(serialize = "m")] + Milli, + + #[strum(serialize = "")] + Empty, + + // (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) + #[strum(serialize = "k")] + Kilo, + + #[strum(serialize = "M")] + Mega, + + #[strum(serialize = "G")] + Giga, + + #[strum(serialize = "T")] + Tera, + + #[strum(serialize = "P")] + Peta, + + #[strum(serialize = "E")] + Exa, +} + +impl DecimalMultiple { + pub fn factor(&self) -> f64 { + match self { + DecimalMultiple::Nano => 10f64.powi(-9), + DecimalMultiple::Micro => 10f64.powi(-6), + DecimalMultiple::Milli => 10f64.powi(-3), + DecimalMultiple::Empty => 10f64.powi(0), + DecimalMultiple::Kilo => 10f64.powi(3), + DecimalMultiple::Mega => 10f64.powi(6), + DecimalMultiple::Giga => 10f64.powi(9), + DecimalMultiple::Tera => 10f64.powi(12), + DecimalMultiple::Peta => 10f64.powi(15), + DecimalMultiple::Exa => 10f64.powi(18), + } + } +} + +/// Scientific (also known as E) notation of numbers. +/// +/// ### See +/// +/// - +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct DecimalExponent(f64); + +impl From for DecimalExponent { + fn from(value: f64) -> Self { + Self(value) + } +} + +impl Deref for DecimalExponent { + type Target = f64; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for DecimalExponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl DecimalExponent { + pub fn factor(&self) -> f64 { + 10f64.powf(self.0) + } +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("Ki", BinaryMultiple::Kibi)] + #[case("Mi", BinaryMultiple::Mebi)] + #[case("Gi", BinaryMultiple::Gibi)] + #[case("Ti", BinaryMultiple::Tebi)] + #[case("Pi", BinaryMultiple::Pebi)] + #[case("Ei", BinaryMultiple::Exbi)] + fn binary_multiple_from_str_pass(#[case] input: &str, #[case] expected: BinaryMultiple) { + let parsed = BinaryMultiple::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("n", DecimalMultiple::Nano)] + #[case("u", DecimalMultiple::Micro)] + #[case("m", DecimalMultiple::Milli)] + #[case("", DecimalMultiple::Empty)] + #[case("k", DecimalMultiple::Kilo)] + #[case("M", DecimalMultiple::Mega)] + #[case("G", DecimalMultiple::Giga)] + #[case("T", DecimalMultiple::Tera)] + #[case("P", DecimalMultiple::Peta)] + #[case("E", DecimalMultiple::Exa)] + fn decimal_multiple_from_str_pass(#[case] input: &str, #[case] expected: DecimalMultiple) { + let parsed = DecimalMultiple::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } +}