OXYGENHUB

OxygenFlow claims hand a runner only jobs whose sandbox fits the room it reports; oxygenflow pin moved to the budgeted runner 76bc5a058351

refs andymccall/oxygenflow#6
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-28 20:33 UTC
Signature
✔ signed by Andy McCall
Hash
76bc5a0583516542e19b76b914ddcb29f9da1078a9639f223dc46455ff3482ea
Depends on
25091e5c3bba 380996db84e5 42fa95540468 fd3db644df22
Depended on by
55673c650986 44a3d2ebaddd 92a797b20912 4d2e44d79b03 efd29a0033e8 b0612e7dce30 c4c87f481d05 74be97962ed8 5e9dbc57bc31 941abdd6bb4c
In bookmarks
api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
In tags
v0.1.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
7 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/hub/src/db/flow.rs +22 −1
…
176 unchanged lines
177 177
    }
178 178
}
179 179
180 +
/// What a runner can take on: free memory (MiB) and vCPUs, `None` for no limit.
181 +
#[derive(Clone, Copy, Debug, Default)]
182 +
pub struct Room {
183 +
    pub memory_mib: Option<u32>,
184 +
    pub cpus: Option<u32>,
185 +
}
186 +
187 +
impl Room {
188 +
    /// Whether a job's sandbox (its stored JSON) fits.
189 +
    pub fn fits(&self, sandbox_json: &str) -> bool {
190 +
        let v: serde_json::Value = serde_json::from_str(sandbox_json).unwrap_or_default();
191 +
        let need = |k: &str| v[k].as_u64().unwrap_or(0);
192 +
        self.memory_mib.is_none_or(|m| need("memory_mib") <= m as u64) && self.cpus.is_none_or(|c| need("cpus") <= c as u64)
193 +
    }
194 +
}
195 +
180 196
pub async fn claim(pool: &Pool, runtimes: &[String], runner: &str, token_hash: &str) -> sqlx::Result<Option<Job>> {
197 +
    claim_within(pool, runtimes, runner, token_hash, Room::default()).await
198 +
}
199 +
200 +
/// The oldest runnable job that fits in `room`.
201 +
pub async fn claim_within(pool: &Pool, runtimes: &[String], runner: &str, token_hash: &str, room: Room) -> sqlx::Result<Option<Job>> {
181 202
    let mut tx = pool.begin().await?;
182 203
    let queued: Vec<Job> = sqlx::query_as(
183 204
        "SELECT j.* FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id WHERE j.status = 'queued' AND r.status IN ('queued', 'running') ORDER BY j.run_id, j.depth, j.id LIMIT 200",
…
1 unchanged lines
185 206
    .fetch_all(&mut *tx)
186 207
    .await?;
187 208
    for j in queued {
188 -
        if !runtimes.iter().any(|r| r == &j.runtime) {
209 +
        if !runtimes.iter().any(|r| r == &j.runtime) || !room.fits(&j.sandbox) {
189 210
            continue;
190 211
        }
191 212
        let deps = j.depends_on();
…
142 unchanged lines
modified crates/hub/src/flow.rs +2 −1
…
128 unchanged lines
129 129
    runner_auth(&app, &headers)?;
130 130
    let runner: String = req.runner.chars().filter(|c| !c.is_control()).take(64).collect();
131 131
    let secret = format!("{JOB_TOKEN_PREFIX}{}", crate::auth::random_hex(32));
132 -
    let Some(job) = fdb::claim(&app.db, &req.runtimes, &runner, &crate::auth::token_hash(&secret)).await? else {
132 +
    let room = fdb::Room { memory_mib: req.free_memory_mib, cpus: req.free_cpus };
133 +
    let Some(job) = fdb::claim_within(&app.db, &req.runtimes, &runner, &crate::auth::token_hash(&secret), room).await? else {
133 134
        return Ok(StatusCode::NO_CONTENT.into_response());
134 135
    };
135 136
    let run = fdb::run_by_id(&app.db, job.run_id).await?.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "no such run".into()))?;
…
206 unchanged lines
replaced deps/oxygenflow +0 −0

No line changes.