6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 1 | //! fifo with easy backfill. the first job that can't start gets a reservation, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 2 | //! and anyone behind it only cuts in if they'll be gone before it starts or fit |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 3 | //! in the leftovers once it does |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 4 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 5 | use std::{collections::BTreeMap, time::Duration}; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 6 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 7 | use serde::{Deserialize, Serialize}; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 8 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 9 | use crate::units::Bytes; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 10 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 11 | pub type JobId = u64; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 12 | pub type FsId = u64; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 13 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 14 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 15 | pub struct Need { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 16 | pub ram: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 17 | pub disk: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 18 | pub cores: u32, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 19 | pub time: Duration, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 20 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 21 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 22 | #[derive(Clone, Debug)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 23 | pub struct Queued { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 24 | pub id: JobId, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 25 | pub need: Need, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 26 | pub heavy: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 27 | pub fs: Vec<FsId>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 28 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 29 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 30 | #[derive(Clone, Debug)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 31 | pub struct Running { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 32 | pub need: Need, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 33 | pub heavy: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 34 | pub fs: Vec<FsId>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 35 | pub rss: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 36 | /// zero once overdue |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 37 | pub remaining: Duration, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 38 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 39 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 40 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 41 | pub struct Disk { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 42 | pub free: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 43 | pub reserve: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 44 | /// written since the fs last went from idle to busy. free space already |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 45 | /// reflects this, so it's netted off promises to avoid counting it twice |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 46 | pub consumed: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 47 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 48 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 49 | #[derive(Clone, Debug)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 50 | pub struct Capacity { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 51 | pub ram_budget: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 52 | pub ram_live: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 53 | pub ram_reserve: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 54 | pub cores: u32, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 55 | pub disks: BTreeMap<FsId, Disk>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 56 | pub pressure: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 57 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 58 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 59 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 60 | #[serde(rename_all = "snake_case", tag = "reason", content = "fs")] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 61 | pub enum Reason { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 62 | HeavyRunning, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 63 | Pressure, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 64 | Ram, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 65 | Cores, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 66 | Disk(FsId), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 67 | Reserved, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 68 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 69 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 70 | impl std::fmt::Display for Reason { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 71 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 72 | f.write_str(match self { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 73 | Self::HeavyRunning => "another heavy build is running", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 74 | Self::Pressure => "memory pressure is high", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 75 | Self::Ram => "not enough free RAM", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 76 | Self::Cores => "not enough free cores", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 77 | Self::Disk(_) => "not enough free disk", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 78 | Self::Reserved => "waiting behind a reserved build", |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 79 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 80 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 81 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 82 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 83 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 84 | pub enum Verdict { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 85 | Admit, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 86 | Wait(Reason), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 87 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 88 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 89 | #[derive(Clone, Debug)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 90 | struct Free { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 91 | ram: Bytes, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 92 | cores: u32, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 93 | disk: BTreeMap<FsId, Bytes>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 94 | heavy_slot: bool, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 95 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 96 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 97 | impl Free { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 98 | fn shortfall(&self, need: &Need, fs: &[FsId], heavy: bool) -> Option<Reason> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 99 | if heavy && !self.heavy_slot { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 100 | return Some(Reason::HeavyRunning); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 101 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 102 | if need.ram > self.ram { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 103 | return Some(Reason::Ram); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 104 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 105 | if need.cores > self.cores { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 106 | return Some(Reason::Cores); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 107 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 108 | fs.iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 109 | .find(|id| self.disk.get(id).is_some_and(|free| need.disk > *free)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 110 | .map(|id| Reason::Disk(*id)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 111 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 112 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 113 | fn take(&mut self, need: &Need, fs: &[FsId], heavy: bool) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 114 | self.ram = self.ram.saturating_sub(need.ram); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 115 | self.cores = self.cores.saturating_sub(need.cores); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 116 | for id in fs { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 117 | if let Some(free) = self.disk.get_mut(id) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 118 | *free = free.saturating_sub(need.disk); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 119 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 120 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 121 | if heavy { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 122 | self.heavy_slot = false; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 123 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 124 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 125 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 126 | /// no disk here. builds leave their output behind when they finish |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 127 | fn give_back(&mut self, ram: Bytes, cores: u32, heavy: bool) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 128 | self.ram += ram; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 129 | self.cores += cores; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 130 | if heavy { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 131 | self.heavy_slot = true; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 132 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 133 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 134 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 135 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 136 | fn clamp(need: Need, cap: &Capacity) -> Need { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 137 | Need { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 138 | ram: need.ram.min(cap.ram_budget), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 139 | cores: need.cores.clamp(1, cap.cores.max(1)), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 140 | ..need |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 141 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 142 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 143 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 144 | fn free_now(running: &[Running], cap: &Capacity) -> Free { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 145 | let charged: Bytes = running.iter().map(|r| r.need.ram.max(r.rss)).sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 146 | let unrealised: Bytes = running |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 147 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 148 | .map(|r| r.need.ram.saturating_sub(r.rss)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 149 | .sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 150 | let by_budget = cap.ram_budget.saturating_sub(charged); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 151 | let by_live = cap |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 152 | .ram_live |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 153 | .saturating_sub(cap.ram_reserve) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 154 | .saturating_sub(unrealised); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 155 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 156 | let disk = cap |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 157 | .disks |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 158 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 159 | .map(|(id, d)| { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 160 | let promised: Bytes = running |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 161 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 162 | .filter(|r| r.fs.contains(id)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 163 | .map(|r| r.need.disk) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 164 | .sum(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 165 | let outstanding = promised.saturating_sub(d.consumed); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 166 | ( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 167 | *id, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 168 | d.free.saturating_sub(d.reserve).saturating_sub(outstanding), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 169 | ) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 170 | }) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 171 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 172 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 173 | Free { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 174 | ram: by_budget.min(by_live), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 175 | cores: cap |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 176 | .cores |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 177 | .saturating_sub(running.iter().map(|r| r.need.cores).sum()), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 178 | disk, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 179 | heavy_slot: !running.iter().any(|r| r.heavy), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 180 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 181 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 182 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 183 | pub fn headroom(running: &[Running], cap: &Capacity) -> (Bytes, u32) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 184 | let free = free_now(running, cap); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 185 | (free.ram, free.cores) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 186 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 187 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 188 | struct Reservation { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 189 | /// `None` when nothing finishing would free enough, e.g. disk or someone |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 190 | /// else's ram. backfill falls back to the time cap then |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 191 | shadow: Option<Duration>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 192 | extra: Option<Free>, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 193 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 194 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 195 | fn reserve( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 196 | job: &Queued, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 197 | need: &Need, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 198 | free: &Free, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 199 | ends: &[(Duration, Bytes, u32, bool)], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 200 | ) -> Reservation { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 201 | let mut sim = free.clone(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 202 | let mut ends = ends.to_vec(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 203 | ends.sort_by_key(|e| e.0); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 204 | for (at, ram, cores, heavy) in ends { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 205 | sim.give_back(ram, cores, heavy); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 206 | if sim.shortfall(need, &job.fs, job.heavy).is_none() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 207 | sim.take(need, &job.fs, job.heavy); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 208 | return Reservation { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 209 | shadow: Some(at), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 210 | extra: Some(sim), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 211 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 212 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 213 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 214 | Reservation { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 215 | shadow: None, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 216 | extra: None, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 217 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 218 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 219 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 220 | pub fn plan( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 221 | queue: &[Queued], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 222 | running: &[Running], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 223 | cap: &Capacity, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 224 | backfill_max: Duration, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 225 | ) -> Vec<(JobId, Verdict)> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 226 | let mut free = free_now(running, cap); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 227 | let mut ends: Vec<_> = running |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 228 | .iter() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 229 | .map(|r| (r.remaining, r.need.ram.max(r.rss), r.need.cores, r.heavy)) |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 230 | .collect(); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 231 | let mut reservation: Option<Reservation> = None; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 232 | let mut out = Vec::with_capacity(queue.len()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 233 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 234 | for job in queue { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 235 | let need = clamp(job.need, cap); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 236 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 237 | if job.heavy && cap.pressure { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 238 | out.push((job.id, Verdict::Wait(Reason::Pressure))); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 239 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 240 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 241 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 242 | if let Some(why) = free.shortfall(&need, &job.fs, job.heavy) { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 243 | if reservation.is_none() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 244 | reservation = Some(reserve(job, &need, &free, &ends)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 245 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 246 | out.push((job.id, Verdict::Wait(why))); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 247 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 248 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 249 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 250 | if let Some(res) = &mut reservation { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 251 | let finishes_first = match res.shadow { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 252 | Some(shadow) => need.time <= shadow, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 253 | None => need.time <= backfill_max, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 254 | }; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 255 | let fits_extra = res |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 256 | .extra |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 257 | .as_ref() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 258 | .is_some_and(|x| x.shortfall(&need, &job.fs, job.heavy).is_none()); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 259 | if !finishes_first && !fits_extra { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 260 | out.push((job.id, Verdict::Wait(Reason::Reserved))); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 261 | continue; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 262 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 263 | if !finishes_first && let Some(extra) = &mut res.extra { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 264 | extra.take(&need, &job.fs, job.heavy); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 265 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 266 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 267 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 268 | free.take(&need, &job.fs, job.heavy); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 269 | ends.push((need.time, need.ram, need.cores, job.heavy)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 270 | out.push((job.id, Verdict::Admit)); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 271 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 272 | out |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 273 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 274 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 275 | #[cfg(test)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 276 | mod tests { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 277 | use super::*; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 278 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 279 | const FS: FsId = 1; |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 280 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 281 | fn need(ram_gib: u64, cores: u32, disk_gib: u64, mins: u64) -> Need { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 282 | Need { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 283 | ram: Bytes::gib(ram_gib), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 284 | disk: Bytes::gib(disk_gib), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 285 | cores, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 286 | time: Duration::from_secs(mins * 60), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 287 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 288 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 289 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 290 | fn q(id: JobId, need: Need, heavy: bool) -> Queued { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 291 | Queued { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 292 | id, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 293 | need, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 294 | heavy, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 295 | fs: vec![FS], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 296 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 297 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 298 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 299 | fn cap() -> Capacity { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 300 | Capacity { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 301 | ram_budget: Bytes::gib(24), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 302 | ram_live: Bytes::gib(28), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 303 | ram_reserve: Bytes::gib(4), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 304 | cores: 16, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 305 | disks: BTreeMap::from([( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 306 | FS, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 307 | Disk { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 308 | free: Bytes::gib(100), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 309 | reserve: Bytes::gib(20), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 310 | consumed: Bytes::ZERO, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 311 | }, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 312 | )]), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 313 | pressure: false, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 314 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 315 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 316 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 317 | fn verdicts(plan: &[(JobId, Verdict)]) -> Vec<Verdict> { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 318 | plan.iter().map(|(_, v)| *v).collect() |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 319 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 320 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 321 | const BF: Duration = Duration::from_secs(120); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 322 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 323 | #[test] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 324 | fn two_heavies_never_collide() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 325 | let out = plan( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 326 | &[q(1, need(8, 8, 5, 10), true), q(2, need(8, 4, 5, 10), true)], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 327 | &[], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 328 | &cap(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 329 | BF, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 330 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 331 | assert_eq!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 332 | verdicts(&out), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 333 | [Verdict::Admit, Verdict::Wait(Reason::HeavyRunning)] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 334 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 335 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 336 | |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 337 | #[test] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 338 | fn disk_never_double_booked() { |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 339 | let out = plan( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 340 | &[ |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 341 | q(1, need(1, 1, 50, 10), false), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 342 | q(2, need(1, 1, 50, 10), false), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 343 | ], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 344 | &[], |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 345 | &cap(), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 346 | BF, |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 347 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 348 | assert_eq!( |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 349 | verdicts(&out), |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 350 | [Verdict::Admit, Verdict::Wait(Reason::Disk(FS))] |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 351 | ); |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 352 | } |
6f5b84fad3ef | atagen | 2026-09-27 03:59 UTC | 353 | } |