Skip to content

Commit 2a0fc6c

Browse files
committed
refactor!: Make Role::{fixed,estimated}_replica_count standalone
1 parent 013bbf4 commit 2a0fc6c

2 files changed

Lines changed: 78 additions & 101 deletions

File tree

crates/stackable-operator/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- BREAKING: Make `Role::fixed_replica_count` and `Role::estimated_replica_count` functions standalone,
10+
so consumers don't need access to the `Role` struct ([#XXXX]).
11+
12+
[#XXXX]: https://github.com/stackabletech/operator-rs/pull/XXXX
13+
714
## [0.113.3] - 2026-07-07
815

916
### Fixed

crates/stackable-operator/src/role_utils.rs

Lines changed: 71 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -403,62 +403,6 @@ where
403403
}
404404
}
405405

406-
impl<Config, ConfigOverrides, RoleConfig, CommonConfig>
407-
Role<Config, ConfigOverrides, RoleConfig, CommonConfig>
408-
where
409-
RoleConfig: Default + JsonSchema + Serialize,
410-
CommonConfig: Default + JsonSchema + Serialize,
411-
ConfigOverrides: Default + JsonSchema + Serialize,
412-
{
413-
/// Returns [`Some<u32>`] in case the number of replicas is hard-coded to a certain value.
414-
///
415-
/// This is the case when all `replicas` are set to [`Some<u16>`], in which case they are simply
416-
/// summed.
417-
///
418-
/// The argument `zero_replicas_counting` is a safety mechanism, which allows the caller to
419-
/// decide if an explicit replica count of `0` should be treated as [`None`]. It also means that
420-
/// [`None`] is returned in case no roleGroups are configured at all.
421-
pub fn fixed_replica_count(&self, zero_replicas_counting: ZeroReplicasCounting) -> Option<u32> {
422-
// An empty role has no fixed replica count when zeros are treated as None.
423-
if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone
424-
&& self.role_groups.is_empty()
425-
{
426-
return None;
427-
}
428-
429-
self.role_groups
430-
.values()
431-
.map(|rg| match rg.replicas {
432-
None => None,
433-
Some(0) if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone => None,
434-
// The individual replicas are [`u16`]s, so a [`u32`] sum has plenty of space.
435-
Some(replicas) => Some(u32::from(replicas)),
436-
})
437-
.sum()
438-
}
439-
440-
/// Returns the estimated total number of replicas across all role groups.
441-
///
442-
/// Unlike [`Self::fixed_replica_count`], this always returns a value: a role group with an unset
443-
/// (i.e. [`None`]) replica count is assumed to run a single replica. Use this when a best-effort
444-
/// estimate is needed even though the exact number of replicas is not hard-coded.
445-
pub fn estimated_replica_count(&self) -> u32 {
446-
self.role_groups
447-
.values()
448-
.map(|rg| u32::from(rg.replicas.unwrap_or(1)))
449-
.sum()
450-
}
451-
}
452-
453-
/// How explicit zero (`0`) replicas on a role group should be counted
454-
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
455-
pub enum ZeroReplicasCounting {
456-
/// Treat them as what they are: `Some(0)`.
457-
TreatAsZero,
458-
/// Treat them as if the user configured [`None`].
459-
TreatAsNone,
460-
}
461-
462406
impl<Config, ConfigOverrides, RoleConfig>
463407
Role<Config, ConfigOverrides, RoleConfig, JavaCommonConfig>
464408
where
@@ -594,6 +538,56 @@ impl<K: Resource> Display for RoleGroupRef<K> {
594538
}
595539
}
596540

541+
/// Returns [`Some<u32>`] in case the number of replicas is hard-coded to a certain value.
542+
///
543+
/// This is the case when all `replicas` are set to [`Some<u16>`], in which case they are simply
544+
/// summed.
545+
///
546+
/// The argument `zero_replicas_counting` is a safety mechanism, which allows the caller to
547+
/// decide if an explicit replica count of `0` should be treated as [`None`]. It also means that
548+
/// [`None`] is returned in case no roleGroups are configured at all.
549+
pub fn fixed_replica_count<I: IntoIterator<Item = Option<u16>>>(
550+
replicas: I,
551+
zero_replicas_counting: ZeroReplicasCounting,
552+
) -> Option<u32> {
553+
let mut replicas = replicas.into_iter().peekable();
554+
555+
// An empty role has no fixed replica count when zeros are treated as None.
556+
if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone && replicas.peek().is_none() {
557+
return None;
558+
}
559+
560+
replicas
561+
.map(|replicas| match replicas {
562+
None => None,
563+
Some(0) if zero_replicas_counting == ZeroReplicasCounting::TreatAsNone => None,
564+
// The individual replicas are [`u16`]s, so a [`u32`] sum has plenty of space.
565+
Some(replicas) => Some(u32::from(replicas)),
566+
})
567+
.sum()
568+
}
569+
570+
/// Returns the estimated total number of replicas across all role groups.
571+
///
572+
/// Unlike [`Self::fixed_replica_count`], this always returns a value: a role group with an unset
573+
/// (i.e. [`None`]) replica count is assumed to run a single replica. Use this when a best-effort
574+
/// estimate is needed even though the exact number of replicas is not hard-coded.
575+
pub fn estimated_replica_count<I: IntoIterator<Item = Option<u16>>>(replicas: I) -> u32 {
576+
replicas
577+
.into_iter()
578+
.map(|replicas| u32::from(replicas.unwrap_or(1)))
579+
.sum()
580+
}
581+
582+
/// How explicit zero (`0`) replicas on a role group should be counted
583+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
584+
pub enum ZeroReplicasCounting {
585+
/// Treat them as what they are: `Some(0)`.
586+
TreatAsZero,
587+
/// Treat them as if the user configured [`None`].
588+
TreatAsNone,
589+
}
590+
597591
#[cfg(test)]
598592
mod tests {
599593
use std::collections::HashSet;
@@ -713,101 +707,77 @@ mod tests {
713707

714708
#[test]
715709
fn replica_counts_with_all_replicas_set() {
716-
let role = construct_role_with_replicas([Some(3), Some(2), Some(5)]);
710+
let replicas = [Some(3), Some(2), Some(5)];
717711

718712
assert_eq!(
719-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
713+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsZero),
720714
Some(10)
721715
);
722716
assert_eq!(
723-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
717+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsNone),
724718
Some(10)
725719
);
726-
assert_eq!(role.estimated_replica_count(), 10);
720+
assert_eq!(estimated_replica_count(replicas), 10);
727721
}
728722

729723
#[test]
730724
fn replica_counts_with_one_replica_unset() {
731-
let role = construct_role_with_replicas([Some(3), None, Some(2)]);
725+
let replicas = [Some(3), None, Some(2)];
732726

733727
assert_eq!(
734-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
728+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsZero),
735729
None
736730
);
737731
assert_eq!(
738-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
732+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsNone),
739733
None
740734
);
741-
assert_eq!(role.estimated_replica_count(), 6);
735+
assert_eq!(estimated_replica_count(replicas), 6);
742736
}
743737

744738
#[test]
745739
fn replica_counts_with_a_zero_replica() {
746-
let role = construct_role_with_replicas([Some(3), Some(0)]);
740+
let replicas = [Some(3), Some(0)];
747741

748742
assert_eq!(
749-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
743+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsZero),
750744
Some(3)
751745
);
752746
// With treat_zero_as_none the zero turns the whole count into None.
753747
assert_eq!(
754-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
748+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsNone),
755749
None
756750
);
757-
assert_eq!(role.estimated_replica_count(), 3);
751+
assert_eq!(estimated_replica_count(replicas), 3);
758752
}
759753

760754
#[test]
761-
fn replica_counts_with_a_single_zero_role_group() {
762-
let role = construct_role_with_replicas([Some(0)]);
755+
fn replica_counts_with_a_single_zero_role_groups_group() {
756+
let replicas = [Some(0)];
763757

764758
assert_eq!(
765-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
759+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsZero),
766760
Some(0)
767761
);
768762
assert_eq!(
769-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
763+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsNone),
770764
None
771765
);
772-
assert_eq!(role.estimated_replica_count(), 0);
766+
assert_eq!(estimated_replica_count(replicas), 0);
773767
}
774768

775769
#[test]
776770
fn replica_counts_without_role_groups() {
777-
let role = construct_role_with_replicas(vec![]);
771+
let replicas = [];
778772

779773
assert_eq!(
780-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsZero),
774+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsZero),
781775
Some(0)
782776
);
783777
assert_eq!(
784-
role.fixed_replica_count(ZeroReplicasCounting::TreatAsNone),
778+
fixed_replica_count(replicas, ZeroReplicasCounting::TreatAsNone),
785779
None
786780
);
787-
assert_eq!(role.estimated_replica_count(), 0);
788-
}
789-
790-
/// Builds a [`Role`] with one role group per passed `replicas` entry, so tests only need to
791-
/// care about the replica counts that [`Role::fixed_replica_count`] operates on.
792-
fn construct_role_with_replicas(
793-
replicas: impl IntoIterator<Item = Option<u16>>,
794-
) -> Role<(), EmptyConfigOverrides, GenericRoleConfig, GenericCommonConfig> {
795-
Role {
796-
config: CommonConfiguration::default(),
797-
role_config: GenericRoleConfig::default(),
798-
role_groups: replicas
799-
.into_iter()
800-
.enumerate()
801-
.map(|(index, replicas)| {
802-
(
803-
format!("role-group-{index}"),
804-
RoleGroup {
805-
config: CommonConfiguration::default(),
806-
replicas,
807-
},
808-
)
809-
})
810-
.collect(),
811-
}
781+
assert_eq!(estimated_replica_count(replicas), 0);
812782
}
813783
}

0 commit comments

Comments
 (0)