OXYGENHUB

Coverage / crates/hub/src/queue.rs 40.0%

36 of 90 lines run, 8 of 36 functions · run #515 on main · run missed

1
//! The worker side of the durable work queue (oxygenhub#276,
2
//! docs/roles.md): every worker process takes due items, a batch at a
3
//! time, and does them. Work that fails where it starts (a notification's
4
//! email, the Git mirror after a push) is queued here instead of being
5
//! lost, and retried with backoff.
6
7
use crate::db::{self, queue::Item};
8
use crate::Shared;
9
use std::time::Duration;
10
11
/// How often an idle worker looks for due items.
12
pub const POLL: Duration = Duration::from_secs(2);
13
/// How long a claim lasts before another worker may take the item.
14
const LEASE_MS: i64 = 5 * 60 * 1000;
15
/// Items taken at once.
16
const BATCH: i64 = 10;
17
18
/// An email to send again (`mail`).
190×
pub async fn queue_mail(
200×
    app: &Shared,
210×
    to_name: &str,
220×
    to_addr: &str,
230×
    subject: &str,
240×
    body: &str,
250×
    action: Option<(&str, &str)>,
260×
    unsubscribe: Option<&str>,
270×
) {
280×
    let payload = serde_json::json!({
290×
        "to_name": to_name, "to_addr": to_addr, "subject": subject, "body": body,
300×
        "action_label": action.map(|a| a.0), "action_url": action.map(|a| a.1), "unsubscribe": unsubscribe,
31
    });
320×
    if let Err(e) = db::queue::enqueue(&app.db, "mail", &payload, "", 30_000).await {
330×
        tracing::warn!("queueing an email to {to_addr}: {e}");
340×
    }
350×
}
36
37
/// A repository's Git mirror to refresh again (`mirror.refresh`), once.
380×
pub async fn queue_mirror(app: &Shared, repo_id: i64) {
390×
    let payload = serde_json::json!({ "repo_id": repo_id });
400×
    if let Err(e) = db::queue::enqueue(&app.db, "mirror.refresh", &payload, &repo_id.to_string(), 30_000).await {
410×
        tracing::warn!("queueing a Git mirror refresh: {e}");
420×
    }
430×
}
44
45
/// This process's name as a claimant: its host and pid.
46197×
fn worker_name() -> String {
47197×
    let host = std::fs::read_to_string("/etc/hostname").map(|h| h.trim().to_string()).unwrap_or_default();
48197×
    format!("{host}:{}", std::process::id())
49197×
}
50
51
/// Take and do due items until the process stops.
52197×
pub async fn worker(app: Shared) {
53197×
    let me = worker_name();
54
    loop {
55289×
        let items = match db::queue::claim(&app.db, &me, BATCH, LEASE_MS).await {
56289×
            Ok(items) => items,
570×
            Err(e) => {
580×
                tracing::warn!("work queue: claiming: {e}");
590×
                Vec::new()
60
            }
61
        };
62289×
        let idle = items.is_empty();
63289×
        for item in items {
640×
            run(&app, item).await;
65
        }
66289×
        if idle {
67289×
            tokio::time::sleep(POLL).await;
680×
        }
69
    }
70
}
71
72
/// Do one item and record how it went.
732×
pub async fn run(app: &Shared, item: Item) {
742×
    let outcome = match item.kind.as_str() {
752×
        "mail" => mail(app, &item).await,
762×
        "mirror.refresh" => mirror(app, &item).await,
771×
        other => Err(format!("no worker knows {other:?}")),
78
    };
792×
    let recorded = match outcome {
801×
        Ok(()) => db::queue::done(&app.db, item.id).await,
811×
        Err(e) => {
821×
            tracing::warn!(kind = %item.kind, attempt = item.attempts, "work queue: {e}");
83
            // An item nobody can do is dead at once.
841×
            let item = if item.kind != "mail" && item.kind != "mirror.refresh" {
851×
                Item { attempts: db::queue::MAX_ATTEMPTS, ..item }
86
            } else {
870×
                item
88
            };
891×
            db::queue::failed(&app.db, &item, &e).await
90
        }
91
    };
922×
    if let Err(e) = recorded {
930×
        tracing::warn!("work queue: recording an outcome: {e}");
942×
    }
952×
}
96
970×
async fn mail(app: &Shared, item: &Item) -> Result<(), String> {
980×
    let p: serde_json::Value = serde_json::from_str(&item.payload).map_err(|e| e.to_string())?;
990×
    let s = |k: &str| p[k].as_str().unwrap_or_default().to_string();
1000×
    let (label, url) = (s("action_label"), s("action_url"));
1010×
    let action = (!url.is_empty()).then_some(crate::mail::Action { label: &label, url: &url });
1020×
    let unsubscribe = s("unsubscribe");
1030×
    crate::mail::send_letter(
1040×
        app,
1050×
        &s("to_name"),
1060×
        &s("to_addr"),
1070×
        &s("subject"),
1080×
        &s("body"),
1090×
        action,
1100×
        (!unsubscribe.is_empty()).then_some(unsubscribe.as_str()),
1110×
    )
1120×
    .await
1130×
}
114
1151×
async fn mirror(app: &Shared, item: &Item) -> Result<(), String> {
1161×
    let p: serde_json::Value = serde_json::from_str(&item.payload).map_err(|e| e.to_string())?;
1171×
    let id = p["repo_id"].as_i64().ok_or("no repo_id")?;
118
    // Gone, or its mirror turned off meanwhile: nothing to do.
1191×
    let Some(row) = db::repos::get_by_id(&app.db, id).await.map_err(|e| e.to_string())? else { return Ok(()) };
1200×
    crate::mirror::refresh(app, &row).await
1211×
}