OXYGENHUB

Coverage / crates/hub/src/db/runner_updates.rs 93.1%

135 of 145 lines run, 36 of 76 functions · run #525 on main · run missed

1
//! Runner updates (oxygenhub#322): the policy at each level and the test
2
//! groups runners are put in.
3
4
use super::{now_ms, Pool};
5
use sqlx::FromRow;
6
7
/// One level's settings; `None` inherits from the next level out.
8
#[derive(Clone, Debug, Default, PartialEq, Eq, FromRow)]
9
pub struct Policy {
10
    pub scope: String,
11
    pub key: String,
12
    pub updates: Option<bool>,
13
    pub channel: Option<String>,
14
    pub window_from: Option<i64>,
15
    pub window_to: Option<i64>,
16
    pub dry_run: Option<bool>,
17
}
18
19
impl Policy {
20
    /// Nothing set: the level inherits everything.
2110×
    pub fn is_empty(&self) -> bool {
2210×
        self.updates.is_none() && self.channel.is_none() && self.window_from.is_none() && self.dry_run.is_none()
2310×
    }
24
}
25
26
#[derive(Clone, Debug, PartialEq, Eq, FromRow)]
27
pub struct Group {
28
    pub id: i64,
29
    /// Empty: a group of the site's runners.
30
    pub owner: String,
31
    pub name: String,
32
    /// A share of the runners in reach, by a stable hash of their ids;
33
    /// `None`: only the chosen ones.
34
    pub percent: Option<i64>,
35
    pub created_ms: i64,
36
    /// The wave its runners take a version in (oxygenflow#84): wave 1
37
    /// first, each later one once those before it have proved the version;
38
    /// `None`: after every wave.
39
    pub wave: Option<i64>,
40
}
41
4280×
pub async fn policies(pool: &Pool) -> sqlx::Result<Vec<Policy>> {
4380×
    sqlx::query_as("SELECT * FROM runner_update_policies").fetch_all(pool).await
4480×
}
45
460×
pub async fn policy(pool: &Pool, scope: &str, key: &str) -> sqlx::Result<Policy> {
470×
    let p: Option<Policy> = sqlx::query_as("SELECT * FROM runner_update_policies WHERE scope = $1 AND key = $2")
480×
        .bind(scope)
490×
        .bind(key)
500×
        .fetch_optional(pool)
510×
        .await?;
520×
    Ok(p.unwrap_or(Policy { scope: scope.into(), key: key.into(), ..Policy::default() }))
530×
}
54
55
/// Set a level's settings; one with nothing set is removed.
5610×
pub async fn set_policy(pool: &Pool, p: &Policy) -> sqlx::Result<()> {
5710×
    sqlx::query("DELETE FROM runner_update_policies WHERE scope = $1 AND key = $2").bind(&p.scope).bind(&p.key).execute(pool).await?;
5810×
    if p.is_empty() {
590×
        return Ok(());
6010×
    }
6110×
    sqlx::query(
6210×
        "INSERT INTO runner_update_policies (scope, key, updates, channel, window_from, window_to, dry_run)
6310×
         VALUES ($1, $2, $3, $4, $5, $6, $7)",
6410×
    )
6510×
    .bind(&p.scope)
6610×
    .bind(&p.key)
6710×
    .bind(p.updates)
6810×
    .bind(&p.channel)
6910×
    .bind(p.window_from)
7010×
    .bind(p.window_to)
7110×
    .bind(p.dry_run)
7210×
    .execute(pool)
7310×
    .await?;
7410×
    Ok(())
7510×
}
76
7787×
pub async fn groups(pool: &Pool) -> sqlx::Result<Vec<Group>> {
7887×
    sqlx::query_as("SELECT * FROM runner_groups ORDER BY owner, name").fetch_all(pool).await
7987×
}
80
817×
pub async fn group(pool: &Pool, id: i64) -> sqlx::Result<Option<Group>> {
827×
    sqlx::query_as("SELECT * FROM runner_groups WHERE id = $1").bind(id).fetch_optional(pool).await
837×
}
84
855×
pub async fn create_group(pool: &Pool, owner: &str, name: &str, percent: Option<i64>) -> sqlx::Result<Group> {
865×
    sqlx::query_as("INSERT INTO runner_groups (owner, name, percent, created_ms) VALUES ($1, $2, $3, $4) RETURNING *")
875×
        .bind(owner)
885×
        .bind(name)
895×
        .bind(percent)
905×
        .bind(now_ms())
915×
        .fetch_one(pool)
925×
        .await
935×
}
94
951×
pub async fn set_percent(pool: &Pool, id: i64, percent: Option<i64>) -> sqlx::Result<()> {
961×
    sqlx::query("UPDATE runner_groups SET percent = $1 WHERE id = $2").bind(percent).bind(id).execute(pool).await?;
971×
    Ok(())
981×
}
99
1005×
pub async fn set_wave(pool: &Pool, id: i64, wave: Option<i64>) -> sqlx::Result<()> {
1015×
    sqlx::query("UPDATE runner_groups SET wave = $1 WHERE id = $2").bind(wave).bind(id).execute(pool).await?;
1025×
    Ok(())
1035×
}
104
105
/// Remove a group, its members and its policy.
1061×
pub async fn delete_group(pool: &Pool, id: i64) -> sqlx::Result<()> {
1071×
    sqlx::query("DELETE FROM runner_group_members WHERE group_id = $1").bind(id).execute(pool).await?;
1081×
    sqlx::query("DELETE FROM runner_update_policies WHERE scope = 'group' AND key = $1").bind(id.to_string()).execute(pool).await?;
1091×
    sqlx::query("DELETE FROM runner_groups WHERE id = $1").bind(id).execute(pool).await?;
1101×
    Ok(())
1111×
}
112
113
/// Every chosen member, as (group, runner).
11480×
pub async fn members(pool: &Pool) -> sqlx::Result<Vec<(i64, i64)>> {
11580×
    sqlx::query_as("SELECT group_id, runner_id FROM runner_group_members").fetch_all(pool).await
11680×
}
117
118
/// Make `runners` the group's chosen members.
1195×
pub async fn set_members(pool: &Pool, group: i64, runners: &[i64]) -> sqlx::Result<()> {
1205×
    sqlx::query("DELETE FROM runner_group_members WHERE group_id = $1").bind(group).execute(pool).await?;
1215×
    for r in runners {
1224×
        sqlx::query("INSERT INTO runner_group_members (group_id, runner_id) VALUES ($1, $2)").bind(group).bind(r).execute(pool).await?;
123
    }
1245×
    Ok(())
1255×
}
126
127
/// What a runner said about its update.
1282×
pub async fn report(pool: &Pool, runner: i64, note: &str) -> sqlx::Result<()> {
1292×
    sqlx::query("UPDATE flow_runners SET update_note = $1, update_ms = $2 WHERE id = $3")
1302×
        .bind(note)
1312×
        .bind(now_ms())
1322×
        .bind(runner)
1332×
        .execute(pool)
1342×
        .await?;
1352×
    Ok(())
1362×
}
137
138
/// A build held back from every runner (#326).
139
#[derive(Clone, Debug, PartialEq, Eq, FromRow)]
140
pub struct Hold {
141
    pub version: String,
142
    /// The runner it rolled back on.
143
    pub runner: String,
144
    pub why: String,
145
    pub at_ms: i64,
146
}
147
148
/// Hold `version` back, the first time it rolls back.
1492×
pub async fn hold(pool: &Pool, version: &str, runner: &str, why: &str) -> sqlx::Result<()> {
1502×
    sqlx::query("INSERT INTO runner_update_holds (version, runner, why, at_ms) VALUES ($1, $2, $3, $4) ON CONFLICT (version) DO NOTHING")
1512×
        .bind(version)
1522×
        .bind(runner)
1532×
        .bind(why)
1542×
        .bind(now_ms())
1552×
        .execute(pool)
1562×
        .await?;
1572×
    Ok(())
1582×
}
159
16030×
pub async fn holds(pool: &Pool) -> sqlx::Result<Vec<Hold>> {
16130×
    sqlx::query_as("SELECT * FROM runner_update_holds ORDER BY at_ms DESC").fetch_all(pool).await
16230×
}
163
16441×
pub async fn held(pool: &Pool, version: &str) -> sqlx::Result<Option<Hold>> {
16541×
    sqlx::query_as("SELECT * FROM runner_update_holds WHERE version = $1").bind(version).fetch_optional(pool).await
16641×
}
167
168
/// Lift the hold on `version`; whether there was one.
1693×
pub async fn lift(pool: &Pool, version: &str) -> sqlx::Result<bool> {
1703×
    Ok(sqlx::query("DELETE FROM runner_update_holds WHERE version = $1").bind(version).execute(pool).await?.rows_affected() > 0)
1713×
}
172
173
/// How a runner has done on a version (oxygenflow#84): its finished jobs
174
/// on that build, how many ended in `error` (the runner's or the
175
/// infrastructure's fault, not a test's), and when the first started.
176
#[derive(Clone, Debug, Default, PartialEq, Eq, FromRow)]
177
pub struct Soak {
178
    pub runner_id: i64,
179
    pub jobs: i64,
180
    pub errors: i64,
181
    pub first_ms: Option<i64>,
182
}
183
184
/// Every runner's [`Soak`] on `version`.
1859×
pub async fn soaks(pool: &Pool, version: &str) -> sqlx::Result<Vec<Soak>> {
1869×
    sqlx::query_as(
1879×
        "SELECT runner_id, COUNT(*) AS jobs, SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS errors, MIN(started_ms) AS first_ms
1889×
         FROM flow_jobs WHERE runner_version = $1 AND runner_id IS NOT NULL AND status IN ('passed', 'failed', 'error')
1899×
         GROUP BY runner_id",
1909×
    )
1919×
    .bind(version)
1929×
    .fetch_all(pool)
1939×
    .await
1949×
}
195
196
/// The first job on `version` that ended in error on runner `runner`, in
197
/// words: `job build of OWNER/NAME run #N: why`.
1982×
pub async fn first_error(pool: &Pool, version: &str, runner: i64) -> sqlx::Result<Option<String>> {
1992×
    let row: Option<(String, String, String, i64, String)> = sqlx::query_as(
2002×
        "SELECT j.name, p.owner, p.name, r.number, j.error FROM flow_jobs j
2012×
         JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id
2022×
         WHERE j.runner_version = $1 AND j.runner_id = $2 AND j.status = 'error'
2032×
         ORDER BY j.finished_ms LIMIT 1",
2042×
    )
2052×
    .bind(version)
2062×
    .bind(runner)
2072×
    .fetch_optional(pool)
2082×
    .await?;
2092×
    Ok(row.map(|(job, owner, name, n, error)| {
2102×
        let why: String = error.chars().take(120).collect();
2112×
        format!("job {job} of {owner}/{name} run #{n}{}", if why.is_empty() { String::new() } else { format!(": {why}") })
2122×
    }))
2132×
}