// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::diagnostics::get::{ GET_SHARD_READ_COST_LOCAL, GET_SHARD_READ_COST_REMOTE, GET_SHARD_READ_COST_SAME_NODE, GET_SHARD_READ_COST_UNKNOWN, }; use crate::disk::error::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ShardReadCost { Local, SameNode, Remote, Unknown, } impl ShardReadCost { pub(crate) const fn as_str(self) -> &'static str { match self { Self::Local => GET_SHARD_READ_COST_LOCAL, Self::SameNode => GET_SHARD_READ_COST_SAME_NODE, Self::Remote => GET_SHARD_READ_COST_REMOTE, Self::Unknown => GET_SHARD_READ_COST_UNKNOWN, } } pub(crate) const fn is_low_cost(self) -> bool { matches!(self, Self::Local | Self::SameNode) } pub(crate) const fn is_remote(self) -> bool { matches!(self, Self::Remote) } } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ShardSlot { index: usize, read_cost: ShardReadCost, data: Option>, error: Option, } impl ShardSlot { pub(crate) fn new(index: usize, data: Option>, error: Option) -> Self { Self::with_read_cost(index, ShardReadCost::Unknown, data, error) } pub(crate) fn with_read_cost(index: usize, read_cost: ShardReadCost, data: Option>, error: Option) -> Self { Self { index, read_cost, data, error, } } pub(crate) fn data(index: usize, data: Vec) -> Self { Self::new(index, Some(data), None) } pub(crate) fn data_with_read_cost(index: usize, read_cost: ShardReadCost, data: Vec) -> Self { Self::with_read_cost(index, read_cost, Some(data), None) } pub(crate) fn missing(index: usize, error: Error) -> Self { Self::new(index, None, Some(error)) } pub(crate) fn missing_with_read_cost(index: usize, read_cost: ShardReadCost, error: Error) -> Self { Self::with_read_cost(index, read_cost, None, Some(error)) } pub(crate) fn index(&self) -> usize { self.index } pub(crate) fn read_cost(&self) -> ShardReadCost { self.read_cost } pub(crate) fn has_data(&self) -> bool { self.data.is_some() } pub(crate) fn data_bytes(&self) -> Option<&[u8]> { self.data.as_deref() } pub(crate) fn error(&self) -> Option<&Error> { self.error.as_ref() } } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct StripeReadState { slots: Vec, read_quorum: usize, } impl StripeReadState { pub(crate) fn new(slots: Vec, read_quorum: usize) -> Self { Self { slots, read_quorum } } pub(crate) fn from_parts(shards: Vec>>, errors: Vec>, read_quorum: usize) -> Self { Self::from_parts_with_read_costs(shards, errors, &[], read_quorum) } pub(crate) fn from_parts_with_read_costs( shards: Vec>>, errors: Vec>, read_costs: &[ShardReadCost], read_quorum: usize, ) -> Self { let slot_count = shards.len().max(errors.len()); let mut slots = Vec::with_capacity(slot_count); let mut shards = shards.into_iter(); let mut errors = errors.into_iter(); for index in 0..slot_count { let read_cost = read_costs.get(index).copied().unwrap_or(ShardReadCost::Unknown); slots.push(ShardSlot::with_read_cost( index, read_cost, shards.next().flatten(), errors.next().flatten(), )); } Self::new(slots, read_quorum) } pub(crate) fn available_shards(&self) -> usize { self.slots.iter().filter(|slot| slot.has_data()).count() } pub(crate) fn can_decode(&self) -> bool { self.available_shards() >= self.read_quorum } pub(crate) fn slots(&self) -> &[ShardSlot] { &self.slots } pub(crate) fn slot_by_index(&self, index: usize) -> Option<&ShardSlot> { if let Some(slot) = self.slots.get(index) && slot.index == index { return Some(slot); } self.slots.iter().find(|slot| slot.index == index) } pub(crate) fn data_shards_complete(&self, data_shards: usize) -> bool { (0..data_shards).all(|index| self.slot_by_index(index).is_some_and(ShardSlot::has_data)) } pub(crate) fn into_parts(self) -> (Vec>>, Vec>) { let part_count = self.slots.iter().map(|slot| slot.index).max().map_or(0, |index| index + 1); let mut shards = Vec::with_capacity(part_count); shards.resize_with(part_count, || None); let mut errors = Vec::with_capacity(part_count); errors.resize_with(part_count, || None); for slot in self.slots { shards[slot.index] = slot.data; errors[slot.index] = slot.error; } (shards, errors) } } #[async_trait::async_trait] pub(crate) trait ShardStripeSource: Send { async fn read_next_stripe(&mut self) -> StripeReadState; } #[cfg(test)] mod tests { use super::*; #[test] fn stripe_read_state_tracks_decode_quorum() { let state = StripeReadState::new( vec![ ShardSlot::data_with_read_cost(0, ShardReadCost::Local, vec![1]), ShardSlot::missing_with_read_cost(1, ShardReadCost::Remote, Error::FileNotFound), ShardSlot::data_with_read_cost(2, ShardReadCost::SameNode, vec![2]), ], 2, ); assert_eq!(state.available_shards(), 2); assert!(state.can_decode()); assert_eq!(state.slots()[1].index(), 1); assert_eq!(state.slots()[0].read_cost(), ShardReadCost::Local); assert!(state.slots()[2].read_cost().is_low_cost()); } #[test] fn stripe_read_state_preserves_shards_and_errors() { let state = StripeReadState::new(vec![ShardSlot::missing(1, Error::FileCorrupt), ShardSlot::data(0, vec![1, 2, 3])], 2); assert!(!state.can_decode()); let (shards, errors) = state.into_parts(); assert_eq!(shards, vec![Some(vec![1, 2, 3]), None]); assert_eq!(errors, vec![None, Some(Error::FileCorrupt)]); } #[test] fn stripe_read_state_builds_slots_from_parallel_reader_parts() { let state = StripeReadState::from_parts(vec![Some(vec![1]), None, Some(vec![3])], vec![None, Some(Error::FileNotFound)], 2); assert!(state.can_decode()); assert_eq!(state.slots()[1].index(), 1); assert_eq!(state.slots()[1].error(), Some(&Error::FileNotFound)); } #[test] fn stripe_read_state_preserves_read_cost_hints() { let state = StripeReadState::from_parts_with_read_costs( vec![Some(vec![1]), None, Some(vec![3])], vec![None, Some(Error::FileNotFound)], &[ShardReadCost::Local, ShardReadCost::Remote, ShardReadCost::Unknown], 2, ); assert_eq!(state.slots()[0].read_cost(), ShardReadCost::Local); assert_eq!(state.slots()[1].read_cost(), ShardReadCost::Remote); assert_eq!(state.slots()[2].read_cost(), ShardReadCost::Unknown); assert_eq!(ShardReadCost::SameNode.as_str(), GET_SHARD_READ_COST_SAME_NODE); } #[test] fn shard_read_cost_reports_all_labels_and_remote_classification() { assert_eq!(ShardReadCost::Local.as_str(), GET_SHARD_READ_COST_LOCAL); assert_eq!(ShardReadCost::SameNode.as_str(), GET_SHARD_READ_COST_SAME_NODE); assert_eq!(ShardReadCost::Remote.as_str(), GET_SHARD_READ_COST_REMOTE); assert_eq!(ShardReadCost::Unknown.as_str(), GET_SHARD_READ_COST_UNKNOWN); assert!(ShardReadCost::Remote.is_remote()); assert!(!ShardReadCost::Local.is_remote()); assert!(!ShardReadCost::Unknown.is_low_cost()); } #[test] fn stripe_read_state_reports_complete_data_shards_without_parity() { let state = StripeReadState::from_parts(vec![Some(vec![1]), Some(vec![2]), None], Vec::new(), 2); assert!(state.data_shards_complete(2)); assert_eq!(state.slots()[0].data_bytes(), Some(&[1][..])); assert_eq!(state.slot_by_index(1).and_then(ShardSlot::data_bytes), Some(&[2][..])); } #[test] fn stripe_read_state_rejects_missing_data_shard_for_complete_fast_path() { let state = StripeReadState::from_parts(vec![Some(vec![1]), None, Some(vec![3])], Vec::new(), 2); assert!(!state.data_shards_complete(2)); } #[test] fn stripe_read_state_finds_out_of_order_slots_by_index() { let state = StripeReadState::new(vec![ShardSlot::data(2, vec![3]), ShardSlot::data(0, vec![1])], 2); assert_eq!(state.slot_by_index(0).and_then(ShardSlot::data_bytes), Some(&[1][..])); assert!(state.slot_by_index(1).is_none()); } }