#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
#[inline]
pub fn crush_hash32(mut a: u32) -> u32 {
a = a.wrapping_add(!(a << 15));
a ^= a >> 10;
a = a.wrapping_add(a << 3);
a ^= a >> 6;
a = a.wrapping_add(!(a << 11));
a ^= a >> 16;
a
}
#[inline]
pub fn crush_hash64(x: u64) -> u64 {
let low = crush_hash32(x as u32);
let high = crush_hash32((x >> 32) as u32);
((high as u64) << 32) | (low as u64)
}
#[inline]
pub fn crush_hash(x: u64, r: u64) -> u64 {
let mut h = x;
h = h.wrapping_mul(0x87c37b91114253d5);
h = h.wrapping_add(r);
h ^= h >> 33;
h = h.wrapping_mul(0x4cf5ad432745937f);
h ^= h >> 29;
h = h.wrapping_mul(0x94d049bb133111eb);
h ^= h >> 32;
h
}
#[inline]
pub fn crush_hash3(x: u64, b: i64, r: u64) -> u64 {
crush_hash(crush_hash(x, b as u64), r)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BucketType {
Osd,
Host,
Rack,
Room,
Datacenter,
Region,
Root,
Custom(u8),
}
impl BucketType {
pub fn type_id(&self) -> i32 {
match self {
BucketType::Osd => 0,
BucketType::Host => 1,
BucketType::Rack => 2,
BucketType::Room => 3,
BucketType::Datacenter => 4,
BucketType::Region => 5,
BucketType::Root => 6,
BucketType::Custom(id) => 100 + (*id as i32),
}
}
pub fn from_type_id(id: i32) -> Self {
match id {
0 => BucketType::Osd,
1 => BucketType::Host,
2 => BucketType::Rack,
3 => BucketType::Room,
4 => BucketType::Datacenter,
5 => BucketType::Region,
6 => BucketType::Root,
x if x >= 100 => BucketType::Custom((x - 100) as u8),
_ => BucketType::Custom(0),
}
}
}
impl fmt::Display for BucketType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BucketType::Osd => write!(f, "osd"),
BucketType::Host => write!(f, "host"),
BucketType::Rack => write!(f, "rack"),
BucketType::Room => write!(f, "room"),
BucketType::Datacenter => write!(f, "datacenter"),
BucketType::Region => write!(f, "region"),
BucketType::Root => write!(f, "root"),
BucketType::Custom(id) => write!(f, "custom-{}", id),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BucketAlgorithm {
Uniform,
List,
Tree,
Straw,
#[default]
Straw2,
}
#[derive(Debug, Clone)]
pub struct CrushBucket {
pub id: i64,
pub name: String,
pub bucket_type: BucketType,
pub algorithm: BucketAlgorithm,
pub hash_seed: u32,
pub weight: u32,
pub children: Vec<i64>,
pub child_weights: Vec<u32>,
}
impl CrushBucket {
pub fn new(id: i64, name: &str, bucket_type: BucketType) -> Self {
Self {
id,
name: name.to_string(),
bucket_type,
algorithm: BucketAlgorithm::Straw2,
hash_seed: id as u32,
weight: 0,
children: Vec::new(),
child_weights: Vec::new(),
}
}
pub fn add_child(&mut self, child_id: i64, weight: u32) {
self.children.push(child_id);
self.child_weights.push(weight);
self.weight += weight;
}
pub fn remove_child(&mut self, child_id: i64) -> bool {
if let Some(pos) = self.children.iter().position(|&id| id == child_id) {
self.weight -= self.child_weights[pos];
self.children.remove(pos);
self.child_weights.remove(pos);
true
} else {
false
}
}
pub fn is_leaf(&self) -> bool {
self.bucket_type == BucketType::Osd
}
pub fn size(&self) -> usize {
self.children.len()
}
}
#[derive(Debug, Clone)]
pub enum CrushStep {
Take(i64),
Choose {
count: usize,
bucket_type: BucketType,
},
ChooseLeaf {
count: usize,
bucket_type: BucketType,
},
ChooseFirstN {
count: usize,
bucket_type: BucketType,
},
Emit,
SetChooseMode(ChooseMode),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ChooseMode {
FirstN,
#[default]
Indep,
}
#[derive(Debug, Clone)]
pub struct CrushRule {
pub id: u32,
pub name: String,
pub min_size: usize,
pub max_size: usize,
pub steps: Vec<CrushStep>,
}
impl CrushRule {
pub fn new(id: u32, name: &str) -> Self {
Self {
id,
name: name.to_string(),
min_size: 1,
max_size: 10,
steps: Vec::new(),
}
}
pub fn add_step(&mut self, step: CrushStep) {
self.steps.push(step);
}
pub fn replicated(id: u32, name: &str, root_id: i64, failure_domain: BucketType) -> Self {
let mut rule = Self::new(id, name);
rule.steps = vec![
CrushStep::Take(root_id),
CrushStep::ChooseLeaf {
count: 0, bucket_type: failure_domain,
},
CrushStep::Emit,
];
rule
}
pub fn erasure(id: u32, name: &str, root_id: i64, failure_domain: BucketType) -> Self {
let mut rule = Self::new(id, name);
rule.steps = vec![
CrushStep::Take(root_id),
CrushStep::Choose {
count: 0,
bucket_type: failure_domain,
},
CrushStep::ChooseLeaf {
count: 1,
bucket_type: BucketType::Osd,
},
CrushStep::Emit,
];
rule
}
}
#[derive(Debug, Clone)]
pub struct CrushMap {
buckets: BTreeMap<i64, CrushBucket>,
rules: BTreeMap<u32, CrushRule>,
rules_by_name: BTreeMap<String, u32>,
weights: BTreeMap<u64, f64>,
root_id: i64,
tunables: CrushTunables,
}
#[derive(Debug, Clone)]
pub struct CrushTunables {
pub straw2: bool,
pub choose_local_tries: u32,
pub choose_local_fallback_tries: u32,
pub choose_total_tries: u32,
pub chooseleaf_descend_once: bool,
pub chooseleaf_vary_r: bool,
pub chooseleaf_stable: bool,
}
impl Default for CrushTunables {
fn default() -> Self {
Self {
straw2: true,
choose_local_tries: 2,
choose_local_fallback_tries: 5,
choose_total_tries: 50,
chooseleaf_descend_once: true,
chooseleaf_vary_r: true,
chooseleaf_stable: true,
}
}
}
#[derive(Debug, Clone)]
pub enum CrushError {
BucketNotFound(i64),
RuleNotFound(String),
InsufficientOsds {
required: usize,
available: usize,
},
InvalidRule(String),
ZeroWeight,
}
impl fmt::Display for CrushError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CrushError::BucketNotFound(id) => write!(f, "Bucket {} not found", id),
CrushError::RuleNotFound(name) => write!(f, "Rule '{}' not found", name),
CrushError::InsufficientOsds {
required,
available,
} => {
write!(f, "Need {} OSDs but only {} available", required, available)
}
CrushError::InvalidRule(msg) => write!(f, "Invalid rule: {}", msg),
CrushError::ZeroWeight => write!(f, "Cannot select from zero-weight bucket"),
}
}
}
impl CrushMap {
pub fn new() -> Self {
Self {
buckets: BTreeMap::new(),
rules: BTreeMap::new(),
rules_by_name: BTreeMap::new(),
weights: BTreeMap::new(),
root_id: -1,
tunables: CrushTunables::default(),
}
}
pub fn simple(num_osds: usize) -> Self {
let mut map = Self::new();
let root_id = -1;
let mut root = CrushBucket::new(root_id, "root", BucketType::Root);
let host_id = -2;
let mut host = CrushBucket::new(host_id, "host0", BucketType::Host);
for i in 0..num_osds {
let osd_id = i as i64;
let weight = 0x10000; host.add_child(osd_id, weight);
map.weights.insert(i as u64, 1.0);
}
root.add_child(host_id, host.weight);
map.buckets.insert(host_id, host);
map.buckets.insert(root_id, root);
map.root_id = root_id;
let rule = CrushRule::replicated(0, "replicated_rule", root_id, BucketType::Host);
map.add_rule(rule);
map
}
pub fn add_bucket(&mut self, bucket: CrushBucket) {
self.buckets.insert(bucket.id, bucket);
}
pub fn get_bucket(&self, id: i64) -> Option<&CrushBucket> {
self.buckets.get(&id)
}
pub fn get_bucket_mut(&mut self, id: i64) -> Option<&mut CrushBucket> {
self.buckets.get_mut(&id)
}
pub fn add_rule(&mut self, rule: CrushRule) {
self.rules_by_name.insert(rule.name.clone(), rule.id);
self.rules.insert(rule.id, rule);
}
pub fn get_rule(&self, name: &str) -> Option<&CrushRule> {
self.rules_by_name
.get(name)
.and_then(|id| self.rules.get(id))
}
pub fn set_weight(&mut self, osd_id: u64, weight: f64) {
self.weights.insert(osd_id, weight);
}
pub fn get_weight(&self, osd_id: u64) -> f64 {
*self.weights.get(&osd_id).unwrap_or(&1.0)
}
pub fn add_osd(&mut self, osd_id: u64, host_name: &str, weight: f64) {
let weight_fixed = (weight * 65536.0) as u32;
if !self.buckets.contains_key(&self.root_id) {
let root = CrushBucket::new(-1, "root", BucketType::Root);
self.buckets.insert(-1, root);
self.root_id = -1;
if self.rules.is_empty() {
let rule = CrushRule::replicated(0, "replicated_rule", -1, BucketType::Host);
self.add_rule(rule);
}
}
let host_id = self.find_or_create_host(host_name);
let new_host_weight = {
if let Some(host) = self.buckets.get_mut(&host_id) {
if !host.children.contains(&(osd_id as i64)) {
host.add_child(osd_id as i64, weight_fixed);
}
Some(host.weight)
} else {
None
}
};
if let Some(host_weight) = new_host_weight {
if let Some(root) = self.buckets.get_mut(&self.root_id) {
if let Some(pos) = root.children.iter().position(|&id| id == host_id) {
root.weight -= root.child_weights[pos];
root.child_weights[pos] = host_weight;
root.weight += host_weight;
}
}
}
self.weights.insert(osd_id, weight);
}
fn find_or_create_host(&mut self, host_name: &str) -> i64 {
for (&id, bucket) in &self.buckets {
if bucket.bucket_type == BucketType::Host && bucket.name == host_name {
return id;
}
}
let host_id = self
.buckets
.keys()
.filter(|&&k| k < 0)
.min()
.map(|&m| m - 1)
.unwrap_or(-2);
let host = CrushBucket::new(host_id, host_name, BucketType::Host);
self.buckets.insert(host_id, host);
if let Some(root) = self.buckets.get_mut(&self.root_id) {
root.add_child(host_id, 0); }
host_id
}
pub fn remove_osd(&mut self, osd_id: u64) {
for bucket in self.buckets.values_mut() {
if bucket.bucket_type == BucketType::Host && bucket.remove_child(osd_id as i64) {
break;
}
}
self.weights.remove(&osd_id);
}
pub fn set_root(&mut self, root_id: i64) {
self.root_id = root_id;
}
pub fn tunables(&self) -> &CrushTunables {
&self.tunables
}
pub fn set_tunables(&mut self, tunables: CrushTunables) {
self.tunables = tunables;
}
pub fn select(&self, rule_name: &str, pgid: u64, count: usize) -> Result<Vec<u64>, CrushError> {
let rule = self
.get_rule(rule_name)
.ok_or_else(|| CrushError::RuleNotFound(rule_name.to_string()))?;
let mut out = Vec::new();
let mut working = Vec::new();
for step in &rule.steps {
match step {
CrushStep::Take(bucket_id) => {
working.clear();
working.push(*bucket_id);
}
CrushStep::Choose {
count: n,
bucket_type,
}
| CrushStep::ChooseLeaf {
count: n,
bucket_type,
} => {
let num_to_select = if *n == 0 { count } else { *n };
let mut new_working = Vec::new();
for &start_bucket in &working {
let selected = self.do_choose(
start_bucket,
pgid,
num_to_select,
*bucket_type,
matches!(step, CrushStep::ChooseLeaf { .. }),
&out,
)?;
new_working.extend(selected);
}
working = new_working;
}
CrushStep::ChooseFirstN {
count: n,
bucket_type,
} => {
let num_to_select = if *n == 0 { count } else { *n };
let mut new_working = Vec::new();
for &start_bucket in &working {
let selected =
self.do_choose_firstn(start_bucket, pgid, num_to_select, *bucket_type)?;
new_working.extend(selected);
}
working = new_working;
}
CrushStep::Emit => {
for &id in &working {
if id >= 0 {
out.push(id as u64);
}
}
working.clear();
}
CrushStep::SetChooseMode(_) => {
}
}
}
let mut seen = alloc::collections::BTreeSet::new();
out.retain(|&x| seen.insert(x));
Ok(out)
}
fn do_choose(
&self,
start: i64,
pgid: u64,
count: usize,
target_type: BucketType,
leaf: bool,
already_selected: &[u64],
) -> Result<Vec<i64>, CrushError> {
let bucket = self
.buckets
.get(&start)
.ok_or(CrushError::BucketNotFound(start))?;
let mut out = Vec::with_capacity(count);
let mut collisions = 0;
let max_tries = self.tunables.choose_total_tries as usize;
for replica in 0..count {
let mut r = replica;
let mut retry = 0;
loop {
if retry >= max_tries {
break;
}
let item = self.select_from_bucket(bucket, pgid, r as u64)?;
let final_item = if item >= 0 {
item
} else if let Some(child_bucket) = self.buckets.get(&item) {
if child_bucket.bucket_type == target_type {
if leaf {
self.find_leaf_osd(item, pgid, r as u64)?
} else {
item
}
} else {
self.descend_to_type(item, pgid, target_type, leaf, r as u64)?
}
} else {
return Err(CrushError::BucketNotFound(item));
};
let osd_id = if final_item >= 0 {
final_item as u64
} else {
u64::MAX
};
let is_collision = out.contains(&final_item) || already_selected.contains(&osd_id);
if is_collision {
collisions += 1;
r += count; retry += 1;
continue;
}
if final_item >= 0 {
let weight = self.get_weight(final_item as u64);
if weight <= 0.0 {
r += count;
retry += 1;
continue;
}
}
out.push(final_item);
break;
}
}
Ok(out)
}
fn descend_to_type(
&self,
start: i64,
pgid: u64,
target_type: BucketType,
leaf: bool,
replica: u64,
) -> Result<i64, CrushError> {
let mut current = start;
let mut depth = 0;
const MAX_DEPTH: usize = 20;
while depth < MAX_DEPTH {
let bucket = self
.buckets
.get(¤t)
.ok_or(CrushError::BucketNotFound(current))?;
if bucket.bucket_type == target_type {
if leaf {
return self.find_leaf_osd(current, pgid, replica);
}
return Ok(current);
}
if bucket.bucket_type == BucketType::Osd || bucket.children.is_empty() {
return Ok(current);
}
let child = self.select_from_bucket(bucket, pgid, replica)?;
current = child;
depth += 1;
}
Ok(current)
}
fn find_leaf_osd(&self, bucket_id: i64, pgid: u64, replica: u64) -> Result<i64, CrushError> {
let mut current = bucket_id;
let mut depth = 0;
const MAX_DEPTH: usize = 20;
while depth < MAX_DEPTH {
if current >= 0 {
return Ok(current);
}
let bucket = self
.buckets
.get(¤t)
.ok_or(CrushError::BucketNotFound(current))?;
if bucket.children.is_empty() {
return Err(CrushError::InsufficientOsds {
required: 1,
available: 0,
});
}
current = self.select_from_bucket(bucket, pgid, replica + depth as u64)?;
depth += 1;
}
Ok(current)
}
fn do_choose_firstn(
&self,
start: i64,
pgid: u64,
count: usize,
target_type: BucketType,
) -> Result<Vec<i64>, CrushError> {
let bucket = self
.buckets
.get(&start)
.ok_or(CrushError::BucketNotFound(start))?;
let mut out = Vec::with_capacity(count);
for (i, &child) in bucket.children.iter().enumerate() {
if out.len() >= count {
break;
}
if child >= 0 {
if target_type == BucketType::Osd {
out.push(child);
}
} else if let Some(child_bucket) = self.buckets.get(&child) {
if child_bucket.bucket_type == target_type {
out.push(child);
} else {
let descended =
self.descend_to_type(child, pgid, target_type, false, i as u64)?;
out.push(descended);
}
}
}
Ok(out)
}
fn select_from_bucket(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
if bucket.children.is_empty() {
return Err(CrushError::InsufficientOsds {
required: 1,
available: 0,
});
}
match bucket.algorithm {
BucketAlgorithm::Uniform => self.select_uniform(bucket, x, r),
BucketAlgorithm::List => self.select_list(bucket, x, r),
BucketAlgorithm::Tree => self.select_tree(bucket, x, r),
BucketAlgorithm::Straw => self.select_straw(bucket, x, r),
BucketAlgorithm::Straw2 => self.select_straw2(bucket, x, r),
}
}
fn select_uniform(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
let hash = crush_hash3(x, bucket.id, r);
let index = (hash as usize) % bucket.children.len();
Ok(bucket.children[index])
}
fn select_list(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
let mut remaining = bucket.weight;
for (i, (&child, &weight)) in bucket
.children
.iter()
.zip(bucket.child_weights.iter())
.enumerate()
{
let hash = crush_hash3(x, bucket.id, r.wrapping_add(i as u64));
let w = hash % (remaining as u64 + 1);
if w < weight as u64 {
return Ok(child);
}
remaining -= weight;
}
Ok(*bucket.children.last().unwrap())
}
fn select_tree(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
self.select_straw2(bucket, x, r)
}
fn select_straw(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
let mut high_draw = 0u64;
let mut high_item = bucket.children[0];
for (&child, &weight) in bucket.children.iter().zip(bucket.child_weights.iter()) {
let hash = crush_hash3(x, child, r);
let draw = hash.wrapping_mul(weight as u64);
if draw > high_draw {
high_draw = draw;
high_item = child;
}
}
Ok(high_item)
}
fn select_straw2(&self, bucket: &CrushBucket, x: u64, r: u64) -> Result<i64, CrushError> {
let mut high_draw = i64::MIN;
let mut high_item = bucket.children[0];
for (&child, &weight) in bucket.children.iter().zip(bucket.child_weights.iter()) {
if weight == 0 {
continue;
}
let hash = crush_hash3(x, child, r);
let u = (hash as f64) / (u64::MAX as f64);
let ln_u = if u > 0.0 { libm::log(u) } else { -1e10 };
let draw = (ln_u / (weight as f64) * 1e9) as i64;
if draw > high_draw {
high_draw = draw;
high_item = child;
}
}
Ok(high_item)
}
pub fn get_all_osds(&self) -> Vec<u64> {
let mut osds = Vec::new();
for (&id, bucket) in &self.buckets {
if bucket.bucket_type == BucketType::Osd && id >= 0 {
osds.push(id as u64);
}
}
for bucket in self.buckets.values() {
for &child in &bucket.children {
if child >= 0 && !osds.contains(&(child as u64)) {
osds.push(child as u64);
}
}
}
osds.sort_unstable();
osds.dedup();
osds
}
pub fn total_weight(&self) -> f64 {
self.weights.values().sum()
}
pub fn count_osds_in_bucket(&self, bucket_id: i64) -> usize {
if bucket_id >= 0 {
return 1; }
let bucket = match self.buckets.get(&bucket_id) {
Some(b) => b,
None => return 0,
};
let mut count = 0;
for &child in &bucket.children {
count += self.count_osds_in_bucket(child);
}
count
}
pub fn get_failure_domain(&self, osd_id: u64, domain_type: BucketType) -> Option<i64> {
for (bucket_id, bucket) in &self.buckets {
if bucket.bucket_type == domain_type && self.bucket_contains_osd(*bucket_id, osd_id) {
return Some(*bucket_id);
}
}
None
}
fn bucket_contains_osd(&self, bucket_id: i64, osd_id: u64) -> bool {
if bucket_id >= 0 {
return bucket_id as u64 == osd_id;
}
let bucket = match self.buckets.get(&bucket_id) {
Some(b) => b,
None => return false,
};
for &child in &bucket.children {
if self.bucket_contains_osd(child, osd_id) {
return true;
}
}
false
}
}
impl Default for CrushMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crush_hash_deterministic() {
let h1 = crush_hash(12345, 0);
let h2 = crush_hash(12345, 0);
assert_eq!(h1, h2);
let h3 = crush_hash(12345, 1);
assert_ne!(h1, h3);
}
#[test]
fn test_crush_hash_distribution() {
let mut buckets = [0u32; 10];
for i in 0..10000u64 {
let h = crush_hash(i, 0);
let bucket = (h % 10) as usize;
buckets[bucket] += 1;
}
for &count in &buckets {
assert!(
count > 800 && count < 1200,
"Bucket count {} is not well distributed",
count
);
}
}
#[test]
fn test_simple_crush_map() {
let map = CrushMap::simple(4);
assert_eq!(map.get_all_osds().len(), 4);
assert!(map.get_rule("replicated_rule").is_some());
}
#[test]
fn test_crush_select_deterministic() {
let map = CrushMap::simple(8);
let osds1 = map.select("replicated_rule", 100, 3).unwrap();
let osds2 = map.select("replicated_rule", 100, 3).unwrap();
assert_eq!(osds1, osds2);
assert_eq!(osds1.len(), 3);
}
#[test]
fn test_crush_select_unique() {
let map = CrushMap::simple(8);
let osds = map.select("replicated_rule", 100, 3).unwrap();
let mut seen = alloc::collections::BTreeSet::new();
for osd in &osds {
assert!(seen.insert(*osd), "Duplicate OSD {} in selection", osd);
}
}
#[test]
fn test_crush_select_distribution() {
let map = CrushMap::simple(8);
let mut osd_counts = [0u32; 8];
for pgid in 0..1000u64 {
let osds = map.select("replicated_rule", pgid, 3).unwrap();
for osd in osds {
osd_counts[osd as usize] += 1;
}
}
let expected = 375;
for &count in &osd_counts {
let deviation = (count as i32 - expected).unsigned_abs();
assert!(
deviation < 100,
"OSD count {} deviates too much from {}",
count,
expected
);
}
}
#[test]
fn test_crush_select_different_pgs() {
let map = CrushMap::simple(8);
let osds1 = map.select("replicated_rule", 1, 3).unwrap();
let osds2 = map.select("replicated_rule", 2, 3).unwrap();
let same = osds1 == osds2;
assert!(osds1.len() == 3);
assert!(osds2.len() == 3);
let _ = same; }
#[test]
fn test_bucket_type_ordering() {
assert!(BucketType::Root.type_id() > BucketType::Datacenter.type_id());
assert!(BucketType::Datacenter.type_id() > BucketType::Rack.type_id());
assert!(BucketType::Rack.type_id() > BucketType::Host.type_id());
assert!(BucketType::Host.type_id() > BucketType::Osd.type_id());
}
#[test]
fn test_bucket_operations() {
let mut bucket = CrushBucket::new(-1, "test", BucketType::Host);
bucket.add_child(0, 100);
bucket.add_child(1, 100);
bucket.add_child(2, 100);
assert_eq!(bucket.size(), 3);
assert_eq!(bucket.weight, 300);
bucket.remove_child(1);
assert_eq!(bucket.size(), 2);
assert_eq!(bucket.weight, 200);
}
#[test]
fn test_crush_rule_creation() {
let rule = CrushRule::replicated(0, "test", -1, BucketType::Host);
assert_eq!(rule.steps.len(), 3);
assert!(matches!(rule.steps[0], CrushStep::Take(-1)));
assert!(matches!(rule.steps[2], CrushStep::Emit));
}
#[test]
fn test_insufficient_osds() {
let map = CrushMap::simple(2);
let result = map.select("replicated_rule", 1, 5);
let osds = result.unwrap();
assert!(osds.len() <= 2);
}
#[test]
fn test_weighted_selection() {
let mut map = CrushMap::new();
let root_id = -1;
let host_id = -2;
let mut host = CrushBucket::new(host_id, "host0", BucketType::Host);
host.add_child(0, 0x10000); host.add_child(1, 0x10000 * 3);
let mut root = CrushBucket::new(root_id, "root", BucketType::Root);
root.add_child(host_id, host.weight);
map.add_bucket(host);
map.add_bucket(root);
map.set_root(root_id);
map.set_weight(0, 1.0);
map.set_weight(1, 3.0);
let rule = CrushRule::replicated(0, "replicated_rule", root_id, BucketType::Host);
map.add_rule(rule);
let mut counts = [0u32; 2];
for pgid in 0..1000u64 {
let osds = map.select("replicated_rule", pgid, 1).unwrap();
if !osds.is_empty() {
counts[osds[0] as usize] += 1;
}
}
assert!(
counts[1] > counts[0],
"Weighted OSD should be selected more often"
);
}
#[test]
fn test_multi_host_crush_map() {
let mut map = CrushMap::new();
let root_id = -1;
let host1_id = -2;
let host2_id = -3;
let mut host1 = CrushBucket::new(host1_id, "host1", BucketType::Host);
host1.add_child(0, 0x10000);
host1.add_child(1, 0x10000);
let mut host2 = CrushBucket::new(host2_id, "host2", BucketType::Host);
host2.add_child(2, 0x10000);
host2.add_child(3, 0x10000);
let mut root = CrushBucket::new(root_id, "root", BucketType::Root);
root.add_child(host1_id, host1.weight);
root.add_child(host2_id, host2.weight);
map.add_bucket(host1);
map.add_bucket(host2);
map.add_bucket(root);
map.set_root(root_id);
for i in 0..4 {
map.set_weight(i, 1.0);
}
let rule = CrushRule::replicated(0, "replicated_rule", root_id, BucketType::Host);
map.add_rule(rule);
let osds = map.select("replicated_rule", 42, 2).unwrap();
assert_eq!(osds.len(), 2);
}
#[test]
fn test_get_failure_domain() {
let mut map = CrushMap::new();
let root_id = -1;
let host1_id = -2;
let host2_id = -3;
let mut host1 = CrushBucket::new(host1_id, "host1", BucketType::Host);
host1.add_child(0, 0x10000);
host1.add_child(1, 0x10000);
let mut host2 = CrushBucket::new(host2_id, "host2", BucketType::Host);
host2.add_child(2, 0x10000);
let mut root = CrushBucket::new(root_id, "root", BucketType::Root);
root.add_child(host1_id, host1.weight);
root.add_child(host2_id, host2.weight);
map.add_bucket(host1);
map.add_bucket(host2);
map.add_bucket(root);
assert_eq!(map.get_failure_domain(0, BucketType::Host), Some(host1_id));
assert_eq!(map.get_failure_domain(1, BucketType::Host), Some(host1_id));
assert_eq!(map.get_failure_domain(2, BucketType::Host), Some(host2_id));
}
#[test]
fn test_count_osds() {
let map = CrushMap::simple(8);
let count = map.count_osds_in_bucket(map.root_id);
assert_eq!(count, 8);
}
#[test]
fn test_straw2_stability() {
let map1 = CrushMap::simple(7);
let map2 = CrushMap::simple(8);
let mut same_count = 0;
let total_pgs = 100;
for pgid in 0..total_pgs {
let osds1 = map1.select("replicated_rule", pgid, 1).unwrap();
let osds2 = map2.select("replicated_rule", pgid, 1).unwrap();
if !osds1.is_empty() && !osds2.is_empty() && osds1[0] == osds2[0] {
same_count += 1;
}
}
let stability = (same_count as f64) / (total_pgs as f64);
assert!(stability > 0.7, "Straw2 stability {} is too low", stability);
}
}