use std::collections::HashMap;
use std::sync::Arc;
use cronoq::TaskId;
use tonic::Status;
use tonic::metadata::MetadataValue;
use crate::config::ShardPlan;
pub const SHARD_HEADER: &str = "cronoq-shard";
pub const ENDPOINT_HEADER: &str = "cronoq-shard-endpoint";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Shard {
pub name: Arc<str>,
pub number: u8,
pub endpoint: Arc<str>,
}
#[derive(Debug)]
pub struct ShardMap {
own: usize,
shards: Vec<Shard>,
by_pool: HashMap<String, usize>,
by_number: HashMap<u8, usize>,
by_resource: HashMap<String, usize>,
}
impl ShardMap {
pub fn new(plan: &ShardPlan) -> ShardMap {
let mut map = ShardMap {
own: 0,
shards: Vec::new(),
by_pool: HashMap::new(),
by_number: HashMap::new(),
by_resource: HashMap::new(),
};
for (i, s) in plan.all.iter().enumerate() {
if s.name == plan.own.name {
map.own = i;
}
map.shards.push(Shard {
name: s.name.as_str().into(),
number: s.number,
endpoint: s.endpoint.as_str().into(),
});
map.by_number.insert(s.number, i);
for p in &s.pools {
map.by_pool.insert(p.clone(), i);
}
}
for (res, shard) in &plan.resources {
if let Some(i) = map.shards.iter().position(|s| *s.name == **shard) {
map.by_resource.insert(res.clone(), i);
}
}
map
}
pub fn own(&self) -> &Shard {
&self.shards[self.own]
}
pub fn others(&self) -> impl Iterator<Item = &Shard> {
self.shards
.iter()
.enumerate()
.filter(move |(i, _)| *i != self.own)
.map(|(_, s)| s)
}
pub fn other_for_pool(&self, pool: &str) -> Option<&Shard> {
let i = *self.by_pool.get(pool)?;
(i != self.own).then(|| &self.shards[i])
}
pub fn other_for_resource(&self, name: &str) -> Option<&Shard> {
let i = *self.by_resource.get(name)?;
(i != self.own).then(|| &self.shards[i])
}
pub fn other_for_id(&self, id: TaskId) -> Option<&Shard> {
let i = *self.by_number.get(&id.shard())?;
(i != self.own).then(|| &self.shards[i])
}
}
pub fn elsewhere(shard: &Shard, what: &str) -> Status {
let mut s = Status::failed_precondition(format!(
"{what} is served by shard '{}' at {}",
shard.name, shard.endpoint
));
if let (Ok(name), Ok(endpoint)) = (
MetadataValue::try_from(&*shard.name),
MetadataValue::try_from(&*shard.endpoint),
) {
s.metadata_mut().insert(SHARD_HEADER, name);
s.metadata_mut().insert(ENDPOINT_HEADER, endpoint);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ShardSection;
fn section(name: &str, number: u8, pools: &[&str]) -> ShardSection {
ShardSection {
name: name.into(),
number,
pools: pools.iter().map(|p| p.to_string()).collect(),
endpoint: format!("https://{name}.internal:7070"),
}
}
#[test]
fn pools_and_ids_map_to_their_shard() {
let all = vec![
section("cpu", 1, &["default", "batch"]),
section("gpu", 2, &["llm"]),
];
let map = ShardMap::new(&ShardPlan {
own: all[0].clone(),
all,
resources: vec![("api".into(), "gpu".into())],
});
assert_eq!(map.other_for_resource("api").map(|s| s.number), Some(2));
assert_eq!(map.other_for_resource("other"), None);
assert_eq!(&*map.own().name, "cpu");
assert_eq!(map.other_for_pool("default"), None);
assert_eq!(map.other_for_pool("unknown"), None);
assert_eq!(map.other_for_pool("llm").map(|s| s.number), Some(2));
assert_eq!(map.other_for_id(TaskId::first_in_shard(1, 3)), None);
assert_eq!(
map.other_for_id(TaskId::first_in_shard(2, 3))
.map(|s| &*s.name),
Some("gpu")
);
assert_eq!(map.other_for_id(TaskId::first_in_shard(9, 3)), None);
let s = elsewhere(map.other_for_pool("llm").unwrap(), "pool 'llm'");
assert_eq!(s.code(), tonic::Code::FailedPrecondition);
assert_eq!(s.metadata().get(SHARD_HEADER).unwrap(), "gpu");
assert_eq!(
s.metadata().get(ENDPOINT_HEADER).unwrap(),
"https://gpu.internal:7070"
);
}
}