atagen

roundabout

public
Star0
Watch0
6f5b84fad3ef5c6affdba81ee1864ee4423cd1c8
1
ssh://brewery@pls.lobotomise.me:33/atagen/roundabout.git

Blame: src/sched.rs

6f5b84fad3efatagen2026-09-27 03:59 UTC1
//! fifo with easy backfill. the first job that can't start gets a reservation,
6f5b84fad3efatagen2026-09-27 03:59 UTC2
//! and anyone behind it only cuts in if they'll be gone before it starts or fit
6f5b84fad3efatagen2026-09-27 03:59 UTC3
//! in the leftovers once it does
6f5b84fad3efatagen2026-09-27 03:59 UTC4
6f5b84fad3efatagen2026-09-27 03:59 UTC5
use std::{collections::BTreeMap, time::Duration};
6f5b84fad3efatagen2026-09-27 03:59 UTC6
6f5b84fad3efatagen2026-09-27 03:59 UTC7
use serde::{Deserialize, Serialize};
6f5b84fad3efatagen2026-09-27 03:59 UTC8
6f5b84fad3efatagen2026-09-27 03:59 UTC9
use crate::units::Bytes;
6f5b84fad3efatagen2026-09-27 03:59 UTC10
6f5b84fad3efatagen2026-09-27 03:59 UTC11
pub type JobId = u64;
6f5b84fad3efatagen2026-09-27 03:59 UTC12
pub type FsId = u64;
6f5b84fad3efatagen2026-09-27 03:59 UTC13
6f5b84fad3efatagen2026-09-27 03:59 UTC14
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
6f5b84fad3efatagen2026-09-27 03:59 UTC15
pub struct Need {
6f5b84fad3efatagen2026-09-27 03:59 UTC16
    pub ram: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC17
    pub disk: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC18
    pub cores: u32,
6f5b84fad3efatagen2026-09-27 03:59 UTC19
    pub time: Duration,
6f5b84fad3efatagen2026-09-27 03:59 UTC20
}
6f5b84fad3efatagen2026-09-27 03:59 UTC21
6f5b84fad3efatagen2026-09-27 03:59 UTC22
#[derive(Clone, Debug)]
6f5b84fad3efatagen2026-09-27 03:59 UTC23
pub struct Queued {
6f5b84fad3efatagen2026-09-27 03:59 UTC24
    pub id: JobId,
6f5b84fad3efatagen2026-09-27 03:59 UTC25
    pub need: Need,
6f5b84fad3efatagen2026-09-27 03:59 UTC26
    pub heavy: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC27
    pub fs: Vec<FsId>,
6f5b84fad3efatagen2026-09-27 03:59 UTC28
}
6f5b84fad3efatagen2026-09-27 03:59 UTC29
6f5b84fad3efatagen2026-09-27 03:59 UTC30
#[derive(Clone, Debug)]
6f5b84fad3efatagen2026-09-27 03:59 UTC31
pub struct Running {
6f5b84fad3efatagen2026-09-27 03:59 UTC32
    pub need: Need,
6f5b84fad3efatagen2026-09-27 03:59 UTC33
    pub heavy: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC34
    pub fs: Vec<FsId>,
6f5b84fad3efatagen2026-09-27 03:59 UTC35
    pub rss: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC36
    /// zero once overdue
6f5b84fad3efatagen2026-09-27 03:59 UTC37
    pub remaining: Duration,
6f5b84fad3efatagen2026-09-27 03:59 UTC38
}
6f5b84fad3efatagen2026-09-27 03:59 UTC39
6f5b84fad3efatagen2026-09-27 03:59 UTC40
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6f5b84fad3efatagen2026-09-27 03:59 UTC41
pub struct Disk {
6f5b84fad3efatagen2026-09-27 03:59 UTC42
    pub free: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC43
    pub reserve: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC44
    /// written since the fs last went from idle to busy. free space already
6f5b84fad3efatagen2026-09-27 03:59 UTC45
    /// reflects this, so it's netted off promises to avoid counting it twice
6f5b84fad3efatagen2026-09-27 03:59 UTC46
    pub consumed: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC47
}
6f5b84fad3efatagen2026-09-27 03:59 UTC48
6f5b84fad3efatagen2026-09-27 03:59 UTC49
#[derive(Clone, Debug)]
6f5b84fad3efatagen2026-09-27 03:59 UTC50
pub struct Capacity {
6f5b84fad3efatagen2026-09-27 03:59 UTC51
    pub ram_budget: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC52
    pub ram_live: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC53
    pub ram_reserve: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC54
    pub cores: u32,
6f5b84fad3efatagen2026-09-27 03:59 UTC55
    pub disks: BTreeMap<FsId, Disk>,
6f5b84fad3efatagen2026-09-27 03:59 UTC56
    pub pressure: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC57
}
6f5b84fad3efatagen2026-09-27 03:59 UTC58
6f5b84fad3efatagen2026-09-27 03:59 UTC59
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
6f5b84fad3efatagen2026-09-27 03:59 UTC60
#[serde(rename_all = "snake_case", tag = "reason", content = "fs")]
6f5b84fad3efatagen2026-09-27 03:59 UTC61
pub enum Reason {
6f5b84fad3efatagen2026-09-27 03:59 UTC62
    HeavyRunning,
6f5b84fad3efatagen2026-09-27 03:59 UTC63
    Pressure,
6f5b84fad3efatagen2026-09-27 03:59 UTC64
    Ram,
6f5b84fad3efatagen2026-09-27 03:59 UTC65
    Cores,
6f5b84fad3efatagen2026-09-27 03:59 UTC66
    Disk(FsId),
6f5b84fad3efatagen2026-09-27 03:59 UTC67
    Reserved,
6f5b84fad3efatagen2026-09-27 03:59 UTC68
}
6f5b84fad3efatagen2026-09-27 03:59 UTC69
6f5b84fad3efatagen2026-09-27 03:59 UTC70
impl std::fmt::Display for Reason {
6f5b84fad3efatagen2026-09-27 03:59 UTC71
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6f5b84fad3efatagen2026-09-27 03:59 UTC72
        f.write_str(match self {
6f5b84fad3efatagen2026-09-27 03:59 UTC73
            Self::HeavyRunning => "another heavy build is running",
6f5b84fad3efatagen2026-09-27 03:59 UTC74
            Self::Pressure => "memory pressure is high",
6f5b84fad3efatagen2026-09-27 03:59 UTC75
            Self::Ram => "not enough free RAM",
6f5b84fad3efatagen2026-09-27 03:59 UTC76
            Self::Cores => "not enough free cores",
6f5b84fad3efatagen2026-09-27 03:59 UTC77
            Self::Disk(_) => "not enough free disk",
6f5b84fad3efatagen2026-09-27 03:59 UTC78
            Self::Reserved => "waiting behind a reserved build",
6f5b84fad3efatagen2026-09-27 03:59 UTC79
        })
6f5b84fad3efatagen2026-09-27 03:59 UTC80
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC81
}
6f5b84fad3efatagen2026-09-27 03:59 UTC82
6f5b84fad3efatagen2026-09-27 03:59 UTC83
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6f5b84fad3efatagen2026-09-27 03:59 UTC84
pub enum Verdict {
6f5b84fad3efatagen2026-09-27 03:59 UTC85
    Admit,
6f5b84fad3efatagen2026-09-27 03:59 UTC86
    Wait(Reason),
6f5b84fad3efatagen2026-09-27 03:59 UTC87
}
6f5b84fad3efatagen2026-09-27 03:59 UTC88
6f5b84fad3efatagen2026-09-27 03:59 UTC89
#[derive(Clone, Debug)]
6f5b84fad3efatagen2026-09-27 03:59 UTC90
struct Free {
6f5b84fad3efatagen2026-09-27 03:59 UTC91
    ram: Bytes,
6f5b84fad3efatagen2026-09-27 03:59 UTC92
    cores: u32,
6f5b84fad3efatagen2026-09-27 03:59 UTC93
    disk: BTreeMap<FsId, Bytes>,
6f5b84fad3efatagen2026-09-27 03:59 UTC94
    heavy_slot: bool,
6f5b84fad3efatagen2026-09-27 03:59 UTC95
}
6f5b84fad3efatagen2026-09-27 03:59 UTC96
6f5b84fad3efatagen2026-09-27 03:59 UTC97
impl Free {
6f5b84fad3efatagen2026-09-27 03:59 UTC98
    fn shortfall(&self, need: &Need, fs: &[FsId], heavy: bool) -> Option<Reason> {
6f5b84fad3efatagen2026-09-27 03:59 UTC99
        if heavy && !self.heavy_slot {
6f5b84fad3efatagen2026-09-27 03:59 UTC100
            return Some(Reason::HeavyRunning);
6f5b84fad3efatagen2026-09-27 03:59 UTC101
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC102
        if need.ram > self.ram {
6f5b84fad3efatagen2026-09-27 03:59 UTC103
            return Some(Reason::Ram);
6f5b84fad3efatagen2026-09-27 03:59 UTC104
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC105
        if need.cores > self.cores {
6f5b84fad3efatagen2026-09-27 03:59 UTC106
            return Some(Reason::Cores);
6f5b84fad3efatagen2026-09-27 03:59 UTC107
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC108
        fs.iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC109
            .find(|id| self.disk.get(id).is_some_and(|free| need.disk > *free))
6f5b84fad3efatagen2026-09-27 03:59 UTC110
            .map(|id| Reason::Disk(*id))
6f5b84fad3efatagen2026-09-27 03:59 UTC111
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC112
6f5b84fad3efatagen2026-09-27 03:59 UTC113
    fn take(&mut self, need: &Need, fs: &[FsId], heavy: bool) {
6f5b84fad3efatagen2026-09-27 03:59 UTC114
        self.ram = self.ram.saturating_sub(need.ram);
6f5b84fad3efatagen2026-09-27 03:59 UTC115
        self.cores = self.cores.saturating_sub(need.cores);
6f5b84fad3efatagen2026-09-27 03:59 UTC116
        for id in fs {
6f5b84fad3efatagen2026-09-27 03:59 UTC117
            if let Some(free) = self.disk.get_mut(id) {
6f5b84fad3efatagen2026-09-27 03:59 UTC118
                *free = free.saturating_sub(need.disk);
6f5b84fad3efatagen2026-09-27 03:59 UTC119
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC120
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC121
        if heavy {
6f5b84fad3efatagen2026-09-27 03:59 UTC122
            self.heavy_slot = false;
6f5b84fad3efatagen2026-09-27 03:59 UTC123
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC124
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC125
6f5b84fad3efatagen2026-09-27 03:59 UTC126
    /// no disk here. builds leave their output behind when they finish
6f5b84fad3efatagen2026-09-27 03:59 UTC127
    fn give_back(&mut self, ram: Bytes, cores: u32, heavy: bool) {
6f5b84fad3efatagen2026-09-27 03:59 UTC128
        self.ram += ram;
6f5b84fad3efatagen2026-09-27 03:59 UTC129
        self.cores += cores;
6f5b84fad3efatagen2026-09-27 03:59 UTC130
        if heavy {
6f5b84fad3efatagen2026-09-27 03:59 UTC131
            self.heavy_slot = true;
6f5b84fad3efatagen2026-09-27 03:59 UTC132
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC133
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC134
}
6f5b84fad3efatagen2026-09-27 03:59 UTC135
6f5b84fad3efatagen2026-09-27 03:59 UTC136
fn clamp(need: Need, cap: &Capacity) -> Need {
6f5b84fad3efatagen2026-09-27 03:59 UTC137
    Need {
6f5b84fad3efatagen2026-09-27 03:59 UTC138
        ram: need.ram.min(cap.ram_budget),
6f5b84fad3efatagen2026-09-27 03:59 UTC139
        cores: need.cores.clamp(1, cap.cores.max(1)),
6f5b84fad3efatagen2026-09-27 03:59 UTC140
        ..need
6f5b84fad3efatagen2026-09-27 03:59 UTC141
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC142
}
6f5b84fad3efatagen2026-09-27 03:59 UTC143
6f5b84fad3efatagen2026-09-27 03:59 UTC144
fn free_now(running: &[Running], cap: &Capacity) -> Free {
6f5b84fad3efatagen2026-09-27 03:59 UTC145
    let charged: Bytes = running.iter().map(|r| r.need.ram.max(r.rss)).sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC146
    let unrealised: Bytes = running
6f5b84fad3efatagen2026-09-27 03:59 UTC147
        .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC148
        .map(|r| r.need.ram.saturating_sub(r.rss))
6f5b84fad3efatagen2026-09-27 03:59 UTC149
        .sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC150
    let by_budget = cap.ram_budget.saturating_sub(charged);
6f5b84fad3efatagen2026-09-27 03:59 UTC151
    let by_live = cap
6f5b84fad3efatagen2026-09-27 03:59 UTC152
        .ram_live
6f5b84fad3efatagen2026-09-27 03:59 UTC153
        .saturating_sub(cap.ram_reserve)
6f5b84fad3efatagen2026-09-27 03:59 UTC154
        .saturating_sub(unrealised);
6f5b84fad3efatagen2026-09-27 03:59 UTC155
6f5b84fad3efatagen2026-09-27 03:59 UTC156
    let disk = cap
6f5b84fad3efatagen2026-09-27 03:59 UTC157
        .disks
6f5b84fad3efatagen2026-09-27 03:59 UTC158
        .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC159
        .map(|(id, d)| {
6f5b84fad3efatagen2026-09-27 03:59 UTC160
            let promised: Bytes = running
6f5b84fad3efatagen2026-09-27 03:59 UTC161
                .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC162
                .filter(|r| r.fs.contains(id))
6f5b84fad3efatagen2026-09-27 03:59 UTC163
                .map(|r| r.need.disk)
6f5b84fad3efatagen2026-09-27 03:59 UTC164
                .sum();
6f5b84fad3efatagen2026-09-27 03:59 UTC165
            let outstanding = promised.saturating_sub(d.consumed);
6f5b84fad3efatagen2026-09-27 03:59 UTC166
            (
6f5b84fad3efatagen2026-09-27 03:59 UTC167
                *id,
6f5b84fad3efatagen2026-09-27 03:59 UTC168
                d.free.saturating_sub(d.reserve).saturating_sub(outstanding),
6f5b84fad3efatagen2026-09-27 03:59 UTC169
            )
6f5b84fad3efatagen2026-09-27 03:59 UTC170
        })
6f5b84fad3efatagen2026-09-27 03:59 UTC171
        .collect();
6f5b84fad3efatagen2026-09-27 03:59 UTC172
6f5b84fad3efatagen2026-09-27 03:59 UTC173
    Free {
6f5b84fad3efatagen2026-09-27 03:59 UTC174
        ram: by_budget.min(by_live),
6f5b84fad3efatagen2026-09-27 03:59 UTC175
        cores: cap
6f5b84fad3efatagen2026-09-27 03:59 UTC176
            .cores
6f5b84fad3efatagen2026-09-27 03:59 UTC177
            .saturating_sub(running.iter().map(|r| r.need.cores).sum()),
6f5b84fad3efatagen2026-09-27 03:59 UTC178
        disk,
6f5b84fad3efatagen2026-09-27 03:59 UTC179
        heavy_slot: !running.iter().any(|r| r.heavy),
6f5b84fad3efatagen2026-09-27 03:59 UTC180
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC181
}
6f5b84fad3efatagen2026-09-27 03:59 UTC182
6f5b84fad3efatagen2026-09-27 03:59 UTC183
pub fn headroom(running: &[Running], cap: &Capacity) -> (Bytes, u32) {
6f5b84fad3efatagen2026-09-27 03:59 UTC184
    let free = free_now(running, cap);
6f5b84fad3efatagen2026-09-27 03:59 UTC185
    (free.ram, free.cores)
6f5b84fad3efatagen2026-09-27 03:59 UTC186
}
6f5b84fad3efatagen2026-09-27 03:59 UTC187
6f5b84fad3efatagen2026-09-27 03:59 UTC188
struct Reservation {
6f5b84fad3efatagen2026-09-27 03:59 UTC189
    /// `None` when nothing finishing would free enough, e.g. disk or someone
6f5b84fad3efatagen2026-09-27 03:59 UTC190
    /// else's ram. backfill falls back to the time cap then
6f5b84fad3efatagen2026-09-27 03:59 UTC191
    shadow: Option<Duration>,
6f5b84fad3efatagen2026-09-27 03:59 UTC192
    extra: Option<Free>,
6f5b84fad3efatagen2026-09-27 03:59 UTC193
}
6f5b84fad3efatagen2026-09-27 03:59 UTC194
6f5b84fad3efatagen2026-09-27 03:59 UTC195
fn reserve(
6f5b84fad3efatagen2026-09-27 03:59 UTC196
    job: &Queued,
6f5b84fad3efatagen2026-09-27 03:59 UTC197
    need: &Need,
6f5b84fad3efatagen2026-09-27 03:59 UTC198
    free: &Free,
6f5b84fad3efatagen2026-09-27 03:59 UTC199
    ends: &[(Duration, Bytes, u32, bool)],
6f5b84fad3efatagen2026-09-27 03:59 UTC200
) -> Reservation {
6f5b84fad3efatagen2026-09-27 03:59 UTC201
    let mut sim = free.clone();
6f5b84fad3efatagen2026-09-27 03:59 UTC202
    let mut ends = ends.to_vec();
6f5b84fad3efatagen2026-09-27 03:59 UTC203
    ends.sort_by_key(|e| e.0);
6f5b84fad3efatagen2026-09-27 03:59 UTC204
    for (at, ram, cores, heavy) in ends {
6f5b84fad3efatagen2026-09-27 03:59 UTC205
        sim.give_back(ram, cores, heavy);
6f5b84fad3efatagen2026-09-27 03:59 UTC206
        if sim.shortfall(need, &job.fs, job.heavy).is_none() {
6f5b84fad3efatagen2026-09-27 03:59 UTC207
            sim.take(need, &job.fs, job.heavy);
6f5b84fad3efatagen2026-09-27 03:59 UTC208
            return Reservation {
6f5b84fad3efatagen2026-09-27 03:59 UTC209
                shadow: Some(at),
6f5b84fad3efatagen2026-09-27 03:59 UTC210
                extra: Some(sim),
6f5b84fad3efatagen2026-09-27 03:59 UTC211
            };
6f5b84fad3efatagen2026-09-27 03:59 UTC212
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC213
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC214
    Reservation {
6f5b84fad3efatagen2026-09-27 03:59 UTC215
        shadow: None,
6f5b84fad3efatagen2026-09-27 03:59 UTC216
        extra: None,
6f5b84fad3efatagen2026-09-27 03:59 UTC217
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC218
}
6f5b84fad3efatagen2026-09-27 03:59 UTC219
6f5b84fad3efatagen2026-09-27 03:59 UTC220
pub fn plan(
6f5b84fad3efatagen2026-09-27 03:59 UTC221
    queue: &[Queued],
6f5b84fad3efatagen2026-09-27 03:59 UTC222
    running: &[Running],
6f5b84fad3efatagen2026-09-27 03:59 UTC223
    cap: &Capacity,
6f5b84fad3efatagen2026-09-27 03:59 UTC224
    backfill_max: Duration,
6f5b84fad3efatagen2026-09-27 03:59 UTC225
) -> Vec<(JobId, Verdict)> {
6f5b84fad3efatagen2026-09-27 03:59 UTC226
    let mut free = free_now(running, cap);
6f5b84fad3efatagen2026-09-27 03:59 UTC227
    let mut ends: Vec<_> = running
6f5b84fad3efatagen2026-09-27 03:59 UTC228
        .iter()
6f5b84fad3efatagen2026-09-27 03:59 UTC229
        .map(|r| (r.remaining, r.need.ram.max(r.rss), r.need.cores, r.heavy))
6f5b84fad3efatagen2026-09-27 03:59 UTC230
        .collect();
6f5b84fad3efatagen2026-09-27 03:59 UTC231
    let mut reservation: Option<Reservation> = None;
6f5b84fad3efatagen2026-09-27 03:59 UTC232
    let mut out = Vec::with_capacity(queue.len());
6f5b84fad3efatagen2026-09-27 03:59 UTC233
6f5b84fad3efatagen2026-09-27 03:59 UTC234
    for job in queue {
6f5b84fad3efatagen2026-09-27 03:59 UTC235
        let need = clamp(job.need, cap);
6f5b84fad3efatagen2026-09-27 03:59 UTC236
6f5b84fad3efatagen2026-09-27 03:59 UTC237
        if job.heavy && cap.pressure {
6f5b84fad3efatagen2026-09-27 03:59 UTC238
            out.push((job.id, Verdict::Wait(Reason::Pressure)));
6f5b84fad3efatagen2026-09-27 03:59 UTC239
            continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC240
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC241
6f5b84fad3efatagen2026-09-27 03:59 UTC242
        if let Some(why) = free.shortfall(&need, &job.fs, job.heavy) {
6f5b84fad3efatagen2026-09-27 03:59 UTC243
            if reservation.is_none() {
6f5b84fad3efatagen2026-09-27 03:59 UTC244
                reservation = Some(reserve(job, &need, &free, &ends));
6f5b84fad3efatagen2026-09-27 03:59 UTC245
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC246
            out.push((job.id, Verdict::Wait(why)));
6f5b84fad3efatagen2026-09-27 03:59 UTC247
            continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC248
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC249
6f5b84fad3efatagen2026-09-27 03:59 UTC250
        if let Some(res) = &mut reservation {
6f5b84fad3efatagen2026-09-27 03:59 UTC251
            let finishes_first = match res.shadow {
6f5b84fad3efatagen2026-09-27 03:59 UTC252
                Some(shadow) => need.time <= shadow,
6f5b84fad3efatagen2026-09-27 03:59 UTC253
                None => need.time <= backfill_max,
6f5b84fad3efatagen2026-09-27 03:59 UTC254
            };
6f5b84fad3efatagen2026-09-27 03:59 UTC255
            let fits_extra = res
6f5b84fad3efatagen2026-09-27 03:59 UTC256
                .extra
6f5b84fad3efatagen2026-09-27 03:59 UTC257
                .as_ref()
6f5b84fad3efatagen2026-09-27 03:59 UTC258
                .is_some_and(|x| x.shortfall(&need, &job.fs, job.heavy).is_none());
6f5b84fad3efatagen2026-09-27 03:59 UTC259
            if !finishes_first && !fits_extra {
6f5b84fad3efatagen2026-09-27 03:59 UTC260
                out.push((job.id, Verdict::Wait(Reason::Reserved)));
6f5b84fad3efatagen2026-09-27 03:59 UTC261
                continue;
6f5b84fad3efatagen2026-09-27 03:59 UTC262
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC263
            if !finishes_first && let Some(extra) = &mut res.extra {
6f5b84fad3efatagen2026-09-27 03:59 UTC264
                extra.take(&need, &job.fs, job.heavy);
6f5b84fad3efatagen2026-09-27 03:59 UTC265
            }
6f5b84fad3efatagen2026-09-27 03:59 UTC266
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC267
6f5b84fad3efatagen2026-09-27 03:59 UTC268
        free.take(&need, &job.fs, job.heavy);
6f5b84fad3efatagen2026-09-27 03:59 UTC269
        ends.push((need.time, need.ram, need.cores, job.heavy));
6f5b84fad3efatagen2026-09-27 03:59 UTC270
        out.push((job.id, Verdict::Admit));
6f5b84fad3efatagen2026-09-27 03:59 UTC271
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC272
    out
6f5b84fad3efatagen2026-09-27 03:59 UTC273
}
6f5b84fad3efatagen2026-09-27 03:59 UTC274
6f5b84fad3efatagen2026-09-27 03:59 UTC275
#[cfg(test)]
6f5b84fad3efatagen2026-09-27 03:59 UTC276
mod tests {
6f5b84fad3efatagen2026-09-27 03:59 UTC277
    use super::*;
6f5b84fad3efatagen2026-09-27 03:59 UTC278
6f5b84fad3efatagen2026-09-27 03:59 UTC279
    const FS: FsId = 1;
6f5b84fad3efatagen2026-09-27 03:59 UTC280
6f5b84fad3efatagen2026-09-27 03:59 UTC281
    fn need(ram_gib: u64, cores: u32, disk_gib: u64, mins: u64) -> Need {
6f5b84fad3efatagen2026-09-27 03:59 UTC282
        Need {
6f5b84fad3efatagen2026-09-27 03:59 UTC283
            ram: Bytes::gib(ram_gib),
6f5b84fad3efatagen2026-09-27 03:59 UTC284
            disk: Bytes::gib(disk_gib),
6f5b84fad3efatagen2026-09-27 03:59 UTC285
            cores,
6f5b84fad3efatagen2026-09-27 03:59 UTC286
            time: Duration::from_secs(mins * 60),
6f5b84fad3efatagen2026-09-27 03:59 UTC287
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC288
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC289
6f5b84fad3efatagen2026-09-27 03:59 UTC290
    fn q(id: JobId, need: Need, heavy: bool) -> Queued {
6f5b84fad3efatagen2026-09-27 03:59 UTC291
        Queued {
6f5b84fad3efatagen2026-09-27 03:59 UTC292
            id,
6f5b84fad3efatagen2026-09-27 03:59 UTC293
            need,
6f5b84fad3efatagen2026-09-27 03:59 UTC294
            heavy,
6f5b84fad3efatagen2026-09-27 03:59 UTC295
            fs: vec![FS],
6f5b84fad3efatagen2026-09-27 03:59 UTC296
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC297
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC298
6f5b84fad3efatagen2026-09-27 03:59 UTC299
    fn cap() -> Capacity {
6f5b84fad3efatagen2026-09-27 03:59 UTC300
        Capacity {
6f5b84fad3efatagen2026-09-27 03:59 UTC301
            ram_budget: Bytes::gib(24),
6f5b84fad3efatagen2026-09-27 03:59 UTC302
            ram_live: Bytes::gib(28),
6f5b84fad3efatagen2026-09-27 03:59 UTC303
            ram_reserve: Bytes::gib(4),
6f5b84fad3efatagen2026-09-27 03:59 UTC304
            cores: 16,
6f5b84fad3efatagen2026-09-27 03:59 UTC305
            disks: BTreeMap::from([(
6f5b84fad3efatagen2026-09-27 03:59 UTC306
                FS,
6f5b84fad3efatagen2026-09-27 03:59 UTC307
                Disk {
6f5b84fad3efatagen2026-09-27 03:59 UTC308
                    free: Bytes::gib(100),
6f5b84fad3efatagen2026-09-27 03:59 UTC309
                    reserve: Bytes::gib(20),
6f5b84fad3efatagen2026-09-27 03:59 UTC310
                    consumed: Bytes::ZERO,
6f5b84fad3efatagen2026-09-27 03:59 UTC311
                },
6f5b84fad3efatagen2026-09-27 03:59 UTC312
            )]),
6f5b84fad3efatagen2026-09-27 03:59 UTC313
            pressure: false,
6f5b84fad3efatagen2026-09-27 03:59 UTC314
        }
6f5b84fad3efatagen2026-09-27 03:59 UTC315
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC316
6f5b84fad3efatagen2026-09-27 03:59 UTC317
    fn verdicts(plan: &[(JobId, Verdict)]) -> Vec<Verdict> {
6f5b84fad3efatagen2026-09-27 03:59 UTC318
        plan.iter().map(|(_, v)| *v).collect()
6f5b84fad3efatagen2026-09-27 03:59 UTC319
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC320
6f5b84fad3efatagen2026-09-27 03:59 UTC321
    const BF: Duration = Duration::from_secs(120);
6f5b84fad3efatagen2026-09-27 03:59 UTC322
6f5b84fad3efatagen2026-09-27 03:59 UTC323
    #[test]
6f5b84fad3efatagen2026-09-27 03:59 UTC324
    fn two_heavies_never_collide() {
6f5b84fad3efatagen2026-09-27 03:59 UTC325
        let out = plan(
6f5b84fad3efatagen2026-09-27 03:59 UTC326
            &[q(1, need(8, 8, 5, 10), true), q(2, need(8, 4, 5, 10), true)],
6f5b84fad3efatagen2026-09-27 03:59 UTC327
            &[],
6f5b84fad3efatagen2026-09-27 03:59 UTC328
            &cap(),
6f5b84fad3efatagen2026-09-27 03:59 UTC329
            BF,
6f5b84fad3efatagen2026-09-27 03:59 UTC330
        );
6f5b84fad3efatagen2026-09-27 03:59 UTC331
        assert_eq!(
6f5b84fad3efatagen2026-09-27 03:59 UTC332
            verdicts(&out),
6f5b84fad3efatagen2026-09-27 03:59 UTC333
            [Verdict::Admit, Verdict::Wait(Reason::HeavyRunning)]
6f5b84fad3efatagen2026-09-27 03:59 UTC334
        );
6f5b84fad3efatagen2026-09-27 03:59 UTC335
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC336
6f5b84fad3efatagen2026-09-27 03:59 UTC337
    #[test]
6f5b84fad3efatagen2026-09-27 03:59 UTC338
    fn disk_never_double_booked() {
6f5b84fad3efatagen2026-09-27 03:59 UTC339
        let out = plan(
6f5b84fad3efatagen2026-09-27 03:59 UTC340
            &[
6f5b84fad3efatagen2026-09-27 03:59 UTC341
                q(1, need(1, 1, 50, 10), false),
6f5b84fad3efatagen2026-09-27 03:59 UTC342
                q(2, need(1, 1, 50, 10), false),
6f5b84fad3efatagen2026-09-27 03:59 UTC343
            ],
6f5b84fad3efatagen2026-09-27 03:59 UTC344
            &[],
6f5b84fad3efatagen2026-09-27 03:59 UTC345
            &cap(),
6f5b84fad3efatagen2026-09-27 03:59 UTC346
            BF,
6f5b84fad3efatagen2026-09-27 03:59 UTC347
        );
6f5b84fad3efatagen2026-09-27 03:59 UTC348
        assert_eq!(
6f5b84fad3efatagen2026-09-27 03:59 UTC349
            verdicts(&out),
6f5b84fad3efatagen2026-09-27 03:59 UTC350
            [Verdict::Admit, Verdict::Wait(Reason::Disk(FS))]
6f5b84fad3efatagen2026-09-27 03:59 UTC351
        );
6f5b84fad3efatagen2026-09-27 03:59 UTC352
    }
6f5b84fad3efatagen2026-09-27 03:59 UTC353
}