use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use rustc_hash::FxHashSet;
use serde::Deserialize;
use crate::protocols::WorkerWithDpRank;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AdmissionId(u64);
impl AdmissionId {
pub fn new(value: u64) -> Self {
Self(value)
}
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct RequestProgress {
context_tokens: Arc<AtomicUsize>,
}
#[derive(Debug, Clone)]
pub struct RequestProgressUpdater {
context_tokens: Arc<AtomicUsize>,
}
impl RequestProgress {
pub fn new(initial_context_tokens: usize) -> (Self, RequestProgressUpdater) {
let context_tokens = Arc::new(AtomicUsize::new(initial_context_tokens));
(
Self {
context_tokens: Arc::clone(&context_tokens),
},
RequestProgressUpdater { context_tokens },
)
}
#[inline]
pub fn context_tokens(&self) -> usize {
self.context_tokens.load(Ordering::Relaxed)
}
}
impl RequestProgressUpdater {
#[inline]
pub fn update_context_tokens(&self, context_tokens: usize) {
self.context_tokens
.fetch_max(context_tokens, Ordering::Relaxed);
}
}
#[derive(Clone)]
pub struct WorkerEligibility {
snapshot: Arc<dyn Fn() -> WorkerEligibilitySnapshot + Send + Sync>,
}
impl WorkerEligibility {
pub fn new(snapshot: impl Fn() -> WorkerEligibilitySnapshot + Send + Sync + 'static) -> Self {
Self {
snapshot: Arc::new(snapshot),
}
}
pub fn snapshot(&self) -> WorkerEligibilitySnapshot {
(self.snapshot)()
}
}
#[derive(Clone)]
pub struct WorkerEligibilitySnapshot {
structural: Arc<FxHashSet<WorkerWithDpRank>>,
available: Arc<FxHashSet<WorkerWithDpRank>>,
}
impl WorkerEligibilitySnapshot {
pub fn new(workers: impl IntoIterator<Item = WorkerWithDpRank>) -> Self {
let workers: Arc<FxHashSet<_>> = Arc::new(workers.into_iter().collect());
Self {
structural: Arc::clone(&workers),
available: workers,
}
}
pub fn with_availability(
structural: FxHashSet<WorkerWithDpRank>,
mut available: FxHashSet<WorkerWithDpRank>,
) -> Self {
available.retain(|worker| structural.contains(worker));
Self {
structural: Arc::new(structural),
available: Arc::new(available),
}
}
pub fn allows(&self, worker: WorkerWithDpRank) -> bool {
self.available.contains(&worker)
}
pub fn structurally_allows(&self, worker: WorkerWithDpRank) -> bool {
self.structural.contains(&worker)
}
pub fn has_available_worker(&self) -> bool {
!self.available.is_empty()
}
pub fn has_structural_worker(&self) -> bool {
!self.structural.is_empty()
}
}
#[derive(Clone)]
pub struct AdmissionRequest<'a> {
id: AdmissionId,
session_id: Option<&'a str>,
progress: RequestProgress,
worker_eligibility: WorkerEligibility,
}
impl<'a> AdmissionRequest<'a> {
pub fn new(
id: AdmissionId,
session_id: Option<&'a str>,
context_tokens: usize,
worker_eligibility: WorkerEligibility,
) -> Self {
let (progress, _) = RequestProgress::new(context_tokens);
Self::with_progress(id, session_id, progress, worker_eligibility)
}
pub(crate) fn with_progress(
id: AdmissionId,
session_id: Option<&'a str>,
progress: RequestProgress,
worker_eligibility: WorkerEligibility,
) -> Self {
Self {
id,
session_id,
progress,
worker_eligibility,
}
}
pub fn id(&self) -> AdmissionId {
self.id
}
pub fn session_id(&self) -> Option<&'a str> {
self.session_id
}
pub fn context_tokens(&self) -> usize {
self.progress.context_tokens()
}
pub fn progress(&self) -> &RequestProgress {
&self.progress
}
pub fn worker_eligibility(&self) -> &WorkerEligibility {
&self.worker_eligibility
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WorkerPlacement {
Any,
Exact(WorkerWithDpRank),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdmissionDecision {
Bypass,
Ready(WorkerPlacement),
Defer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdmissionEvent {
Dispatched {
id: AdmissionId,
worker: WorkerWithDpRank,
},
Completed {
id: AdmissionId,
context_tokens: usize,
},
Aborted { id: AdmissionId },
Reconcile,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdmissionAction {
MakeReady {
id: AdmissionId,
placement: WorkerPlacement,
},
}
pub trait PolicyClassAdmissionPolicy: Send {
fn admit(&mut self, request: AdmissionRequest<'_>) -> AdmissionDecision;
fn on_event(&mut self, _event: AdmissionEvent) -> Vec<AdmissionAction> {
Vec::new()
}
fn reconcile_interval(&self) -> Option<Duration> {
None
}
}
pub type PolicyClassAdmissionPolicies = HashMap<String, Box<dyn PolicyClassAdmissionPolicy>>;
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct AdmissionPolicyConfig {
#[serde(rename = "type")]
policy_type: String,
#[serde(flatten)]
options: serde_yaml::Mapping,
}
impl AdmissionPolicyConfig {
pub fn policy_type(&self) -> &str {
&self.policy_type
}
pub fn options(&self) -> &serde_yaml::Mapping {
&self.options
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AdmissionTicket {
pub class_index: usize,
pub id: AdmissionId,
}
pub(crate) struct ClassAdmissionAction {
pub class_index: usize,
pub action: AdmissionAction,
}
#[cfg(test)]
mod tests {
use super::*;
struct ReadyPolicy;
impl PolicyClassAdmissionPolicy for ReadyPolicy {
fn admit(&mut self, request: AdmissionRequest<'_>) -> AdmissionDecision {
assert_eq!(request.id(), AdmissionId::new(7));
assert_eq!(request.session_id(), Some("session"));
assert_eq!(request.context_tokens(), 42);
let worker = WorkerWithDpRank::new(3, 0);
let eligibility = request.worker_eligibility().snapshot();
assert!(eligibility.allows(worker));
assert!(eligibility.structurally_allows(worker));
AdmissionDecision::Ready(WorkerPlacement::Any)
}
}
#[test]
fn policy_contract_is_object_safe() {
let mut policy: Box<dyn PolicyClassAdmissionPolicy> = Box::new(ReadyPolicy);
let worker = WorkerWithDpRank::new(3, 0);
let eligibility = WorkerEligibility::new(move || WorkerEligibilitySnapshot::new([worker]));
assert_eq!(
policy.admit(AdmissionRequest::new(
AdmissionId::new(7),
Some("session"),
42,
eligibility,
)),
AdmissionDecision::Ready(WorkerPlacement::Any)
);
assert!(policy.on_event(AdmissionEvent::Reconcile).is_empty());
}
#[test]
fn admission_policy_config_keeps_policy_owned_options() {
let config: AdmissionPolicyConfig = serde_yaml::from_str(
"type: session_aware\npause_threshold: 0.9\ncustom_option: enabled\n",
)
.unwrap();
assert_eq!(config.policy_type(), "session_aware");
assert_eq!(
config.options()["pause_threshold"],
serde_yaml::Value::from(0.9)
);
assert_eq!(
config.options()["custom_option"],
serde_yaml::Value::from("enabled")
);
}
#[test]
fn request_progress_is_monotonic() {
let (progress, updater) = RequestProgress::new(42);
updater.update_context_tokens(55);
updater.update_context_tokens(50);
assert_eq!(progress.context_tokens(), 55);
}
#[test]
fn worker_eligibility_distinguishes_structure_from_availability() {
let available = WorkerWithDpRank::new(1, 0);
let overloaded = WorkerWithDpRank::new(2, 0);
let snapshot = WorkerEligibilitySnapshot::with_availability(
FxHashSet::from_iter([available, overloaded]),
FxHashSet::from_iter([available]),
);
assert!(snapshot.allows(available));
assert!(!snapshot.allows(overloaded));
assert!(snapshot.structurally_allows(overloaded));
assert!(snapshot.has_available_worker());
assert!(snapshot.has_structural_worker());
}
}