From f9dd2c973a752e33bad3be28bebd3f95d45e4b0c Mon Sep 17 00:00:00 2001 From: Techassi Date: Mon, 6 Jan 2025 15:47:20 +0100 Subject: [PATCH 01/11] refactor!(stackable-operator): Fix and improve quantity parsing --- Cargo.lock | 4 +- .../src/builder/pod/resources.rs | 6 +- .../src/commons/resources.rs | 14 +- crates/stackable-operator/src/cpu.rs | 307 -------- crates/stackable-operator/src/lib.rs | 3 +- crates/stackable-operator/src/memory.rs | 712 ------------------ .../src/product_logging/framework.rs | 4 +- crates/stackable-operator/src/quantity/cpu.rs | 38 + .../stackable-operator/src/quantity/memory.rs | 53 ++ crates/stackable-operator/src/quantity/mod.rs | 549 ++++++++++++++ 10 files changed, 652 insertions(+), 1038 deletions(-) delete mode 100644 crates/stackable-operator/src/cpu.rs delete mode 100644 crates/stackable-operator/src/memory.rs create mode 100644 crates/stackable-operator/src/quantity/cpu.rs create mode 100644 crates/stackable-operator/src/quantity/memory.rs create mode 100644 crates/stackable-operator/src/quantity/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1cdc26c21..222e62c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3277,14 +3277,14 @@ dependencies = [ [[package]] name = "stackable-versioned" -version = "0.4.1" +version = "0.5.0" dependencies = [ "stackable-versioned-macros", ] [[package]] name = "stackable-versioned-macros" -version = "0.4.1" +version = "0.5.0" dependencies = [ "convert_case", "darling", diff --git a/crates/stackable-operator/src/builder/pod/resources.rs b/crates/stackable-operator/src/builder/pod/resources.rs index 040231457..c559c5f8d 100644 --- a/crates/stackable-operator/src/builder/pod/resources.rs +++ b/crates/stackable-operator/src/builder/pod/resources.rs @@ -5,11 +5,7 @@ use k8s_openapi::{ }; use tracing::warn; -use crate::{ - commons::resources::ResourceRequirementsType, - cpu::{self, CpuQuantity}, - memory::{self, MemoryQuantity}, -}; +use crate::{commons::resources::ResourceRequirementsType, quantity::CpuQuantity}; const RESOURCE_DENYLIST: &[&str] = &["cpu", "memory"]; diff --git a/crates/stackable-operator/src/commons/resources.rs b/crates/stackable-operator/src/commons/resources.rs index fbaaf9f1e..266783a5d 100644 --- a/crates/stackable-operator/src/commons/resources.rs +++ b/crates/stackable-operator/src/commons/resources.rs @@ -68,14 +68,6 @@ //! } //! ``` -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, @@ -89,6 +81,12 @@ use snafu::{ResultExt, Snafu}; use std::{collections::BTreeMap, fmt::Debug}; use strum::Display; +use crate::config::{ + fragment::{Fragment, FromFragment}, + merge::Merge, +}; +use crate::quantity::{CpuQuantity, MemoryQuantity}; + pub const LIMIT_REQUEST_RATIO_CPU: f32 = 5.0; pub const LIMIT_REQUEST_RATIO_MEMORY: f32 = 1.0; 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..dca4da25b 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::{BinaryByteMultiple, MemoryQuantity, Suffix}, role_utils::RoleGroupRef, }; @@ -105,7 +105,7 @@ pub fn calculate_log_volume_size_limit(max_log_files_size: &[MemoryQuantity]) -> .iter() .cloned() .sum::() - .scale_to(BinaryMultiple::Mebi) + .scale_to(Suffix::BinaryByteMultiple(BinaryByteMultiple::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. diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs new file mode 100644 index 000000000..df398c3dc --- /dev/null +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -0,0 +1,38 @@ +use std::{ops::Deref, str::FromStr}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; + +use crate::quantity::{ + macros::forward_from_impls, DecimalByteMultiple, ParseQuantityError, Quantity, Suffix, +}; + +#[derive(Clone, Debug, PartialEq)] +pub struct CpuQuantity(Quantity); + +impl Deref for CpuQuantity { + type Target = Quantity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for CpuQuantity { + type Err = ParseQuantityError; + + fn from_str(input: &str) -> Result { + let quantity = Quantity::from_str(input)?; + Ok(Self(quantity)) + } +} + +forward_from_impls!(Quantity, K8sQuantity, CpuQuantity); + +impl CpuQuantity { + pub fn from_millis(value: u32) -> Self { + CpuQuantity(Quantity { + suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli)), + value: value.into(), + }) + } +} diff --git a/crates/stackable-operator/src/quantity/memory.rs b/crates/stackable-operator/src/quantity/memory.rs new file mode 100644 index 000000000..ab0a66c7f --- /dev/null +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -0,0 +1,53 @@ +use std::{ops::Deref, str::FromStr}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; + +use crate::quantity::{ + macros::forward_from_impls, BinaryByteMultiple, ParseQuantityError, Quantity, 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; +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MemoryQuantity(Quantity); + +impl Deref for MemoryQuantity { + type Target = Quantity; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for MemoryQuantity { + type Err = ParseQuantityError; + + fn from_str(input: &str) -> Result { + let quantity = Quantity::from_str(input)?; + Ok(Self(quantity)) + } +} + +forward_from_impls!(Quantity, K8sQuantity, MemoryQuantity); + +impl MemoryQuantity { + pub const fn from_gibi(value: f64) -> Self { + MemoryQuantity(Quantity { + suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi)), + value, + }) + } + + pub const fn from_mebi(value: f64) -> Self { + MemoryQuantity(Quantity { + suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)), + value, + }) + } +} diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs new file mode 100644 index 000000000..16eb61234 --- /dev/null +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -0,0 +1,549 @@ +use std::{ + fmt::{Display, Write}, + num::ParseFloatError, + ops::Deref, + str::FromStr, +}; + +use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; +use snafu::{ensure, ResultExt as _, Snafu}; + +mod cpu; +mod memory; + +pub use cpu::*; +pub use memory::*; + +#[derive(Debug, Snafu)] +pub enum ParseQuantityError { + #[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: ParseSuffixError }, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Quantity { + // 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 optional suffix of the quantity. + /// + /// This field holds data parsed from `` according to the spec. + suffix: Option, +} + +impl FromStr for Quantity { + type Err = ParseQuantityError; + + fn from_str(input: &str) -> Result { + ensure!(!input.is_empty() && input.is_ascii(), InvalidFormatSnafu); + + if input == "0" { + return Ok(Self { + value: 0.0, + suffix: None, + }); + } + + 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 = Suffix::from_str(parts.1).context(InvalidSuffixSnafu)?; + + Ok(Self { + suffix: Some(suffix), + value, + }) + } + None => { + let value = f64::from_str(input).context(InvalidFloatSnafu)?; + Ok(Self { + value, + suffix: None, + }) + } + } + } +} + +impl Display for Quantity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.value == 0.0 { + return f.write_char('0'); + } + + match &self.suffix { + Some(suffix) => write!(f, "{value}{suffix}", value = self.value,), + None => write!(f, "{value}", value = self.value), + } + } +} + +impl From for K8sQuantity { + fn from(value: Quantity) -> Self { + K8sQuantity(value.to_string()) + } +} + +impl TryFrom for Quantity { + type Error = ParseQuantityError; + + fn try_from(value: K8sQuantity) -> Result { + Quantity::from_str(&value.0) + } +} + +impl TryFrom<&K8sQuantity> for Quantity { + type Error = ParseQuantityError; + + fn try_from(value: &K8sQuantity) -> Result { + Quantity::from_str(&value.0) + } +} + +impl Quantity { + /// Optionally scales up or down to the provided `suffix`. + /// + /// It additionally returns `true` if the suffix was scaled, and `false` if it was not. This can + /// be the case if the suffixes already match, the quantity has no suffix, or if the value is 0. + pub fn scale_to(self, suffix: Suffix) -> (Self, bool) { + match (self.value, &self.suffix) { + (_, None) | (0.0, _) => (self, false), + (_, Some(s)) if *s == suffix => (self, false), + (v, Some(s)) => { + let factor = (s.base() as f64).powf(s.exponent()) + / (suffix.base() as f64).powf(suffix.exponent()); + + let quantity = Self { + value: v * factor, + suffix: Some(suffix), + }; + + (quantity, true) + } + } + } +} + +#[derive(Debug, Snafu)] +#[snafu(display("failed to parse {input:?} as quantity suffix"))] +pub struct ParseSuffixError { + input: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Suffix { + DecimalByteMultiple(DecimalByteMultiple), + BinaryByteMultiple(BinaryByteMultiple), + DecimalExponent(DecimalExponent), +} + +impl FromStr for Suffix { + type Err = ParseSuffixError; + + fn from_str(input: &str) -> Result { + if let Ok(binary_si) = BinaryByteMultiple::from_str(input) { + return Ok(Self::BinaryByteMultiple(binary_si)); + } + + if let Ok(decimal_si) = DecimalByteMultiple::from_str(input) { + return Ok(Self::DecimalByteMultiple(decimal_si)); + } + + if input.starts_with(['e', 'E']) { + if let Ok(decimal_exponent) = f64::from_str(&input[1..]) { + return Ok(Self::DecimalExponent(DecimalExponent(decimal_exponent))); + } + } + + ParseSuffixSnafu { input }.fail() + } +} + +impl Display for Suffix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Suffix::DecimalByteMultiple(decimal) => write!(f, "{decimal}"), + Suffix::BinaryByteMultiple(binary) => write!(f, "{binary}"), + Suffix::DecimalExponent(float) => write!(f, "e{float}"), + } + } +} + +impl Suffix { + pub fn exponent(&self) -> f64 { + match self { + Suffix::DecimalByteMultiple(s) => s.exponent(), + Suffix::BinaryByteMultiple(s) => s.exponent(), + Suffix::DecimalExponent(s) => s.exponent(), + } + } + + pub fn base(&self) -> usize { + match self { + Suffix::DecimalByteMultiple(_) => DecimalByteMultiple::BASE, + Suffix::BinaryByteMultiple(_) => BinaryByteMultiple::BASE, + Suffix::DecimalExponent(_) => DecimalExponent::BASE, + } + } +} + +/// Provides a trait for suffix multiples to provide their base and exponent for each unit variant +/// or scientific notation exponent. +pub trait SuffixMultiple { + /// The base of the multiple. + const BASE: usize; + + /// Returns the exponent based on the unit variant or scientific notation exponent. + fn exponent(&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, Debug, PartialEq, strum::Display, strum::EnumString)] +pub enum BinaryByteMultiple { + #[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 SuffixMultiple for BinaryByteMultiple { + /// The base of the binary byte multiple is 2 because 2^10 = 1024^1 = 1 KiB. + const BASE: usize = 2; + + fn exponent(&self) -> f64 { + match self { + BinaryByteMultiple::Kibi => 10.0, + BinaryByteMultiple::Mebi => 20.0, + BinaryByteMultiple::Gibi => 30.0, + BinaryByteMultiple::Tebi => 40.0, + BinaryByteMultiple::Pebi => 50.0, + BinaryByteMultiple::Exbi => 60.0, + } + } +} + +/// 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, unit-less) +/// - 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, Debug, PartialEq, strum::Display, strum::EnumString)] +pub enum DecimalByteMultiple { + #[strum(serialize = "m")] + Milli, + + #[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 SuffixMultiple for DecimalByteMultiple { + const BASE: usize = 10; + + fn exponent(&self) -> f64 { + match self { + DecimalByteMultiple::Milli => -3.0, + DecimalByteMultiple::Kilo => 3.0, + DecimalByteMultiple::Mega => 6.0, + DecimalByteMultiple::Giga => 9.0, + DecimalByteMultiple::Tera => 12.0, + DecimalByteMultiple::Peta => 15.0, + DecimalByteMultiple::Exa => 18.0, + } + } +} + +/// Scientific (also know as E) notation of numbers. +/// +/// ### See +/// +/// - +#[derive(Clone, Debug, PartialEq)] +pub struct DecimalExponent(f64); + +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 SuffixMultiple for DecimalExponent { + const BASE: usize = 10; + + fn exponent(&self) -> f64 { + self.0 + } +} + +mod macros { + macro_rules! forward_from_impls { + ($q:ty, $kq:ty, $for:ty) => { + impl From<$q> for $for { + fn from(quantity: $q) -> Self { + Self(quantity) + } + } + + impl TryFrom<$kq> for $for { + type Error = ParseQuantityError; + + fn try_from(value: $kq) -> Result { + Ok(Self(Quantity::try_from(value)?)) + } + } + + impl TryFrom<&$kq> for $for { + type Error = ParseQuantityError; + + fn try_from(value: &$kq) -> Result { + Ok(Self(Quantity::try_from(value)?)) + } + } + }; + } + + pub(crate) use forward_from_impls; +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("Ki", Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi))] + #[case("Mi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi))] + #[case("Gi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi))] + #[case("Ti", Suffix::BinaryByteMultiple(BinaryByteMultiple::Tebi))] + #[case("Pi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Pebi))] + #[case("Ei", Suffix::BinaryByteMultiple(BinaryByteMultiple::Exbi))] + fn binary_byte_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + let parsed = Suffix::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("m", Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli))] + #[case("k", Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo))] + #[case("M", Suffix::DecimalByteMultiple(DecimalByteMultiple::Mega))] + #[case("G", Suffix::DecimalByteMultiple(DecimalByteMultiple::Giga))] + #[case("T", Suffix::DecimalByteMultiple(DecimalByteMultiple::Tera))] + #[case("P", Suffix::DecimalByteMultiple(DecimalByteMultiple::Peta))] + #[case("E", Suffix::DecimalByteMultiple(DecimalByteMultiple::Exa))] + fn decimal_byte_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + let parsed = Suffix::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("49041204Ki", Quantity { value: 49041204.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi)) })] + #[case("256Ki", Quantity { value: 256.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi)) })] + #[case("1.5Gi", Quantity { value: 1.5, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi)) })] + #[case("0.8Ti", Quantity { value: 0.8, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Tebi)) })] + #[case("3.2Pi", Quantity { value: 3.2, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Pebi)) })] + #[case("0.2Ei", Quantity { value: 0.2, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Exbi)) })] + #[case("8Mi", Quantity { value: 8.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)) })] + fn binary_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { + let parsed = Quantity::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("49041204k", Quantity { value: 49041204.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo)) })] + #[case("256k", Quantity { value: 256.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo)) })] + #[case("1.5G", Quantity { value: 1.5, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Giga)) })] + #[case("0.8T", Quantity { value: 0.8, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Tera)) })] + #[case("3.2P", Quantity { value: 3.2, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Peta)) })] + #[case("0.2E", Quantity { value: 0.2, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Exa)) })] + #[case("4m", Quantity { value: 4.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli)) })] + #[case("8M", Quantity { value: 8.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Mega)) })] + fn decimal_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { + let parsed = Quantity::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("1.234e-3.21", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(-3.21))) })] + #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(-3.21))) })] + #[case("1.234e3", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(3.0))) })] + #[case("1.234E3", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(3.0))) })] + fn decimal_exponent_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { + let parsed = Quantity::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[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", BinaryByteMultiple::Kibi, "1024Ki", true)] + #[case("1024Ki", BinaryByteMultiple::Mebi, "1Mi", true)] + #[case("1Mi", BinaryByteMultiple::Mebi, "1Mi", false)] + fn binary_byte_to_binary_byte_scale_pass( + #[case] input: &str, + #[case] scale_to: BinaryByteMultiple, + #[case] output: &str, + #[case] scaled: bool, + ) { + let parsed = Quantity::from_str(input).unwrap(); + let (quantity, was_scaled) = parsed.scale_to(Suffix::BinaryByteMultiple(scale_to)); + + assert_eq!(quantity.to_string(), output); + assert_eq!(was_scaled, scaled); + } + + #[rstest] + #[case("1Mi", DecimalByteMultiple::Kilo, "1048.576k", true)] + #[case("1Mi", DecimalByteMultiple::Mega, "1.048576M", true)] + fn binary_byte_to_decimal_byte_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalByteMultiple, + #[case] output: &str, + #[case] scaled: bool, + ) { + let parsed = Quantity::from_str(input).unwrap(); + let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + + assert_eq!(quantity.to_string(), output); + assert_eq!(was_scaled, scaled); + } + + #[rstest] + #[case("1M", DecimalByteMultiple::Kilo, "1000k", true)] + #[case("1000k", DecimalByteMultiple::Mega, "1M", true)] + #[case("1M", DecimalByteMultiple::Mega, "1M", false)] + fn decimal_byte_to_decimal_byte_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalByteMultiple, + #[case] output: &str, + #[case] scaled: bool, + ) { + let parsed = Quantity::from_str(input).unwrap(); + let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + + assert_eq!(quantity.to_string(), output); + assert_eq!(was_scaled, scaled); + } + + #[rstest] + #[case("1e3", DecimalExponent(0.0), "1000e0", true)] + #[case("1000e0", DecimalExponent(3.0), "1e3", true)] + #[case("1e3", DecimalExponent(3.0), "1e3", false)] + fn decimal_exponent_to_decimal_exponent_scale_pass( + #[case] input: &str, + #[case] scale_to: DecimalExponent, + #[case] output: &str, + #[case] scaled: bool, + ) { + let parsed = Quantity::from_str(input).unwrap(); + let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalExponent(scale_to)); + + assert_eq!(quantity.to_string(), output); + assert_eq!(was_scaled, scaled); + } +} From 0582bdfd095d1b33395d4edfcf56eb5a0ece0678 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 7 Jan 2025 11:13:43 +0100 Subject: [PATCH 02/11] feat: Add support for math ops --- crates/stackable-operator/src/quantity/cpu.rs | 13 +- .../stackable-operator/src/quantity/macros.rs | 104 ++++++++++++++++ .../stackable-operator/src/quantity/memory.rs | 15 ++- crates/stackable-operator/src/quantity/mod.rs | 32 +---- crates/stackable-operator/src/quantity/ops.rs | 111 ++++++++++++++++++ 5 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 crates/stackable-operator/src/quantity/macros.rs create mode 100644 crates/stackable-operator/src/quantity/ops.rs diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs index df398c3dc..1f11e4f14 100644 --- a/crates/stackable-operator/src/quantity/cpu.rs +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -3,7 +3,8 @@ use std::{ops::Deref, str::FromStr}; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; use crate::quantity::{ - macros::forward_from_impls, DecimalByteMultiple, ParseQuantityError, Quantity, Suffix, + macros::{forward_from_impls, forward_op_impls}, + DecimalByteMultiple, ParseQuantityError, Quantity, Suffix, }; #[derive(Clone, Debug, PartialEq)] @@ -27,6 +28,16 @@ impl FromStr for CpuQuantity { } forward_from_impls!(Quantity, K8sQuantity, CpuQuantity); +forward_op_impls!( + CpuQuantity(Quantity { + value: 0.0, + suffix: None, + }), + CpuQuantity, + usize, + f32, + f64 +); impl CpuQuantity { pub fn from_millis(value: u32) -> Self { diff --git a/crates/stackable-operator/src/quantity/macros.rs b/crates/stackable-operator/src/quantity/macros.rs new file mode 100644 index 000000000..e3d5d6b46 --- /dev/null +++ b/crates/stackable-operator/src/quantity/macros.rs @@ -0,0 +1,104 @@ +/// This macro is intended to be used to implement the From and TryFrom traits on specialized +/// quantities. +/// +/// Currently two specialized quantities exist: [`MemoryQuantity`][1] and [`CpuQuantity`][2]. +/// The traits are implemented by forwarding to the inner [`Quantity`][3] implementation. Both +/// specialized quantities are just newtypes / wrappers around [`Quantity`][3]. +/// +/// [1]: super::MemoryQuantity +/// [2]: super::CpuQuantity +/// [3]: super::Quantity +macro_rules! forward_from_impls { + ($q:ty, $kq:ty, $for:ty) => { + impl From<$q> for $for { + fn from(quantity: $q) -> Self { + Self(quantity) + } + } + + impl TryFrom<$kq> for $for { + type Error = ParseQuantityError; + + fn try_from(value: $kq) -> Result { + Ok(Self(Quantity::try_from(value)?)) + } + } + + impl TryFrom<&$kq> for $for { + type Error = ParseQuantityError; + + fn try_from(value: &$kq) -> Result { + Ok(Self(Quantity::try_from(value)?)) + } + } + }; +} + +macro_rules! forward_op_impls { + ($acc:expr, $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::iter::Sum for $for { + fn sum>(iter: I) -> Self { + iter.fold( + $acc, + <$for as ::std::ops::Add>::add, + ) + } + } + + $( + 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; diff --git a/crates/stackable-operator/src/quantity/memory.rs b/crates/stackable-operator/src/quantity/memory.rs index ab0a66c7f..6687d4d0f 100644 --- a/crates/stackable-operator/src/quantity/memory.rs +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -3,7 +3,8 @@ use std::{ops::Deref, str::FromStr}; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; use crate::quantity::{ - macros::forward_from_impls, BinaryByteMultiple, ParseQuantityError, Quantity, Suffix, + macros::{forward_from_impls, forward_op_impls}, + BinaryByteMultiple, ParseQuantityError, Quantity, Suffix, }; pub trait JavaHeap { @@ -35,6 +36,18 @@ impl FromStr for MemoryQuantity { } forward_from_impls!(Quantity, K8sQuantity, MemoryQuantity); +forward_op_impls!( + MemoryQuantity(Quantity { + value: 0.0, + // TODO (@Techassi): This needs to be talked about. The previous implementation used Kibi + // here. Code which relies on that fact (for later scaling) will thus break. + suffix: None, + }), + MemoryQuantity, + usize, + f32, + f64 +); impl MemoryQuantity { pub const fn from_gibi(value: f64) -> Self { diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 16eb61234..4cfe1cab9 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -9,7 +9,9 @@ use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; use snafu::{ensure, ResultExt as _, Snafu}; mod cpu; +mod macros; mod memory; +mod ops; pub use cpu::*; pub use memory::*; @@ -369,36 +371,6 @@ impl SuffixMultiple for DecimalExponent { } } -mod macros { - macro_rules! forward_from_impls { - ($q:ty, $kq:ty, $for:ty) => { - impl From<$q> for $for { - fn from(quantity: $q) -> Self { - Self(quantity) - } - } - - impl TryFrom<$kq> for $for { - type Error = ParseQuantityError; - - fn try_from(value: $kq) -> Result { - Ok(Self(Quantity::try_from(value)?)) - } - } - - impl TryFrom<&$kq> for $for { - type Error = ParseQuantityError; - - fn try_from(value: &$kq) -> Result { - Ok(Self(Quantity::try_from(value)?)) - } - } - }; - } - - pub(crate) use forward_from_impls; -} - #[cfg(test)] mod test { use super::*; diff --git a/crates/stackable-operator/src/quantity/ops.rs b/crates/stackable-operator/src/quantity/ops.rs new file mode 100644 index 000000000..608d4c1ca --- /dev/null +++ b/crates/stackable-operator/src/quantity/ops.rs @@ -0,0 +1,111 @@ +use std::{ + iter::Sum, + 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 { + Self { + value: self.value + rhs.value, + ..self + } + } +} + +impl AddAssign for Quantity { + fn add_assign(&mut self, rhs: Quantity) { + self.value += rhs.value + } +} + +impl Sub for Quantity { + type Output = Quantity; + + fn sub(self, rhs: Quantity) -> Self::Output { + Self { + value: self.value - rhs.value, + ..self + } + } +} + +impl SubAssign for Quantity { + fn sub_assign(&mut self, rhs: Self) { + self.value -= rhs.value + } +} + +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.value *= rhs as f64 + } +} + +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.value *= rhs as f64 + } +} + +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.value *= rhs + } +} + +impl Div for Quantity { + type Output = f64; + + fn div(self, rhs: Self) -> Self::Output { + self.value / rhs.value + } +} + +impl Sum for Quantity { + fn sum>(iter: I) -> Self { + iter.fold( + Quantity { + value: 0.0, + suffix: None, + }, + Quantity::add, + ) + } +} From 7f29068b7ec661491d98cfa36dbe6b9943b1b921 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 7 Jan 2025 11:18:50 +0100 Subject: [PATCH 03/11] feat: Implement DerefMut to access scale_to and ceil functions --- crates/stackable-operator/src/quantity/cpu.rs | 17 +++++++++- .../stackable-operator/src/quantity/memory.rs | 17 +++++++++- crates/stackable-operator/src/quantity/mod.rs | 31 ++++++++++++------- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs index 1f11e4f14..80dd5847b 100644 --- a/crates/stackable-operator/src/quantity/cpu.rs +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -1,4 +1,7 @@ -use std::{ops::Deref, str::FromStr}; +use std::{ + ops::{Deref, DerefMut}, + str::FromStr, +}; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; @@ -18,6 +21,12 @@ impl Deref for CpuQuantity { } } +impl DerefMut for CpuQuantity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl FromStr for CpuQuantity { type Err = ParseQuantityError; @@ -27,6 +36,12 @@ impl FromStr for CpuQuantity { } } +impl From for K8sQuantity { + fn from(value: CpuQuantity) -> Self { + K8sQuantity(value.to_string()) + } +} + forward_from_impls!(Quantity, K8sQuantity, CpuQuantity); forward_op_impls!( CpuQuantity(Quantity { diff --git a/crates/stackable-operator/src/quantity/memory.rs b/crates/stackable-operator/src/quantity/memory.rs index 6687d4d0f..b3ccd6683 100644 --- a/crates/stackable-operator/src/quantity/memory.rs +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -1,4 +1,7 @@ -use std::{ops::Deref, str::FromStr}; +use std::{ + ops::{Deref, DerefMut}, + str::FromStr, +}; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; @@ -26,6 +29,12 @@ impl Deref for MemoryQuantity { } } +impl DerefMut for MemoryQuantity { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl FromStr for MemoryQuantity { type Err = ParseQuantityError; @@ -35,6 +44,12 @@ impl FromStr for MemoryQuantity { } } +impl From for K8sQuantity { + fn from(value: MemoryQuantity) -> Self { + K8sQuantity(value.to_string()) + } +} + forward_from_impls!(Quantity, K8sQuantity, MemoryQuantity); forward_op_impls!( MemoryQuantity(Quantity { diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 4cfe1cab9..6ba080162 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -115,24 +115,33 @@ impl TryFrom<&K8sQuantity> for Quantity { } impl Quantity { + // TODO (@Techassi): Discuss if this should consume or mutate in place. Consumption requires us + // to add these function on specialized quantities (which then forward). If we mutate in place, + // we could leverage the Deref impl instead. + /// Optionally scales up or down to the provided `suffix`. /// - /// It additionally returns `true` if the suffix was scaled, and `false` if it was not. This can - /// be the case if the suffixes already match, the quantity has no suffix, or if the value is 0. - pub fn scale_to(self, suffix: Suffix) -> (Self, bool) { - match (self.value, &self.suffix) { - (_, None) | (0.0, _) => (self, false), - (_, Some(s)) if *s == suffix => (self, false), + /// It additionally returns `true` if the suffix was scaled, and `false` in the following cases: + /// + /// - the suffixes already match + /// - the quantity has no suffix, in which case the suffix will be added without scaling + /// - the value is 0 + pub fn scale_to(&mut self, suffix: Suffix) -> bool { + match (&mut self.value, &mut self.suffix) { + (0.0, _) => false, + (_, Some(s)) if *s == suffix => false, + (_, None) => { + self.suffix = Some(suffix); + false + } (v, Some(s)) => { let factor = (s.base() as f64).powf(s.exponent()) / (suffix.base() as f64).powf(suffix.exponent()); - let quantity = Self { - value: v * factor, - suffix: Some(suffix), - }; + *v = *v * factor; + *s = suffix; - (quantity, true) + false } } } From dec9539dc12a198a43905ce37b2832ebcf3cabc3 Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 7 Jan 2025 11:20:07 +0100 Subject: [PATCH 04/11] chore: Make code work again --- .../src/builder/pod/resources.rs | 9 ++-- .../src/commons/resources.rs | 46 +++++++++++-------- .../src/product_logging/framework.rs | 11 ++--- crates/stackable-operator/src/quantity/cpu.rs | 2 +- .../stackable-operator/src/quantity/memory.rs | 2 +- crates/stackable-operator/src/quantity/mod.rs | 44 ++++++++++-------- 6 files changed, 63 insertions(+), 51 deletions(-) diff --git a/crates/stackable-operator/src/builder/pod/resources.rs b/crates/stackable-operator/src/builder/pod/resources.rs index c559c5f8d..6ed6ef218 100644 --- a/crates/stackable-operator/src/builder/pod/resources.rs +++ b/crates/stackable-operator/src/builder/pod/resources.rs @@ -5,7 +5,10 @@ use k8s_openapi::{ }; use tracing::warn; -use crate::{commons::resources::ResourceRequirementsType, quantity::CpuQuantity}; +use crate::{ + commons::resources::ResourceRequirementsType, + quantity::{CpuQuantity, MemoryQuantity, ParseQuantityError}, +}; const RESOURCE_DENYLIST: &[&str] = &["cpu", "memory"]; @@ -50,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; @@ -120,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 266783a5d..f08682724 100644 --- a/crates/stackable-operator/src/commons/resources.rs +++ b/crates/stackable-operator/src/commons/resources.rs @@ -67,28 +67,34 @@ //! shared_storage: PvcConfig, //! } //! ``` +use std::{collections::BTreeMap, fmt::Debug}; 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; -use crate::config::{ - fragment::{Fragment, FromFragment}, - merge::Merge, +use crate::{ + config::{ + fragment::{Fragment, FromFragment}, + merge::Merge, + }, + quantity::{CpuQuantity, MemoryQuantity, ParseQuantityError}, }; -use crate::quantity::{CpuQuantity, MemoryQuantity}; -pub const LIMIT_REQUEST_RATIO_CPU: f32 = 5.0; -pub const LIMIT_REQUEST_RATIO_MEMORY: f32 = 1.0; +pub const LIMIT_REQUEST_RATIO_CPU: f64 = 5.0; +pub const LIMIT_REQUEST_RATIO_MEMORY: f64 = 1.0; type Result = std::result::Result; @@ -107,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 @@ -427,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<()>; } @@ -457,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() @@ -509,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/product_logging/framework.rs b/crates/stackable-operator/src/product_logging/framework.rs index dca4da25b..49d20398c 100644 --- a/crates/stackable-operator/src/product_logging/framework.rs +++ b/crates/stackable-operator/src/product_logging/framework.rs @@ -101,17 +101,16 @@ pub enum LoggingError { /// .unwrap(); /// ``` pub fn calculate_log_volume_size_limit(max_log_files_size: &[MemoryQuantity]) -> Quantity { - let log_volume_size_limit = max_log_files_size - .iter() - .cloned() - .sum::() - .scale_to(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)) + let mut log_volume_size_limit = max_log_files_size.iter().cloned().sum::(); + log_volume_size_limit.scale_to(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)); + log_volume_size_limit // 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() } diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs index 80dd5847b..24791e54c 100644 --- a/crates/stackable-operator/src/quantity/cpu.rs +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -10,7 +10,7 @@ use crate::quantity::{ DecimalByteMultiple, ParseQuantityError, Quantity, Suffix, }; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct CpuQuantity(Quantity); impl Deref for CpuQuantity { diff --git a/crates/stackable-operator/src/quantity/memory.rs b/crates/stackable-operator/src/quantity/memory.rs index b3ccd6683..b1be91440 100644 --- a/crates/stackable-operator/src/quantity/memory.rs +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -18,7 +18,7 @@ pub trait JavaHeap { fn to_java_heap_string(&self) -> Result; } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct MemoryQuantity(Quantity); impl Deref for MemoryQuantity { diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 6ba080162..3c9dfdd7d 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -16,7 +16,7 @@ mod ops; pub use cpu::*; pub use memory::*; -#[derive(Debug, Snafu)] +#[derive(Debug, PartialEq, Snafu)] pub enum ParseQuantityError { #[snafu(display("input is either empty or contains non-ascii characters"))] InvalidFormat, @@ -28,7 +28,7 @@ pub enum ParseQuantityError { InvalidSuffix { source: ParseSuffixError }, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct Quantity { // FIXME (@Techassi): Support arbitrary-precision numbers /// The numeric value of the quantity. @@ -138,22 +138,26 @@ impl Quantity { let factor = (s.base() as f64).powf(s.exponent()) / (suffix.base() as f64).powf(suffix.exponent()); - *v = *v * factor; + *v *= factor; *s = suffix; false } } } + + pub fn ceil(&mut self) { + self.value = self.value.ceil(); + } } -#[derive(Debug, Snafu)] +#[derive(Debug, PartialEq, Snafu)] #[snafu(display("failed to parse {input:?} as quantity suffix"))] pub struct ParseSuffixError { input: String, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub enum Suffix { DecimalByteMultiple(DecimalByteMultiple), BinaryByteMultiple(BinaryByteMultiple), @@ -244,7 +248,7 @@ pub trait SuffixMultiple { /// - /// /// [k8s-serialization-format]: https://github.com/kubernetes/apimachinery/blob/8c60292e48e46c4faa1e92acb232ce6adb37512c/pkg/api/resource/quantity.go#L37-L59 -#[derive(Clone, Debug, PartialEq, strum::Display, strum::EnumString)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, strum::Display, strum::EnumString)] pub enum BinaryByteMultiple { #[strum(serialize = "Ki")] Kibi, @@ -310,7 +314,7 @@ impl SuffixMultiple for BinaryByteMultiple { /// - /// /// [k8s-serialization-format]: https://github.com/kubernetes/apimachinery/blob/8c60292e48e46c4faa1e92acb232ce6adb37512c/pkg/api/resource/quantity.go#L37-L59 -#[derive(Clone, Debug, PartialEq, strum::Display, strum::EnumString)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, strum::Display, strum::EnumString)] pub enum DecimalByteMultiple { #[strum(serialize = "m")] Milli, @@ -355,7 +359,7 @@ impl SuffixMultiple for DecimalByteMultiple { /// ### See /// /// - -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct DecimalExponent(f64); impl Deref for DecimalExponent { @@ -471,10 +475,10 @@ mod test { #[case] output: &str, #[case] scaled: bool, ) { - let parsed = Quantity::from_str(input).unwrap(); - let (quantity, was_scaled) = parsed.scale_to(Suffix::BinaryByteMultiple(scale_to)); + let mut parsed = Quantity::from_str(input).unwrap(); + let was_scaled = parsed.scale_to(Suffix::BinaryByteMultiple(scale_to)); - assert_eq!(quantity.to_string(), output); + assert_eq!(parsed.to_string(), output); assert_eq!(was_scaled, scaled); } @@ -487,10 +491,10 @@ mod test { #[case] output: &str, #[case] scaled: bool, ) { - let parsed = Quantity::from_str(input).unwrap(); - let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + let mut parsed = Quantity::from_str(input).unwrap(); + let was_scaled = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); - assert_eq!(quantity.to_string(), output); + assert_eq!(parsed.to_string(), output); assert_eq!(was_scaled, scaled); } @@ -504,10 +508,10 @@ mod test { #[case] output: &str, #[case] scaled: bool, ) { - let parsed = Quantity::from_str(input).unwrap(); - let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + let mut parsed = Quantity::from_str(input).unwrap(); + let was_scaled = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); - assert_eq!(quantity.to_string(), output); + assert_eq!(parsed.to_string(), output); assert_eq!(was_scaled, scaled); } @@ -521,10 +525,10 @@ mod test { #[case] output: &str, #[case] scaled: bool, ) { - let parsed = Quantity::from_str(input).unwrap(); - let (quantity, was_scaled) = parsed.scale_to(Suffix::DecimalExponent(scale_to)); + let mut parsed = Quantity::from_str(input).unwrap(); + let was_scaled = parsed.scale_to(Suffix::DecimalExponent(scale_to)); - assert_eq!(quantity.to_string(), output); + assert_eq!(parsed.to_string(), output); assert_eq!(was_scaled, scaled); } } From d26fb07926706ed672ae8772c6334294e008d86c Mon Sep 17 00:00:00 2001 From: Techassi Date: Tue, 7 Jan 2025 11:27:52 +0100 Subject: [PATCH 05/11] chore: Correct upsie --- crates/stackable-operator/src/quantity/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 3c9dfdd7d..3b19ba4cc 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -141,7 +141,7 @@ impl Quantity { *v *= factor; *s = suffix; - false + true } } } From 102d4b6f54c037bf94e291c383c5f6c2495cad76 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 8 Jan 2025 13:51:36 +0100 Subject: [PATCH 06/11] fix: Scale rhs in math ops --- crates/stackable-operator/src/quantity/cpu.rs | 54 +++++-------- .../stackable-operator/src/quantity/macros.rs | 52 ++++++------ .../stackable-operator/src/quantity/memory.rs | 81 +++++++++---------- crates/stackable-operator/src/quantity/ops.rs | 32 +++----- 4 files changed, 94 insertions(+), 125 deletions(-) diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs index 24791e54c..58a9f646d 100644 --- a/crates/stackable-operator/src/quantity/cpu.rs +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -1,14 +1,11 @@ use std::{ - ops::{Deref, DerefMut}, - str::FromStr, + iter::Sum, + ops::{Add, Deref}, }; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; -use crate::quantity::{ - macros::{forward_from_impls, forward_op_impls}, - DecimalByteMultiple, ParseQuantityError, Quantity, Suffix, -}; +use crate::quantity::{macros::forward_quantity_impls, DecimalMultiple, Quantity, Suffix}; #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct CpuQuantity(Quantity); @@ -21,44 +18,35 @@ impl Deref for CpuQuantity { } } -impl DerefMut for CpuQuantity { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromStr for CpuQuantity { - type Err = ParseQuantityError; - - fn from_str(input: &str) -> Result { - let quantity = Quantity::from_str(input)?; - Ok(Self(quantity)) - } -} - impl From for K8sQuantity { fn from(value: CpuQuantity) -> Self { K8sQuantity(value.to_string()) } } -forward_from_impls!(Quantity, K8sQuantity, CpuQuantity); -forward_op_impls!( - CpuQuantity(Quantity { - value: 0.0, - suffix: None, - }), - CpuQuantity, - usize, - f32, - f64 -); +impl Sum for CpuQuantity { + fn sum>(iter: I) -> Self { + iter.fold( + CpuQuantity(Quantity { + value: 0.0, + suffix: Suffix::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: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli)), + suffix: Suffix::DecimalMultiple(DecimalMultiple::Milli), value: value.into(), }) } + + pub fn scale_to(self, suffix: Suffix) -> 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 index e3d5d6b46..18e0cae04 100644 --- a/crates/stackable-operator/src/quantity/macros.rs +++ b/crates/stackable-operator/src/quantity/macros.rs @@ -1,31 +1,37 @@ -/// This macro is intended to be used to implement the From and TryFrom traits on specialized -/// quantities. -/// -/// Currently two specialized quantities exist: [`MemoryQuantity`][1] and [`CpuQuantity`][2]. -/// The traits are implemented by forwarding to the inner [`Quantity`][3] implementation. Both -/// specialized quantities are just newtypes / wrappers around [`Quantity`][3]. -/// -/// [1]: super::MemoryQuantity -/// [2]: super::CpuQuantity -/// [3]: super::Quantity +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 { - ($q:ty, $kq:ty, $for:ty) => { - impl From<$q> for $for { - fn from(quantity: $q) -> Self { + ($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 = ParseQuantityError; + type Error = $crate::quantity::ParseQuantityError; fn try_from(value: $kq) -> Result { - Ok(Self(Quantity::try_from(value)?)) + Ok(Self($crate::quantity::Quantity::try_from(value)?)) } } impl TryFrom<&$kq> for $for { - type Error = ParseQuantityError; + type Error = $crate::quantity::ParseQuantityError; fn try_from(value: &$kq) -> Result { Ok(Self(Quantity::try_from(value)?)) @@ -35,7 +41,7 @@ macro_rules! forward_from_impls { } macro_rules! forward_op_impls { - ($acc:expr, $for:ty, $($on:ty),+) => { + ($for:ty, $($on:ty),+) => { impl ::std::ops::Add for $for { type Output = $for; @@ -72,15 +78,6 @@ macro_rules! forward_op_impls { } } - impl ::std::iter::Sum for $for { - fn sum>(iter: I) -> Self { - iter.fold( - $acc, - <$for as ::std::ops::Add>::add, - ) - } - } - $( impl ::std::ops::Mul<$on> for $for { type Output = $for; @@ -99,6 +96,7 @@ macro_rules! forward_op_impls { }; } -/// HACK: Make the macros only available in this crate. +// 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 index b1be91440..e60401da4 100644 --- a/crates/stackable-operator/src/quantity/memory.rs +++ b/crates/stackable-operator/src/quantity/memory.rs @@ -1,22 +1,11 @@ use std::{ - ops::{Deref, DerefMut}, - str::FromStr, + iter::Sum, + ops::{Add, Deref}, }; use k8s_openapi::apimachinery::pkg::api::resource::Quantity as K8sQuantity; -use crate::quantity::{ - macros::{forward_from_impls, forward_op_impls}, - BinaryByteMultiple, ParseQuantityError, Quantity, 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; -} +use crate::quantity::{macros::forward_quantity_impls, BinaryMultiple, Quantity, Suffix}; #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct MemoryQuantity(Quantity); @@ -29,53 +18,57 @@ impl Deref for MemoryQuantity { } } -impl DerefMut for MemoryQuantity { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl FromStr for MemoryQuantity { - type Err = ParseQuantityError; - - fn from_str(input: &str) -> Result { - let quantity = Quantity::from_str(input)?; - Ok(Self(quantity)) - } -} - impl From for K8sQuantity { fn from(value: MemoryQuantity) -> Self { K8sQuantity(value.to_string()) } } -forward_from_impls!(Quantity, K8sQuantity, MemoryQuantity); -forward_op_impls!( - MemoryQuantity(Quantity { - value: 0.0, - // TODO (@Techassi): This needs to be talked about. The previous implementation used Kibi - // here. Code which relies on that fact (for later scaling) will thus break. - suffix: None, - }), - MemoryQuantity, - usize, - f32, - f64 -); +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: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi)), + suffix: Suffix::BinaryMultiple(BinaryMultiple::Gibi), value, }) } pub const fn from_mebi(value: f64) -> Self { MemoryQuantity(Quantity { - suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)), + 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/ops.rs b/crates/stackable-operator/src/quantity/ops.rs index 608d4c1ca..a6ef1f92a 100644 --- a/crates/stackable-operator/src/quantity/ops.rs +++ b/crates/stackable-operator/src/quantity/ops.rs @@ -1,7 +1,4 @@ -use std::{ - iter::Sum, - ops::{Add, AddAssign, Div, Mul, MulAssign, Sub, SubAssign}, -}; +use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Sub, SubAssign}; use crate::quantity::Quantity; @@ -9,6 +6,8 @@ impl Add for Quantity { type Output = Quantity; fn add(self, rhs: Quantity) -> Self::Output { + let rhs = rhs.scale_to(self.suffix); + Self { value: self.value + rhs.value, ..self @@ -18,7 +17,7 @@ impl Add for Quantity { impl AddAssign for Quantity { fn add_assign(&mut self, rhs: Quantity) { - self.value += rhs.value + *self = self.add(rhs) } } @@ -26,6 +25,8 @@ impl Sub for Quantity { type Output = Quantity; fn sub(self, rhs: Quantity) -> Self::Output { + let rhs = rhs.scale_to(self.suffix); + Self { value: self.value - rhs.value, ..self @@ -35,7 +36,7 @@ impl Sub for Quantity { impl SubAssign for Quantity { fn sub_assign(&mut self, rhs: Self) { - self.value -= rhs.value + *self = self.sub(rhs) } } @@ -52,7 +53,7 @@ impl Mul for Quantity { impl MulAssign for Quantity { fn mul_assign(&mut self, rhs: usize) { - self.value *= rhs as f64 + *self = self.mul(rhs) } } @@ -69,7 +70,7 @@ impl Mul for Quantity { impl MulAssign for Quantity { fn mul_assign(&mut self, rhs: f32) { - self.value *= rhs as f64 + *self = self.mul(rhs) } } @@ -86,7 +87,7 @@ impl Mul for Quantity { impl MulAssign for Quantity { fn mul_assign(&mut self, rhs: f64) { - self.value *= rhs + *self = self.mul(rhs) } } @@ -94,18 +95,7 @@ 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 } } - -impl Sum for Quantity { - fn sum>(iter: I) -> Self { - iter.fold( - Quantity { - value: 0.0, - suffix: None, - }, - Quantity::add, - ) - } -} From 9c29483b0b96172902a8d10d875d0256a6328990 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 8 Jan 2025 13:54:56 +0100 Subject: [PATCH 07/11] refactor: Make suffix non-optional, change methods accordingly --- .../src/product_logging/framework.rs | 28 +- crates/stackable-operator/src/quantity/mod.rs | 300 ++++++++---------- 2 files changed, 149 insertions(+), 179 deletions(-) diff --git a/crates/stackable-operator/src/product_logging/framework.rs b/crates/stackable-operator/src/product_logging/framework.rs index 49d20398c..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, - quantity::{BinaryByteMultiple, MemoryQuantity, Suffix}, + 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()) @@ -101,9 +95,11 @@ pub enum LoggingError { /// .unwrap(); /// ``` pub fn calculate_log_volume_size_limit(max_log_files_size: &[MemoryQuantity]) -> Quantity { - let mut log_volume_size_limit = max_log_files_size.iter().cloned().sum::(); - log_volume_size_limit.scale_to(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)); - log_volume_size_limit + let log_volume_size_limit = max_log_files_size + .iter() + .cloned() + .sum::() + .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. @@ -1527,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"])] @@ -1541,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/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 3b19ba4cc..1677effe1 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -38,10 +38,10 @@ pub struct Quantity { /// need to support these huge numbers. value: f64, - /// The optional suffix of the quantity. + /// The suffix of the quantity. /// /// This field holds data parsed from `` according to the spec. - suffix: Option, + suffix: Suffix, } impl FromStr for Quantity { @@ -52,8 +52,8 @@ impl FromStr for Quantity { if input == "0" { return Ok(Self { + suffix: Suffix::DecimalMultiple(DecimalMultiple::Empty), value: 0.0, - suffix: None, }); } @@ -63,16 +63,14 @@ impl FromStr for Quantity { let value = f64::from_str(parts.0).context(InvalidFloatSnafu)?; let suffix = Suffix::from_str(parts.1).context(InvalidSuffixSnafu)?; - Ok(Self { - suffix: Some(suffix), - value, - }) + Ok(Self { suffix, value }) } None => { let value = f64::from_str(input).context(InvalidFloatSnafu)?; + Ok(Self { + suffix: Suffix::DecimalMultiple(DecimalMultiple::Empty), value, - suffix: None, }) } } @@ -85,10 +83,12 @@ impl Display for Quantity { return f.write_char('0'); } - match &self.suffix { - Some(suffix) => write!(f, "{value}{suffix}", value = self.value,), - None => write!(f, "{value}", value = self.value), - } + write!( + f, + "{value}{suffix}", + value = self.value, + suffix = self.suffix + ) } } @@ -98,6 +98,12 @@ impl From for K8sQuantity { } } +impl From<&Quantity> for K8sQuantity { + fn from(value: &Quantity) -> Self { + K8sQuantity(value.to_string()) + } +} + impl TryFrom for Quantity { type Error = ParseQuantityError; @@ -115,40 +121,27 @@ impl TryFrom<&K8sQuantity> for Quantity { } impl Quantity { - // TODO (@Techassi): Discuss if this should consume or mutate in place. Consumption requires us - // to add these function on specialized quantities (which then forward). If we mutate in place, - // we could leverage the Deref impl instead. - /// Optionally scales up or down to the provided `suffix`. /// - /// It additionally returns `true` if the suffix was scaled, and `false` in the following cases: + /// This function returns a value pair which contains an optional [`Quantity`] and a bool + /// indicating if the function performed any scaling. It returns `false` in the following cases: /// /// - the suffixes already match - /// - the quantity has no suffix, in which case the suffix will be added without scaling /// - the value is 0 - pub fn scale_to(&mut self, suffix: Suffix) -> bool { - match (&mut self.value, &mut self.suffix) { - (0.0, _) => false, - (_, Some(s)) if *s == suffix => false, - (_, None) => { - self.suffix = Some(suffix); - false - } - (v, Some(s)) => { - let factor = (s.base() as f64).powf(s.exponent()) - / (suffix.base() as f64).powf(suffix.exponent()); - - *v *= factor; - *s = suffix; - - true + pub fn scale_to(self, suffix: Suffix) -> 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 ceil(&mut self) { - self.value = self.value.ceil(); - } } #[derive(Debug, PartialEq, Snafu)] @@ -159,8 +152,8 @@ pub struct ParseSuffixError { #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub enum Suffix { - DecimalByteMultiple(DecimalByteMultiple), - BinaryByteMultiple(BinaryByteMultiple), + DecimalMultiple(DecimalMultiple), + BinaryMultiple(BinaryMultiple), DecimalExponent(DecimalExponent), } @@ -168,12 +161,12 @@ impl FromStr for Suffix { type Err = ParseSuffixError; fn from_str(input: &str) -> Result { - if let Ok(binary_si) = BinaryByteMultiple::from_str(input) { - return Ok(Self::BinaryByteMultiple(binary_si)); + if let Ok(binary_si) = BinaryMultiple::from_str(input) { + return Ok(Self::BinaryMultiple(binary_si)); } - if let Ok(decimal_si) = DecimalByteMultiple::from_str(input) { - return Ok(Self::DecimalByteMultiple(decimal_si)); + if let Ok(decimal_si) = DecimalMultiple::from_str(input) { + return Ok(Self::DecimalMultiple(decimal_si)); } if input.starts_with(['e', 'E']) { @@ -189,41 +182,23 @@ impl FromStr for Suffix { impl Display for Suffix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Suffix::DecimalByteMultiple(decimal) => write!(f, "{decimal}"), - Suffix::BinaryByteMultiple(binary) => write!(f, "{binary}"), + Suffix::DecimalMultiple(decimal) => write!(f, "{decimal}"), + Suffix::BinaryMultiple(binary) => write!(f, "{binary}"), Suffix::DecimalExponent(float) => write!(f, "e{float}"), } } } impl Suffix { - pub fn exponent(&self) -> f64 { + pub fn factor(&self) -> f64 { match self { - Suffix::DecimalByteMultiple(s) => s.exponent(), - Suffix::BinaryByteMultiple(s) => s.exponent(), - Suffix::DecimalExponent(s) => s.exponent(), - } - } - - pub fn base(&self) -> usize { - match self { - Suffix::DecimalByteMultiple(_) => DecimalByteMultiple::BASE, - Suffix::BinaryByteMultiple(_) => BinaryByteMultiple::BASE, - Suffix::DecimalExponent(_) => DecimalExponent::BASE, + Suffix::DecimalMultiple(s) => s.factor(), + Suffix::BinaryMultiple(s) => s.factor(), + Suffix::DecimalExponent(s) => s.factor(), } } } -/// Provides a trait for suffix multiples to provide their base and exponent for each unit variant -/// or scientific notation exponent. -pub trait SuffixMultiple { - /// The base of the multiple. - const BASE: usize; - - /// Returns the exponent based on the unit variant or scientific notation exponent. - fn exponent(&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. @@ -249,7 +224,7 @@ pub trait SuffixMultiple { /// /// [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 BinaryByteMultiple { +pub enum BinaryMultiple { #[strum(serialize = "Ki")] Kibi, @@ -269,18 +244,16 @@ pub enum BinaryByteMultiple { Exbi, } -impl SuffixMultiple for BinaryByteMultiple { - /// The base of the binary byte multiple is 2 because 2^10 = 1024^1 = 1 KiB. - const BASE: usize = 2; - - fn exponent(&self) -> f64 { +impl BinaryMultiple { + /// Returns the factor based on powers of 2. + pub fn factor(&self) -> f64 { match self { - BinaryByteMultiple::Kibi => 10.0, - BinaryByteMultiple::Mebi => 20.0, - BinaryByteMultiple::Gibi => 30.0, - BinaryByteMultiple::Tebi => 40.0, - BinaryByteMultiple::Pebi => 50.0, - BinaryByteMultiple::Exbi => 60.0, + 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), } } } @@ -296,7 +269,7 @@ impl SuffixMultiple for BinaryByteMultiple { /// /// ```plain /// - 1000^-1, (m): millibyte (Kubernetes only) -/// - 1000^ 0, B ( ): byte (no suffix, unit-less) +/// - 1000^ 0, B ( ): byte (no suffix) /// - 1000^ 1, kB (k): kilobyte /// - 1000^ 2, MB (M): Megabyte /// - 1000^ 3, GB (G): Gigabyte @@ -315,10 +288,13 @@ impl SuffixMultiple for BinaryByteMultiple { /// /// [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 DecimalByteMultiple { +pub enum DecimalMultiple { #[strum(serialize = "m")] Milli, + #[strum(serialize = "")] + Empty, + #[strum(serialize = "k")] Kilo, @@ -338,23 +314,22 @@ pub enum DecimalByteMultiple { Exa, } -impl SuffixMultiple for DecimalByteMultiple { - const BASE: usize = 10; - - fn exponent(&self) -> f64 { +impl DecimalMultiple { + pub fn factor(&self) -> f64 { match self { - DecimalByteMultiple::Milli => -3.0, - DecimalByteMultiple::Kilo => 3.0, - DecimalByteMultiple::Mega => 6.0, - DecimalByteMultiple::Giga => 9.0, - DecimalByteMultiple::Tera => 12.0, - DecimalByteMultiple::Peta => 15.0, - DecimalByteMultiple::Exa => 18.0, + 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 know as E) notation of numbers. +/// Scientific (also known as E) notation of numbers. /// /// ### See /// @@ -376,11 +351,9 @@ impl Display for DecimalExponent { } } -impl SuffixMultiple for DecimalExponent { - const BASE: usize = 10; - - fn exponent(&self) -> f64 { - self.0 +impl DecimalExponent { + pub fn factor(&self) -> f64 { + 10f64.powf(self.0) } } @@ -390,62 +363,63 @@ mod test { use rstest::rstest; #[rstest] - #[case("Ki", Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi))] - #[case("Mi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi))] - #[case("Gi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi))] - #[case("Ti", Suffix::BinaryByteMultiple(BinaryByteMultiple::Tebi))] - #[case("Pi", Suffix::BinaryByteMultiple(BinaryByteMultiple::Pebi))] - #[case("Ei", Suffix::BinaryByteMultiple(BinaryByteMultiple::Exbi))] - fn binary_byte_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + #[case("Ki", Suffix::BinaryMultiple(BinaryMultiple::Kibi))] + #[case("Mi", Suffix::BinaryMultiple(BinaryMultiple::Mebi))] + #[case("Gi", Suffix::BinaryMultiple(BinaryMultiple::Gibi))] + #[case("Ti", Suffix::BinaryMultiple(BinaryMultiple::Tebi))] + #[case("Pi", Suffix::BinaryMultiple(BinaryMultiple::Pebi))] + #[case("Ei", Suffix::BinaryMultiple(BinaryMultiple::Exbi))] + fn binary_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { let parsed = Suffix::from_str(input).unwrap(); assert_eq!(parsed, expected); } #[rstest] - #[case("m", Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli))] - #[case("k", Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo))] - #[case("M", Suffix::DecimalByteMultiple(DecimalByteMultiple::Mega))] - #[case("G", Suffix::DecimalByteMultiple(DecimalByteMultiple::Giga))] - #[case("T", Suffix::DecimalByteMultiple(DecimalByteMultiple::Tera))] - #[case("P", Suffix::DecimalByteMultiple(DecimalByteMultiple::Peta))] - #[case("E", Suffix::DecimalByteMultiple(DecimalByteMultiple::Exa))] - fn decimal_byte_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + #[case("m", Suffix::DecimalMultiple(DecimalMultiple::Milli))] + #[case("", Suffix::DecimalMultiple(DecimalMultiple::Empty))] + #[case("k", Suffix::DecimalMultiple(DecimalMultiple::Kilo))] + #[case("M", Suffix::DecimalMultiple(DecimalMultiple::Mega))] + #[case("G", Suffix::DecimalMultiple(DecimalMultiple::Giga))] + #[case("T", Suffix::DecimalMultiple(DecimalMultiple::Tera))] + #[case("P", Suffix::DecimalMultiple(DecimalMultiple::Peta))] + #[case("E", Suffix::DecimalMultiple(DecimalMultiple::Exa))] + fn decimal_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { let parsed = Suffix::from_str(input).unwrap(); assert_eq!(parsed, expected); } #[rstest] - #[case("49041204Ki", Quantity { value: 49041204.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi)) })] - #[case("256Ki", Quantity { value: 256.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Kibi)) })] - #[case("1.5Gi", Quantity { value: 1.5, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Gibi)) })] - #[case("0.8Ti", Quantity { value: 0.8, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Tebi)) })] - #[case("3.2Pi", Quantity { value: 3.2, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Pebi)) })] - #[case("0.2Ei", Quantity { value: 0.2, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Exbi)) })] - #[case("8Mi", Quantity { value: 8.0, suffix: Some(Suffix::BinaryByteMultiple(BinaryByteMultiple::Mebi)) })] + #[case("49041204Ki", Quantity { value: 49041204.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] + #[case("256Ki", Quantity { value: 256.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] + #[case("1.5Gi", Quantity { value: 1.5, suffix: Suffix::BinaryMultiple(BinaryMultiple::Gibi) })] + #[case("0.8Ti", Quantity { value: 0.8, suffix: Suffix::BinaryMultiple(BinaryMultiple::Tebi) })] + #[case("3.2Pi", Quantity { value: 3.2, suffix: Suffix::BinaryMultiple(BinaryMultiple::Pebi) })] + #[case("0.2Ei", Quantity { value: 0.2, suffix: Suffix::BinaryMultiple(BinaryMultiple::Exbi) })] + #[case("8Mi", Quantity { value: 8.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Mebi) })] fn binary_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { let parsed = Quantity::from_str(input).unwrap(); assert_eq!(parsed, expected); } #[rstest] - #[case("49041204k", Quantity { value: 49041204.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo)) })] - #[case("256k", Quantity { value: 256.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Kilo)) })] - #[case("1.5G", Quantity { value: 1.5, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Giga)) })] - #[case("0.8T", Quantity { value: 0.8, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Tera)) })] - #[case("3.2P", Quantity { value: 3.2, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Peta)) })] - #[case("0.2E", Quantity { value: 0.2, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Exa)) })] - #[case("4m", Quantity { value: 4.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Milli)) })] - #[case("8M", Quantity { value: 8.0, suffix: Some(Suffix::DecimalByteMultiple(DecimalByteMultiple::Mega)) })] + #[case("49041204k", Quantity { value: 49041204.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Kilo) })] + #[case("256k", Quantity { value: 256.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Kilo) })] + #[case("1.5G", Quantity { value: 1.5, suffix: Suffix::DecimalMultiple(DecimalMultiple::Giga) })] + #[case("0.8T", Quantity { value: 0.8, suffix: Suffix::DecimalMultiple(DecimalMultiple::Tera) })] + #[case("3.2P", Quantity { value: 3.2, suffix: Suffix::DecimalMultiple(DecimalMultiple::Peta) })] + #[case("0.2E", Quantity { value: 0.2, suffix: Suffix::DecimalMultiple(DecimalMultiple::Exa) })] + #[case("4m", Quantity { value: 4.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Milli) })] + #[case("8M", Quantity { value: 8.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Mega) })] fn decimal_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { let parsed = Quantity::from_str(input).unwrap(); assert_eq!(parsed, expected); } #[rstest] - #[case("1.234e-3.21", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(-3.21))) })] - #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(-3.21))) })] - #[case("1.234e3", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(3.0))) })] - #[case("1.234E3", Quantity { value: 1.234, suffix: Some(Suffix::DecimalExponent(DecimalExponent(3.0))) })] + #[case("1.234e-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(-3.21)) })] + #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(-3.21)) })] + #[case("1.234e3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(3.0)) })] + #[case("1.234E3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(3.0)) })] fn decimal_exponent_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { let parsed = Quantity::from_str(input).unwrap(); assert_eq!(parsed, expected); @@ -466,53 +440,53 @@ mod test { } #[rstest] - #[case("1Mi", BinaryByteMultiple::Kibi, "1024Ki", true)] - #[case("1024Ki", BinaryByteMultiple::Mebi, "1Mi", true)] - #[case("1Mi", BinaryByteMultiple::Mebi, "1Mi", false)] - fn binary_byte_to_binary_byte_scale_pass( + #[case("1Mi", BinaryMultiple::Kibi, "1024Ki", true)] + #[case("1024Ki", BinaryMultiple::Mebi, "1Mi", true)] + #[case("1Mi", BinaryMultiple::Mebi, "1Mi", false)] + fn binary_to_binary_scale_pass( #[case] input: &str, - #[case] scale_to: BinaryByteMultiple, + #[case] scale_to: BinaryMultiple, #[case] output: &str, - #[case] scaled: bool, + #[case] _scaled: bool, ) { - let mut parsed = Quantity::from_str(input).unwrap(); - let was_scaled = parsed.scale_to(Suffix::BinaryByteMultiple(scale_to)); + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::BinaryMultiple(scale_to)); assert_eq!(parsed.to_string(), output); - assert_eq!(was_scaled, scaled); } #[rstest] - #[case("1Mi", DecimalByteMultiple::Kilo, "1048.576k", true)] - #[case("1Mi", DecimalByteMultiple::Mega, "1.048576M", true)] - fn binary_byte_to_decimal_byte_scale_pass( + #[case("1Mi", DecimalMultiple::Kilo, "1048.576k", true)] + #[case("1Mi", DecimalMultiple::Mega, "1.048576M", true)] + fn binary_to_decimal_scale_pass( #[case] input: &str, - #[case] scale_to: DecimalByteMultiple, + #[case] scale_to: DecimalMultiple, #[case] output: &str, - #[case] scaled: bool, + #[case] _scaled: bool, ) { - let mut parsed = Quantity::from_str(input).unwrap(); - let was_scaled = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalMultiple(scale_to)); assert_eq!(parsed.to_string(), output); - assert_eq!(was_scaled, scaled); } #[rstest] - #[case("1M", DecimalByteMultiple::Kilo, "1000k", true)] - #[case("1000k", DecimalByteMultiple::Mega, "1M", true)] - #[case("1M", DecimalByteMultiple::Mega, "1M", false)] - fn decimal_byte_to_decimal_byte_scale_pass( + #[case("1M", DecimalMultiple::Kilo, "1000k", true)] + #[case("1000k", DecimalMultiple::Mega, "1M", true)] + #[case("1M", DecimalMultiple::Mega, "1M", false)] + fn decimal_to_decimal_scale_pass( #[case] input: &str, - #[case] scale_to: DecimalByteMultiple, + #[case] scale_to: DecimalMultiple, #[case] output: &str, - #[case] scaled: bool, + #[case] _scaled: bool, ) { - let mut parsed = Quantity::from_str(input).unwrap(); - let was_scaled = parsed.scale_to(Suffix::DecimalByteMultiple(scale_to)); + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalMultiple(scale_to)); assert_eq!(parsed.to_string(), output); - assert_eq!(was_scaled, scaled); } #[rstest] @@ -523,12 +497,12 @@ mod test { #[case] input: &str, #[case] scale_to: DecimalExponent, #[case] output: &str, - #[case] scaled: bool, + #[case] _scaled: bool, ) { - let mut parsed = Quantity::from_str(input).unwrap(); - let was_scaled = parsed.scale_to(Suffix::DecimalExponent(scale_to)); + let parsed = Quantity::from_str(input) + .unwrap() + .scale_to(Suffix::DecimalExponent(scale_to)); assert_eq!(parsed.to_string(), output); - assert_eq!(was_scaled, scaled); } } From d6dec5f80be5816f798c8d6d638ec948d1b8cf4c Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 8 Jan 2025 17:09:53 +0100 Subject: [PATCH 08/11] chore: Move suffix code into own file --- crates/stackable-operator/src/quantity/mod.rs | 279 ++--------------- .../stackable-operator/src/quantity/suffix.rs | 286 ++++++++++++++++++ 2 files changed, 304 insertions(+), 261 deletions(-) create mode 100644 crates/stackable-operator/src/quantity/suffix.rs diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 1677effe1..f9ff6b43b 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -1,7 +1,6 @@ use std::{ fmt::{Display, Write}, num::ParseFloatError, - ops::Deref, str::FromStr, }; @@ -12,9 +11,11 @@ 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 { @@ -123,8 +124,7 @@ impl TryFrom<&K8sQuantity> for Quantity { impl Quantity { /// Optionally scales up or down to the provided `suffix`. /// - /// This function returns a value pair which contains an optional [`Quantity`] and a bool - /// indicating if the function performed any scaling. It returns `false` in the following cases: + /// No scaling is performed in the following cases: /// /// - the suffixes already match /// - the value is 0 @@ -144,250 +144,11 @@ impl Quantity { } } -#[derive(Debug, PartialEq, Snafu)] -#[snafu(display("failed to parse {input:?} as quantity suffix"))] -pub struct ParseSuffixError { - input: String, -} - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] -pub enum Suffix { - DecimalMultiple(DecimalMultiple), - BinaryMultiple(BinaryMultiple), - DecimalExponent(DecimalExponent), -} - -impl FromStr for Suffix { - type Err = ParseSuffixError; - - fn from_str(input: &str) -> Result { - if let Ok(binary_si) = BinaryMultiple::from_str(input) { - return Ok(Self::BinaryMultiple(binary_si)); - } - - if let Ok(decimal_si) = DecimalMultiple::from_str(input) { - return Ok(Self::DecimalMultiple(decimal_si)); - } - - if input.starts_with(['e', 'E']) { - if let Ok(decimal_exponent) = f64::from_str(&input[1..]) { - return Ok(Self::DecimalExponent(DecimalExponent(decimal_exponent))); - } - } - - ParseSuffixSnafu { input }.fail() - } -} - -impl Display for Suffix { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Suffix::DecimalMultiple(decimal) => write!(f, "{decimal}"), - Suffix::BinaryMultiple(binary) => write!(f, "{binary}"), - Suffix::DecimalExponent(float) => write!(f, "e{float}"), - } - } -} - -impl Suffix { - pub fn factor(&self) -> f64 { - match self { - Suffix::DecimalMultiple(s) => s.factor(), - Suffix::BinaryMultiple(s) => s.factor(), - Suffix::DecimalExponent(s) => s.factor(), - } - } -} - -/// 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), - } - } -} - -/// 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 = "m")] - Milli, - - #[strum(serialize = "")] - Empty, - - #[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::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 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", Suffix::BinaryMultiple(BinaryMultiple::Kibi))] - #[case("Mi", Suffix::BinaryMultiple(BinaryMultiple::Mebi))] - #[case("Gi", Suffix::BinaryMultiple(BinaryMultiple::Gibi))] - #[case("Ti", Suffix::BinaryMultiple(BinaryMultiple::Tebi))] - #[case("Pi", Suffix::BinaryMultiple(BinaryMultiple::Pebi))] - #[case("Ei", Suffix::BinaryMultiple(BinaryMultiple::Exbi))] - fn binary_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { - let parsed = Suffix::from_str(input).unwrap(); - assert_eq!(parsed, expected); - } - - #[rstest] - #[case("m", Suffix::DecimalMultiple(DecimalMultiple::Milli))] - #[case("", Suffix::DecimalMultiple(DecimalMultiple::Empty))] - #[case("k", Suffix::DecimalMultiple(DecimalMultiple::Kilo))] - #[case("M", Suffix::DecimalMultiple(DecimalMultiple::Mega))] - #[case("G", Suffix::DecimalMultiple(DecimalMultiple::Giga))] - #[case("T", Suffix::DecimalMultiple(DecimalMultiple::Tera))] - #[case("P", Suffix::DecimalMultiple(DecimalMultiple::Peta))] - #[case("E", Suffix::DecimalMultiple(DecimalMultiple::Exa))] - fn decimal_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { - let parsed = Suffix::from_str(input).unwrap(); - assert_eq!(parsed, expected); - } - #[rstest] #[case("49041204Ki", Quantity { value: 49041204.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] #[case("256Ki", Quantity { value: 256.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] @@ -416,10 +177,10 @@ mod test { } #[rstest] - #[case("1.234e-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(-3.21)) })] - #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(-3.21)) })] - #[case("1.234e3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(3.0)) })] - #[case("1.234E3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent(3.0)) })] + #[case("1.234e-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(-3.21)) })] + #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(-3.21)) })] + #[case("1.234e3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(3.0)) })] + #[case("1.234E3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(3.0)) })] fn decimal_exponent_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { let parsed = Quantity::from_str(input).unwrap(); assert_eq!(parsed, expected); @@ -440,14 +201,13 @@ mod test { } #[rstest] - #[case("1Mi", BinaryMultiple::Kibi, "1024Ki", true)] - #[case("1024Ki", BinaryMultiple::Mebi, "1Mi", true)] - #[case("1Mi", BinaryMultiple::Mebi, "1Mi", false)] + #[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, - #[case] _scaled: bool, ) { let parsed = Quantity::from_str(input) .unwrap() @@ -457,13 +217,12 @@ mod test { } #[rstest] - #[case("1Mi", DecimalMultiple::Kilo, "1048.576k", true)] - #[case("1Mi", DecimalMultiple::Mega, "1.048576M", true)] + #[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, - #[case] _scaled: bool, ) { let parsed = Quantity::from_str(input) .unwrap() @@ -473,14 +232,13 @@ mod test { } #[rstest] - #[case("1M", DecimalMultiple::Kilo, "1000k", true)] - #[case("1000k", DecimalMultiple::Mega, "1M", true)] - #[case("1M", DecimalMultiple::Mega, "1M", false)] + #[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, - #[case] _scaled: bool, ) { let parsed = Quantity::from_str(input) .unwrap() @@ -490,14 +248,13 @@ mod test { } #[rstest] - #[case("1e3", DecimalExponent(0.0), "1000e0", true)] - #[case("1000e0", DecimalExponent(3.0), "1e3", true)] - #[case("1e3", DecimalExponent(3.0), "1e3", false)] + #[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, - #[case] _scaled: bool, ) { let parsed = Quantity::from_str(input) .unwrap() diff --git a/crates/stackable-operator/src/quantity/suffix.rs b/crates/stackable-operator/src/quantity/suffix.rs new file mode 100644 index 000000000..bafadd524 --- /dev/null +++ b/crates/stackable-operator/src/quantity/suffix.rs @@ -0,0 +1,286 @@ +use std::{fmt::Display, ops::Deref, str::FromStr}; + +use snafu::Snafu; + +#[derive(Debug, PartialEq, Snafu)] +#[snafu(display("failed to parse {input:?} as quantity suffix"))] +pub struct ParseSuffixError { + input: String, +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub enum Suffix { + DecimalMultiple(DecimalMultiple), + BinaryMultiple(BinaryMultiple), + DecimalExponent(DecimalExponent), +} + +impl FromStr for Suffix { + type Err = ParseSuffixError; + + fn from_str(input: &str) -> Result { + if let Ok(binary_si) = BinaryMultiple::from_str(input) { + return Ok(Self::BinaryMultiple(binary_si)); + } + + if let Ok(decimal_si) = DecimalMultiple::from_str(input) { + return Ok(Self::DecimalMultiple(decimal_si)); + } + + if input.starts_with(['e', 'E']) { + if let Ok(decimal_exponent) = f64::from_str(&input[1..]) { + return Ok(Self::DecimalExponent(DecimalExponent(decimal_exponent))); + } + } + + ParseSuffixSnafu { input }.fail() + } +} + +impl Display for Suffix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Suffix::DecimalMultiple(decimal) => write!(f, "{decimal}"), + Suffix::BinaryMultiple(binary) => write!(f, "{binary}"), + Suffix::DecimalExponent(float) => write!(f, "e{float}"), + } + } +} + +impl Suffix { + pub fn factor(&self) -> f64 { + match self { + Suffix::DecimalMultiple(s) => s.factor(), + Suffix::BinaryMultiple(s) => s.factor(), + Suffix::DecimalExponent(s) => s.factor(), + } + } + + pub fn scale_down(self) -> Option { + match self { + Suffix::DecimalMultiple(s) => todo!(), + Suffix::BinaryMultiple(s) => match s.scale_down() { + Some(s) => Some(Self::BinaryMultiple(s)), + None => Some(Self::DecimalMultiple(DecimalMultiple::Milli)), + }, + Suffix::DecimalExponent(s) => todo!(), + } + } +} + +/// 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, + + #[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", Suffix::BinaryMultiple(BinaryMultiple::Kibi))] + #[case("Mi", Suffix::BinaryMultiple(BinaryMultiple::Mebi))] + #[case("Gi", Suffix::BinaryMultiple(BinaryMultiple::Gibi))] + #[case("Ti", Suffix::BinaryMultiple(BinaryMultiple::Tebi))] + #[case("Pi", Suffix::BinaryMultiple(BinaryMultiple::Pebi))] + #[case("Ei", Suffix::BinaryMultiple(BinaryMultiple::Exbi))] + fn binary_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + let parsed = Suffix::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } + + #[rstest] + #[case("n", Suffix::DecimalMultiple(DecimalMultiple::Nano))] + #[case("u", Suffix::DecimalMultiple(DecimalMultiple::Micro))] + #[case("m", Suffix::DecimalMultiple(DecimalMultiple::Milli))] + #[case("", Suffix::DecimalMultiple(DecimalMultiple::Empty))] + #[case("k", Suffix::DecimalMultiple(DecimalMultiple::Kilo))] + #[case("M", Suffix::DecimalMultiple(DecimalMultiple::Mega))] + #[case("G", Suffix::DecimalMultiple(DecimalMultiple::Giga))] + #[case("T", Suffix::DecimalMultiple(DecimalMultiple::Tera))] + #[case("P", Suffix::DecimalMultiple(DecimalMultiple::Peta))] + #[case("E", Suffix::DecimalMultiple(DecimalMultiple::Exa))] + fn decimal_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { + let parsed = Suffix::from_str(input).unwrap(); + assert_eq!(parsed, expected); + } +} From d64bf0e95fddd1e7f534b5d6bad64b002beeb868 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 8 Jan 2025 17:12:40 +0100 Subject: [PATCH 09/11] refactor: Improve suffix handling for add and sub ops --- crates/stackable-operator/src/quantity/mod.rs | 19 +++++++++++++++++++ crates/stackable-operator/src/quantity/ops.rs | 12 ++++++------ .../stackable-operator/src/quantity/suffix.rs | 4 ++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index f9ff6b43b..d89be30df 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -142,6 +142,25 @@ impl Quantity { } } } + + /// 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)) + } + } } #[cfg(test)] diff --git a/crates/stackable-operator/src/quantity/ops.rs b/crates/stackable-operator/src/quantity/ops.rs index a6ef1f92a..38fe532e0 100644 --- a/crates/stackable-operator/src/quantity/ops.rs +++ b/crates/stackable-operator/src/quantity/ops.rs @@ -6,11 +6,11 @@ impl Add for Quantity { type Output = Quantity; fn add(self, rhs: Quantity) -> Self::Output { - let rhs = rhs.scale_to(self.suffix); + let (this, rhs) = self.set_suffix_or_scale_rhs(rhs); Self { - value: self.value + rhs.value, - ..self + value: this.value + rhs.value, + suffix: this.suffix, } } } @@ -25,11 +25,11 @@ impl Sub for Quantity { type Output = Quantity; fn sub(self, rhs: Quantity) -> Self::Output { - let rhs = rhs.scale_to(self.suffix); + let (this, rhs) = self.set_suffix_or_scale_rhs(rhs); Self { - value: self.value - rhs.value, - ..self + value: this.value - rhs.value, + suffix: this.suffix, } } } diff --git a/crates/stackable-operator/src/quantity/suffix.rs b/crates/stackable-operator/src/quantity/suffix.rs index bafadd524..ffef37b3a 100644 --- a/crates/stackable-operator/src/quantity/suffix.rs +++ b/crates/stackable-operator/src/quantity/suffix.rs @@ -58,12 +58,12 @@ impl Suffix { pub fn scale_down(self) -> Option { match self { - Suffix::DecimalMultiple(s) => todo!(), + Suffix::DecimalMultiple(_s) => todo!(), Suffix::BinaryMultiple(s) => match s.scale_down() { Some(s) => Some(Self::BinaryMultiple(s)), None => Some(Self::DecimalMultiple(DecimalMultiple::Milli)), }, - Suffix::DecimalExponent(s) => todo!(), + Suffix::DecimalExponent(_s) => todo!(), } } } From 845c022995c72bd2713067ee1f3bbd3d12970463 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 8 Jan 2025 17:13:44 +0100 Subject: [PATCH 10/11] docs: Start writing doc comments --- crates/stackable-operator/src/quantity/mod.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index d89be30df..732a41eb4 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -1,3 +1,5 @@ +//! This module contains types and functions to parse and handle Kubernetes quantities. + use std::{ fmt::{Display, Write}, num::ParseFloatError, @@ -29,6 +31,69 @@ pub enum ParseQuantityError { InvalidSuffix { source: ParseSuffixError }, } +/// 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 { // FIXME (@Techassi): Support arbitrary-precision numbers From 0760882c2af0d5a9c0108af3e68ded1a83216b6c Mon Sep 17 00:00:00 2001 From: Techassi Date: Fri, 10 Jan 2025 09:36:55 +0100 Subject: [PATCH 11/11] wip: Switch device --- crates/stackable-operator/src/quantity/cpu.rs | 66 ++++++- crates/stackable-operator/src/quantity/mod.rs | 167 +++++++++++++----- .../stackable-operator/src/quantity/suffix.rs | 117 ++++-------- 3 files changed, 210 insertions(+), 140 deletions(-) diff --git a/crates/stackable-operator/src/quantity/cpu.rs b/crates/stackable-operator/src/quantity/cpu.rs index 58a9f646d..4a4ca4a81 100644 --- a/crates/stackable-operator/src/quantity/cpu.rs +++ b/crates/stackable-operator/src/quantity/cpu.rs @@ -1,17 +1,71 @@ 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, DecimalMultiple, Quantity, Suffix}; +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); +pub struct CpuQuantity(Quantity); impl Deref for CpuQuantity { - type Target = Quantity; + type Target = Quantity; fn deref(&self) -> &Self::Target { &self.0 @@ -29,7 +83,7 @@ impl Sum for CpuQuantity { iter.fold( CpuQuantity(Quantity { value: 0.0, - suffix: Suffix::DecimalMultiple(DecimalMultiple::Empty), + suffix: CpuSuffix::DecimalMultiple(DecimalMultiple::Empty), }), CpuQuantity::add, ) @@ -41,12 +95,12 @@ forward_quantity_impls!(CpuQuantity, K8sQuantity, usize, f32, f64); impl CpuQuantity { pub fn from_millis(value: u32) -> Self { CpuQuantity(Quantity { - suffix: Suffix::DecimalMultiple(DecimalMultiple::Milli), + suffix: CpuSuffix::DecimalMultiple(DecimalMultiple::Milli), value: value.into(), }) } - pub fn scale_to(self, suffix: Suffix) -> Self { + pub fn scale_to(self, suffix: CpuSuffix) -> Self { Self(self.0.scale_to(suffix)) } } diff --git a/crates/stackable-operator/src/quantity/mod.rs b/crates/stackable-operator/src/quantity/mod.rs index 732a41eb4..a869a54c6 100644 --- a/crates/stackable-operator/src/quantity/mod.rs +++ b/crates/stackable-operator/src/quantity/mod.rs @@ -20,7 +20,10 @@ pub use memory::*; pub use suffix::*; #[derive(Debug, PartialEq, Snafu)] -pub enum ParseQuantityError { +pub enum ParseQuantityError +where + E: std::error::Error + 'static, +{ #[snafu(display("input is either empty or contains non-ascii characters"))] InvalidFormat, @@ -28,9 +31,23 @@ pub enum ParseQuantityError { InvalidFloat { source: ParseFloatError }, #[snafu(display("failed to parse suffix"))] - InvalidSuffix { source: ParseSuffixError }, + 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 @@ -95,7 +112,10 @@ pub enum ParseQuantityError { /// [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 { +pub struct Quantity +where + S: Suffix, +{ // FIXME (@Techassi): Support arbitrary-precision numbers /// The numeric value of the quantity. /// @@ -107,18 +127,21 @@ pub struct Quantity { /// The suffix of the quantity. /// /// This field holds data parsed from `` according to the spec. - suffix: Suffix, + suffix: S, } -impl FromStr for Quantity { - type Err = ParseQuantityError; +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: Suffix::DecimalMultiple(DecimalMultiple::Empty), + suffix: S::default(), value: 0.0, }); } @@ -127,7 +150,7 @@ impl FromStr for Quantity { Some(suffix_index) => { let parts = input.split_at(suffix_index); let value = f64::from_str(parts.0).context(InvalidFloatSnafu)?; - let suffix = Suffix::from_str(parts.1).context(InvalidSuffixSnafu)?; + let suffix = S::from_str(parts.1).context(InvalidSuffixSnafu)?; Ok(Self { suffix, value }) } @@ -135,7 +158,7 @@ impl FromStr for Quantity { let value = f64::from_str(input).context(InvalidFloatSnafu)?; Ok(Self { - suffix: Suffix::DecimalMultiple(DecimalMultiple::Empty), + suffix: S::default(), value, }) } @@ -143,7 +166,10 @@ impl FromStr for Quantity { } } -impl Display for Quantity { +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'); @@ -158,42 +184,57 @@ impl Display for Quantity { } } -impl From for K8sQuantity { - fn from(value: Quantity) -> Self { +impl From> for K8sQuantity +where + S: Suffix, +{ + fn from(value: Quantity) -> Self { K8sQuantity(value.to_string()) } } -impl From<&Quantity> for K8sQuantity { - fn from(value: &Quantity) -> Self { +impl From<&Quantity> for K8sQuantity +where + S: Suffix, +{ + fn from(value: &Quantity) -> Self { K8sQuantity(value.to_string()) } } -impl TryFrom for Quantity { - type Error = ParseQuantityError; +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 { - type Error = ParseQuantityError; +impl TryFrom<&K8sQuantity> for Quantity +where + S: Suffix, +{ + type Error = ParseQuantityError; fn try_from(value: &K8sQuantity) -> Result { Quantity::from_str(&value.0) } } -impl Quantity { +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: Suffix) -> Self { + pub fn scale_to(self, suffix: S) -> Self { match (self.value, &self.suffix) { (0.0, _) => self, (_, s) if *s == suffix => self, @@ -208,6 +249,28 @@ impl Quantity { } } + // 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. /// @@ -228,46 +291,54 @@ impl Quantity { } } +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("49041204Ki", Quantity { value: 49041204.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] - #[case("256Ki", Quantity { value: 256.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Kibi) })] - #[case("1.5Gi", Quantity { value: 1.5, suffix: Suffix::BinaryMultiple(BinaryMultiple::Gibi) })] - #[case("0.8Ti", Quantity { value: 0.8, suffix: Suffix::BinaryMultiple(BinaryMultiple::Tebi) })] - #[case("3.2Pi", Quantity { value: 3.2, suffix: Suffix::BinaryMultiple(BinaryMultiple::Pebi) })] - #[case("0.2Ei", Quantity { value: 0.2, suffix: Suffix::BinaryMultiple(BinaryMultiple::Exbi) })] - #[case("8Mi", Quantity { value: 8.0, suffix: Suffix::BinaryMultiple(BinaryMultiple::Mebi) })] - fn binary_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { + #[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, expected); - } - #[rstest] - #[case("49041204k", Quantity { value: 49041204.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Kilo) })] - #[case("256k", Quantity { value: 256.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Kilo) })] - #[case("1.5G", Quantity { value: 1.5, suffix: Suffix::DecimalMultiple(DecimalMultiple::Giga) })] - #[case("0.8T", Quantity { value: 0.8, suffix: Suffix::DecimalMultiple(DecimalMultiple::Tera) })] - #[case("3.2P", Quantity { value: 3.2, suffix: Suffix::DecimalMultiple(DecimalMultiple::Peta) })] - #[case("0.2E", Quantity { value: 0.2, suffix: Suffix::DecimalMultiple(DecimalMultiple::Exa) })] - #[case("4m", Quantity { value: 4.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Milli) })] - #[case("8M", Quantity { value: 8.0, suffix: Suffix::DecimalMultiple(DecimalMultiple::Mega) })] - fn decimal_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { - let parsed = Quantity::from_str(input).unwrap(); - assert_eq!(parsed, expected); + 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("1.234e-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(-3.21)) })] - #[case("1.234E-3.21", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(-3.21)) })] - #[case("1.234e3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(3.0)) })] - #[case("1.234E3", Quantity { value: 1.234, suffix: Suffix::DecimalExponent(DecimalExponent::from(3.0)) })] - fn decimal_exponent_quantity_from_str_pass(#[case] input: &str, #[case] expected: Quantity) { + #[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, expected); + assert_eq!(parsed.value, expected_value); } #[rstest] diff --git a/crates/stackable-operator/src/quantity/suffix.rs b/crates/stackable-operator/src/quantity/suffix.rs index ffef37b3a..8a4aa75e1 100644 --- a/crates/stackable-operator/src/quantity/suffix.rs +++ b/crates/stackable-operator/src/quantity/suffix.rs @@ -1,71 +1,15 @@ use std::{fmt::Display, ops::Deref, str::FromStr}; -use snafu::Snafu; - -#[derive(Debug, PartialEq, Snafu)] -#[snafu(display("failed to parse {input:?} as quantity suffix"))] -pub struct ParseSuffixError { - input: String, -} - -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] -pub enum Suffix { - DecimalMultiple(DecimalMultiple), - BinaryMultiple(BinaryMultiple), - DecimalExponent(DecimalExponent), -} - -impl FromStr for Suffix { - type Err = ParseSuffixError; - - fn from_str(input: &str) -> Result { - if let Ok(binary_si) = BinaryMultiple::from_str(input) { - return Ok(Self::BinaryMultiple(binary_si)); - } - - if let Ok(decimal_si) = DecimalMultiple::from_str(input) { - return Ok(Self::DecimalMultiple(decimal_si)); - } - - if input.starts_with(['e', 'E']) { - if let Ok(decimal_exponent) = f64::from_str(&input[1..]) { - return Ok(Self::DecimalExponent(DecimalExponent(decimal_exponent))); - } - } - - ParseSuffixSnafu { input }.fail() - } -} - -impl Display for Suffix { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Suffix::DecimalMultiple(decimal) => write!(f, "{decimal}"), - Suffix::BinaryMultiple(binary) => write!(f, "{binary}"), - Suffix::DecimalExponent(float) => write!(f, "e{float}"), - } - } -} - -impl Suffix { - pub fn factor(&self) -> f64 { - match self { - Suffix::DecimalMultiple(s) => s.factor(), - Suffix::BinaryMultiple(s) => s.factor(), - Suffix::DecimalExponent(s) => s.factor(), - } - } - - pub fn scale_down(self) -> Option { - match self { - Suffix::DecimalMultiple(_s) => todo!(), - Suffix::BinaryMultiple(s) => match s.scale_down() { - Some(s) => Some(Self::BinaryMultiple(s)), - None => Some(Self::DecimalMultiple(DecimalMultiple::Milli)), - }, - Suffix::DecimalExponent(_s) => todo!(), - } - } +pub trait Suffix: + FromStr + + Clone + + Copy + + Default + + Display + + PartialEq + + PartialOrd +{ + fn factor(&self) -> f64; } /// Supported byte-multiples based on powers of 2. @@ -181,6 +125,7 @@ pub enum DecimalMultiple { #[strum(serialize = "")] Empty, + // (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.) #[strum(serialize = "k")] Kilo, @@ -257,30 +202,30 @@ mod test { use rstest::rstest; #[rstest] - #[case("Ki", Suffix::BinaryMultiple(BinaryMultiple::Kibi))] - #[case("Mi", Suffix::BinaryMultiple(BinaryMultiple::Mebi))] - #[case("Gi", Suffix::BinaryMultiple(BinaryMultiple::Gibi))] - #[case("Ti", Suffix::BinaryMultiple(BinaryMultiple::Tebi))] - #[case("Pi", Suffix::BinaryMultiple(BinaryMultiple::Pebi))] - #[case("Ei", Suffix::BinaryMultiple(BinaryMultiple::Exbi))] - fn binary_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { - let parsed = Suffix::from_str(input).unwrap(); + #[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", Suffix::DecimalMultiple(DecimalMultiple::Nano))] - #[case("u", Suffix::DecimalMultiple(DecimalMultiple::Micro))] - #[case("m", Suffix::DecimalMultiple(DecimalMultiple::Milli))] - #[case("", Suffix::DecimalMultiple(DecimalMultiple::Empty))] - #[case("k", Suffix::DecimalMultiple(DecimalMultiple::Kilo))] - #[case("M", Suffix::DecimalMultiple(DecimalMultiple::Mega))] - #[case("G", Suffix::DecimalMultiple(DecimalMultiple::Giga))] - #[case("T", Suffix::DecimalMultiple(DecimalMultiple::Tera))] - #[case("P", Suffix::DecimalMultiple(DecimalMultiple::Peta))] - #[case("E", Suffix::DecimalMultiple(DecimalMultiple::Exa))] - fn decimal_multiple_from_str_pass(#[case] input: &str, #[case] expected: Suffix) { - let parsed = Suffix::from_str(input).unwrap(); + #[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); } }