use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
iter::StepBy,
ops::{Bound, RangeBounds, RangeInclusive},
};
#[derive(Debug)]
pub struct ShardSchemeRangeError {
kind: ShardSchemeRangeErrorType,
}
impl ShardSchemeRangeError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ShardSchemeRangeErrorType {
&self.kind
}
#[allow(clippy::unused_self)]
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
None
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
ShardSchemeRangeErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, None)
}
}
impl Display for ShardSchemeRangeError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
ShardSchemeRangeErrorType::BucketTooLarge {
bucket_id,
concurrency,
..
} => {
f.write_str("bucket ID ")?;
Display::fmt(bucket_id, f)?;
f.write_str(" is larger than maximum concurrency (")?;
Display::fmt(concurrency, f)?;
f.write_str(")")
}
ShardSchemeRangeErrorType::IdTooLarge { end, start, total } => {
f.write_str("The shard ID range ")?;
Display::fmt(start, f)?;
f.write_str("-")?;
Display::fmt(end, f)?;
f.write_str("/")?;
Display::fmt(total, f)?;
f.write_str(" is larger than the total")
}
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ShardSchemeRangeErrorType {
BucketTooLarge {
bucket_id: u64,
concurrency: u64,
total: u64,
},
IdTooLarge {
end: u64,
start: u64,
total: u64,
},
}
impl Error for ShardSchemeRangeError {}
#[derive(Clone, Debug)]
pub struct ShardSchemeIter {
inner: StepBy<RangeInclusive<u64>>,
}
impl ShardSchemeIter {
fn new(scheme: &ShardScheme) -> Option<Self> {
let (from, to, step) = match scheme {
ShardScheme::Auto => return None,
ShardScheme::Bucket {
bucket_id,
concurrency,
total,
} => {
let concurrency = usize::try_from(*concurrency)
.expect("concurrency is larger than target pointer width");
(*bucket_id, *total - 1, concurrency)
}
ShardScheme::Range { from, to, .. } => (*from, *to, 1),
};
Some(Self {
inner: (from..=to).step_by(step),
})
}
}
impl Iterator for ShardSchemeIter {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ShardScheme {
Auto,
Bucket {
bucket_id: u64,
concurrency: u64,
total: u64,
},
Range {
from: u64,
to: u64,
total: u64,
},
}
impl ShardScheme {
#[allow(clippy::iter_not_returning_iterator)]
pub fn iter(&self) -> Option<ShardSchemeIter> {
ShardSchemeIter::new(self)
}
pub const fn from(&self) -> Option<u64> {
match self {
Self::Auto => None,
Self::Bucket { bucket_id, .. } => Some(*bucket_id),
Self::Range { from, .. } => Some(*from),
}
}
pub const fn total(&self) -> Option<u64> {
match self {
Self::Auto => None,
Self::Bucket { total, .. } | Self::Range { total, .. } => Some(*total),
}
}
pub fn to(&self) -> Option<u64> {
match self {
Self::Auto => None,
Self::Bucket {
bucket_id,
concurrency,
total,
} => {
let buckets = total / concurrency;
Some(total - (buckets - bucket_id) - 1)
}
Self::Range { to, .. } => Some(*to),
}
}
}
impl Default for ShardScheme {
fn default() -> Self {
Self::Auto
}
}
impl<T: RangeBounds<u64>> TryFrom<(T, u64)> for ShardScheme {
type Error = ShardSchemeRangeError;
fn try_from((range, total): (T, u64)) -> Result<Self, Self::Error> {
let start = match range.start_bound() {
Bound::Excluded(num) => *num - 1,
Bound::Included(num) => *num,
Bound::Unbounded => 0,
};
let end = match range.end_bound() {
Bound::Excluded(num) => *num - 1,
Bound::Included(num) => *num,
Bound::Unbounded => total - 1,
};
if start > end {
return Err(ShardSchemeRangeError {
kind: ShardSchemeRangeErrorType::IdTooLarge { end, start, total },
});
}
Ok(Self::Range {
from: start,
to: end,
total,
})
}
}
impl TryFrom<(u64, u64, u64)> for ShardScheme {
type Error = ShardSchemeRangeError;
fn try_from((bucket_id, concurrency, total): (u64, u64, u64)) -> Result<Self, Self::Error> {
let buckets = total / concurrency;
if bucket_id >= buckets {
return Err(ShardSchemeRangeError {
kind: ShardSchemeRangeErrorType::BucketTooLarge {
bucket_id,
concurrency,
total,
},
});
}
Ok(ShardScheme::Bucket {
bucket_id,
concurrency,
total,
})
}
}
#[cfg(test)]
mod tests {
use super::{ShardScheme, ShardSchemeIter, ShardSchemeRangeError, ShardSchemeRangeErrorType};
use static_assertions::{assert_fields, assert_impl_all};
use std::{error::Error, fmt::Debug, hash::Hash};
assert_impl_all!(ShardSchemeIter: Clone, Debug, Send, Sync);
assert_fields!(ShardSchemeRangeErrorType::IdTooLarge: end, start, total);
assert_impl_all!(ShardSchemeRangeError: Error, Send, Sync);
assert_fields!(ShardScheme::Range: from, to, total);
assert_impl_all!(
ShardScheme: Clone,
Debug,
Default,
Eq,
Hash,
PartialEq,
Send,
Sync,
TryFrom<(u64, u64, u64)>,
);
#[test]
fn test_scheme() -> Result<(), Box<dyn Error>> {
assert_eq!(
ShardScheme::Range {
from: 0,
to: 9,
total: 10,
},
ShardScheme::try_from((0..=9, 10))?
);
Ok(())
}
#[test]
fn test_scheme_from() {
assert!(ShardScheme::Auto.from().is_none());
assert_eq!(
18,
ShardScheme::Bucket {
bucket_id: 18,
concurrency: 16,
total: 320,
}
.from()
.unwrap()
);
assert_eq!(
50,
ShardScheme::Range {
from: 50,
to: 99,
total: 200,
}
.from()
.unwrap()
);
}
#[test]
fn test_scheme_total() {
assert!(ShardScheme::Auto.total().is_none());
assert_eq!(
160,
ShardScheme::Bucket {
bucket_id: 3,
concurrency: 16,
total: 160,
}
.total()
.unwrap()
);
assert_eq!(
17,
ShardScheme::Range {
from: 0,
to: 9,
total: 17,
}
.total()
.unwrap()
);
}
#[test]
fn test_scheme_to() {
assert!(ShardScheme::Auto.to().is_none());
assert_eq!(
317,
ShardScheme::Bucket {
bucket_id: 18,
concurrency: 16,
total: 320,
}
.to()
.unwrap()
);
assert_eq!(
299,
ShardScheme::Bucket {
bucket_id: 0,
concurrency: 16,
total: 320,
}
.to()
.unwrap()
);
assert_eq!(
99,
ShardScheme::Range {
from: 50,
to: 99,
total: 200,
}
.to()
.unwrap()
);
}
#[test]
fn test_scheme_bucket_larger_than_concurrency() {
assert!(matches!(
ShardScheme::try_from((25, 16, 320)).unwrap_err(),
ShardSchemeRangeError {
kind: ShardSchemeRangeErrorType::BucketTooLarge { bucket_id, concurrency, total }}
if bucket_id == 25 && concurrency == 16 && total == 320
));
}
}