use std::collections::HashSet;
use std::sync::Arc;
use chrono::{Duration, Utc};
use chronon_core::models::PartitionAssignment;
use chronon_core::store::SchedulerStore;
use chronon_core::Result;
use chronon_telemetry::TelemetrySink;
use tokio::sync::{Notify, RwLock};
use crate::partitioning;
pub struct PartitionAssigner {
store: Arc<dyn SchedulerStore>,
telemetry: Arc<dyn TelemetrySink>,
instance_id: String,
num_partitions: u32,
owned: Arc<RwLock<Vec<u32>>>,
}
impl PartitionAssigner {
pub fn new(
store: Arc<dyn SchedulerStore>,
telemetry: Arc<dyn TelemetrySink>,
instance_id: String,
num_partitions: u32,
) -> Self {
Self {
store,
telemetry,
instance_id,
num_partitions: num_partitions.max(1),
owned: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn owned_partitions(&self) -> Vec<u32> {
self.owned.read().await.clone()
}
async fn ensure_row(&self, pid: u32) -> Result<()> {
let key = pid.to_string();
let rows = self.store.list_partition_assignments().await?;
if rows.iter().any(|r| r.partition_id == key) {
return Ok(());
}
let epoch = chrono::DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z")
.expect("epoch")
.with_timezone(&Utc);
let now = Utc::now();
self.store
.upsert_partition_assignment(&PartitionAssignment {
partition_id: key,
owner_instance_id: String::new(),
lease_until: epoch,
updated_at: now,
})
.await
}
async fn try_steal_partition(&self, pid: u32) -> Result<bool> {
let key = pid.to_string();
let now = Utc::now();
let ttl = Duration::seconds(partitioning::partition_lease_ttl_secs());
let until = now + ttl;
let rows = self.store.list_partition_assignments().await?;
let Some(row) = rows.into_iter().find(|r| r.partition_id == key) else {
return Ok(false);
};
if row.lease_until > now && row.owner_instance_id != self.instance_id {
return Ok(false);
}
self.store
.upsert_partition_assignment(&PartitionAssignment {
partition_id: key,
owner_instance_id: self.instance_id.clone(),
lease_until: until,
updated_at: now,
})
.await?;
Ok(true)
}
async fn renew_owned(&self, pid: u32) -> Result<()> {
let key = pid.to_string();
let rows = self.store.list_partition_assignments().await?;
let Some(row) = rows.into_iter().find(|r| r.partition_id == key) else {
return Ok(());
};
if row.owner_instance_id != self.instance_id {
return Ok(());
}
let now = Utc::now();
let until = now + Duration::seconds(partitioning::partition_lease_ttl_secs());
self.store
.upsert_partition_assignment(&PartitionAssignment {
partition_id: key,
owner_instance_id: self.instance_id.clone(),
lease_until: until,
updated_at: now,
})
.await
}
pub async fn refresh_leases(&self) -> Result<()> {
for pid in 0..self.num_partitions {
self.ensure_row(pid).await?;
}
let rows = self.store.list_partition_assignments().await?;
let mut alive_owners = HashSet::new();
for row in &rows {
if row.lease_until > Utc::now() && !row.owner_instance_id.is_empty() {
alive_owners.insert(row.owner_instance_id.clone());
}
}
let live = alive_owners.len().max(1);
let target = (self.num_partitions as usize).div_ceil(live);
let mut mine: Vec<u32> = rows
.iter()
.filter(|r| {
r.owner_instance_id == self.instance_id
&& r.lease_until > Utc::now()
&& r.partition_id.parse::<u32>().is_ok()
})
.filter_map(|r| r.partition_id.parse().ok())
.collect();
for &pid in &mine {
let _ = self.renew_owned(pid).await;
}
if mine.len() < target {
for pid in 0..self.num_partitions {
if mine.len() >= target {
break;
}
if mine.contains(&pid) {
continue;
}
if self.try_steal_partition(pid).await? {
mine.push(pid);
mine.sort_unstable();
}
}
}
mine.sort_unstable();
*self.owned.write().await = mine;
Ok(())
}
pub async fn run_lease_loop(self: Arc<Self>, shutdown: Arc<Notify>) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
partitioning::partition_lease_renew_interval_secs(),
));
loop {
tokio::select! {
_ = shutdown.notified() => break,
_ = interval.tick() => {
if let Err(e) = self.refresh_leases().await {
self.telemetry.log_event(
"chronon_scheduler_warn",
&[("component", "partition_assigner"), ("message", &e.to_string())],
);
} else {
for partition in self.owned_partitions().await {
self.telemetry.record_counter(
"chronon_partition_assignments",
&[("partition", &partition.to_string())],
1,
);
}
}
}
}
}
}
pub async fn assign_all_embedded(&self) {
let all: Vec<u32> = (0..self.num_partitions).collect();
*self.owned.write().await = all;
}
}