use std::collections::BTreeMap;
use std::sync::Arc;
use futures::StreamExt;
use rskit_errors::AppResult;
use serde::{Deserialize, Serialize};
use crate::{Handler, Pool, PoolConfig};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkloadConfig {
#[serde(default = "default_max_concurrent")]
pub max_concurrent: usize,
#[serde(default = "default_queue_size")]
pub queue_size: usize,
}
impl Default for WorkloadConfig {
fn default() -> Self {
Self {
max_concurrent: default_max_concurrent(),
queue_size: default_queue_size(),
}
}
}
impl WorkloadConfig {
#[must_use]
pub fn to_pool_config(&self, name: impl Into<String>) -> PoolConfig {
PoolConfig::new(name)
.with_size(self.max_concurrent.max(1))
.with_queue_size(self.queue_size.max(1))
}
}
const fn default_max_concurrent() -> usize {
4
}
const fn default_queue_size() -> usize {
256
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkloadSpec {
pub name: String,
#[serde(default)]
pub resources: ResourceRequirements,
#[serde(default)]
pub labels: BTreeMap<String, String>,
}
impl WorkloadSpec {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
resources: ResourceRequirements::default(),
labels: BTreeMap::new(),
}
}
#[must_use]
pub const fn with_resources(mut self, resources: ResourceRequirements) -> Self {
self.resources = resources;
self
}
#[must_use]
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ResourceRequirements {
#[serde(default = "default_cpu_units")]
pub cpu_units: u32,
#[serde(default = "default_memory_mib")]
pub memory_mib: u64,
}
impl Default for ResourceRequirements {
fn default() -> Self {
Self {
cpu_units: default_cpu_units(),
memory_mib: default_memory_mib(),
}
}
}
const fn default_cpu_units() -> u32 {
1
}
const fn default_memory_mib() -> u64 {
128
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchedulingDecision {
pub workload: String,
pub pool: String,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkloadBatch {
pub name: String,
pub workloads: Vec<WorkloadSpec>,
}
impl WorkloadBatch {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
workloads: Vec::new(),
}
}
#[must_use]
pub fn with_workload(mut self, workload: WorkloadSpec) -> Self {
self.workloads.push(workload);
self
}
pub fn into_stream(self) -> impl futures::Stream<Item = WorkloadSpec> + Send + 'static {
futures::stream::iter(self.workloads)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExecutionPlan {
pub batch: String,
pub decisions: Vec<SchedulingDecision>,
}
#[async_trait::async_trait]
pub trait WorkloadScheduler: Send + Sync {
async fn schedule(&self, spec: &WorkloadSpec) -> AppResult<SchedulingDecision>;
async fn plan_batch(&self, batch: WorkloadBatch) -> AppResult<ExecutionPlan> {
let name = batch.name.clone();
let mut stream = batch.into_stream();
let mut decisions = Vec::new();
while let Some(workload) = stream.next().await {
decisions.push(self.schedule(&workload).await?);
}
Ok(ExecutionPlan {
batch: name,
decisions,
})
}
}
#[derive(Debug, Clone)]
pub struct WorkerScheduler {
pool_name: String,
config: WorkloadConfig,
}
impl WorkerScheduler {
#[must_use]
pub fn new(pool_name: impl Into<String>, config: WorkloadConfig) -> Self {
Self {
pool_name: pool_name.into(),
config,
}
}
#[must_use]
pub fn pool<I, O, H>(&self, handler: Arc<H>) -> Pool<I, O>
where
I: Send + 'static,
O: Clone + Send + 'static,
H: Handler<I, O> + 'static,
{
Pool::new(handler, self.config.to_pool_config(self.pool_name.clone()))
}
}
#[async_trait::async_trait]
impl WorkloadScheduler for WorkerScheduler {
async fn schedule(&self, spec: &WorkloadSpec) -> AppResult<SchedulingDecision> {
Ok(SchedulingDecision {
workload: spec.name.clone(),
pool: self.pool_name.clone(),
reason: format!(
"worker pool capacity={} queue={}",
self.config.max_concurrent, self.config.queue_size
),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn worker_scheduler_returns_observable_decision() {
let scheduler = WorkerScheduler::new("default", WorkloadConfig::default());
let decision = scheduler
.schedule(&WorkloadSpec::new("images").with_label("tier", "batch"))
.await
.unwrap();
assert_eq!(decision.workload, "images");
assert_eq!(decision.pool, "default");
assert!(decision.reason.contains("capacity"));
}
#[tokio::test]
async fn worker_scheduler_plans_batch_through_stream() {
let scheduler = WorkerScheduler::new("default", WorkloadConfig::default());
let plan = scheduler
.plan_batch(
WorkloadBatch::new("batch")
.with_workload(WorkloadSpec::new("first"))
.with_workload(WorkloadSpec::new("second")),
)
.await
.unwrap();
assert_eq!(plan.batch, "batch");
assert_eq!(plan.decisions.len(), 2);
assert_eq!(plan.decisions[0].workload, "first");
assert_eq!(plan.decisions[1].workload, "second");
}
}