Skip to content

Commit b3af9c1

Browse files
committed
Create Job trait and centralize scheduled jobs
1 parent 60b14eb commit b3af9c1

File tree

6 files changed

+150
-73
lines changed

6 files changed

+150
-73
lines changed

src/db.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::handlers::jobs::handle_job;
2-
use crate::{db::jobs::*, handlers::Context};
1+
use crate::{db::jobs::*, handlers::Context, jobs::jobs};
32
use anyhow::Context as _;
43
use chrono::Utc;
54
use native_tls::{Certificate, TlsConnector};
@@ -188,9 +187,9 @@ pub async fn schedule_jobs(db: &DbClient, jobs: Vec<JobSchedule>) -> anyhow::Res
188187
let mut upcoming = job.schedule.upcoming(Utc).take(1);
189188

190189
if let Some(scheduled_at) = upcoming.next() {
191-
if let Err(_) = get_job_by_name_and_scheduled_at(&db, &job.name, &scheduled_at).await {
190+
if let Err(_) = get_job_by_name_and_scheduled_at(&db, job.name, &scheduled_at).await {
192191
// mean there's no job already in the db with that name and scheduled_at
193-
insert_job(&db, &job.name, &scheduled_at, &job.metadata).await?;
192+
insert_job(&db, job.name, &scheduled_at, &job.metadata).await?;
194193
}
195194
}
196195
}
@@ -220,6 +219,26 @@ pub async fn run_scheduled_jobs(ctx: &Context, db: &DbClient) -> anyhow::Result<
220219
Ok(())
221220
}
222221

222+
// Try to handle a specific job
223+
async fn handle_job(
224+
ctx: &Context,
225+
name: &String,
226+
metadata: &serde_json::Value,
227+
) -> anyhow::Result<()> {
228+
for job in jobs() {
229+
if &job.name() == &name {
230+
return job.run(ctx, metadata).await;
231+
}
232+
}
233+
tracing::trace!(
234+
"handle_job fell into default case: (name={:?}, metadata={:?})",
235+
name,
236+
metadata
237+
);
238+
239+
Ok(())
240+
}
241+
223242
static MIGRATIONS: &[&str] = &[
224243
"
225244
CREATE TABLE notifications (

src/handlers.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ mod close;
2929
pub mod docs_update;
3030
mod github_releases;
3131
mod glacier;
32-
pub mod jobs;
3332
mod major_change;
3433
mod mentions;
3534
mod milestone_prs;

src/handlers/docs_update.rs

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
//! A scheduled job to post a PR to update the documentation on rust-lang/rust.
22
3-
use crate::db::jobs::JobSchedule;
43
use crate::github::{self, GitTreeEntry, GithubClient, Issue, Repository};
4+
use crate::jobs::Job;
55
use anyhow::Context;
66
use anyhow::Result;
7-
use cron::Schedule;
7+
use async_trait::async_trait;
88
use reqwest::Client;
99
use std::fmt::Write;
10-
use std::str::FromStr;
1110

1211
/// This is the repository where the commits will be created.
1312
const WORK_REPO: &str = "rustbot/rust";
@@ -28,38 +27,42 @@ const SUBMODULES: &[&str] = &[
2827

2928
const TITLE: &str = "Update books";
3029

31-
pub fn job() -> JobSchedule {
32-
JobSchedule {
33-
name: "docs_update",
34-
// Around 9am Pacific time on every Monday.
35-
schedule: Schedule::from_str("0 00 17 * * Mon *").unwrap(),
36-
metadata: serde_json::Value::Null,
37-
}
38-
}
30+
pub struct DocsUpdateJob;
3931

40-
pub async fn handle_job() -> Result<()> {
41-
// Only run every other week. Doing it every week can be a bit noisy, and
42-
// (rarely) a PR can take longer than a week to merge (like if there are
43-
// CI issues). `Schedule` does not allow expressing this, so check it
44-
// manually.
45-
//
46-
// This is set to run the first week after a release, and the week just
47-
// before a release. That allows getting the latest changes in the next
48-
// release, accounting for possibly taking a few days for the PR to land.
49-
let today = chrono::Utc::today().naive_utc();
50-
let base = chrono::naive::NaiveDate::from_ymd(2015, 12, 10);
51-
let duration = today.signed_duration_since(base);
52-
let weeks = duration.num_weeks();
53-
if weeks % 2 != 0 {
54-
tracing::trace!("skipping job, this is an odd week");
55-
return Ok(());
32+
#[async_trait]
33+
impl Job for DocsUpdateJob {
34+
fn name(&self) -> &'static str {
35+
"docs_update"
5636
}
5737

58-
tracing::trace!("starting docs-update");
59-
docs_update()
60-
.await
61-
.context("failed to process docs update")?;
62-
Ok(())
38+
async fn run(
39+
&self,
40+
_ctx: &super::Context,
41+
_metadata: &serde_json::Value,
42+
) -> anyhow::Result<()> {
43+
// Only run every other week. Doing it every week can be a bit noisy, and
44+
// (rarely) a PR can take longer than a week to merge (like if there are
45+
// CI issues). `Schedule` does not allow expressing this, so check it
46+
// manually.
47+
//
48+
// This is set to run the first week after a release, and the week just
49+
// before a release. That allows getting the latest changes in the next
50+
// release, accounting for possibly taking a few days for the PR to land.
51+
let today = chrono::Utc::today().naive_utc();
52+
let base = chrono::naive::NaiveDate::from_ymd(2015, 12, 10);
53+
let duration = today.signed_duration_since(base);
54+
let weeks = duration.num_weeks();
55+
if weeks % 2 != 0 {
56+
tracing::trace!("skipping job, this is an odd week");
57+
return Ok(());
58+
}
59+
60+
tracing::trace!("starting docs-update");
61+
docs_update()
62+
.await
63+
.context("failed to process docs update")?;
64+
Ok(())
65+
}
6366
}
6467

6568
pub async fn docs_update() -> Result<Option<Issue>> {

src/handlers/rustc_commits.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
use crate::db::jobs::JobSchedule;
21
use crate::db::rustc_commits;
32
use crate::db::rustc_commits::get_missing_commits;
3+
use crate::jobs::Job;
44
use crate::{
55
github::{self, Event},
66
handlers::Context,
77
};
8-
use cron::Schedule;
8+
use async_trait::async_trait;
99
use std::collections::VecDeque;
1010
use std::convert::TryInto;
11-
use std::str::FromStr;
1211
use tracing as log;
1312

1413
const BORS_GH_ID: i64 = 3372342;
@@ -153,12 +152,17 @@ pub async fn synchronize_commits_inner(ctx: &Context, starter: Option<(String, u
153152
}
154153
}
155154

156-
pub fn job() -> JobSchedule {
157-
JobSchedule {
158-
name: "rustc_commits",
159-
// Every 30 minutes...
160-
schedule: Schedule::from_str("* 0,30 * * * * *").unwrap(),
161-
metadata: serde_json::Value::Null,
155+
pub struct RustcCommitsJob;
156+
157+
#[async_trait]
158+
impl Job for RustcCommitsJob {
159+
fn name(&self) -> &'static str {
160+
"rustc_commits"
161+
}
162+
163+
async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
164+
synchronize_commits_inner(ctx, None).await;
165+
Ok(())
162166
}
163167
}
164168

src/jobs.rs

Lines changed: 75 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
//! SCHEDULED JOBS
1+
//! # Scheduled Jobs
2+
//!
3+
//! Scheduled jobs essentially come in two flavors: automatically repeating
4+
//! (cron) jobs and one-off jobs.
5+
//!
6+
//! The core trait here is the `Job` trait, which *must* define the name of the
7+
//! job (to be used as an identifier in the database) and the function to run
8+
//! when the job runs.
29
//!
310
//! The metadata is a serde_json::Value
411
//! Please refer to https://docs.rs/serde_json/latest/serde_json/value/fn.from_value.html
@@ -7,33 +14,42 @@
714
//! The schedule is a cron::Schedule
815
//! Please refer to https://docs.rs/cron/latest/cron/struct.Schedule.html for further info
916
//!
10-
//! For example, if we want to sends a Zulip message every Friday at 11:30am ET into #t-release
11-
//! with a @T-release meeting! content, we should create some JobSchedule like:
17+
//! ## Example, sending a zulip message once a week
1218
//!
19+
//! To give an example, let's imagine we want to sends a Zulip message every
20+
//! Friday at 11:30am ET into #t-release with a "@T-release meeting!"" content.
21+
//!
22+
//! To begin, let's create a generic zulip message Job:
1323
//! #[derive(Serialize, Deserialize)]
1424
//! struct ZulipMetadata {
1525
//! pub message: String
26+
//! pub channel: String,
1627
//! }
28+
//! struct ZulipMessageJob;
29+
//! impl Job for ZulipMessageJob { ... }
1730
//!
18-
//! let metadata = serde_json::value::to_value(ZulipMetadata {
19-
//! message: "@T-release meeting!".to_string()
20-
//! }).unwrap();
21-
//!
22-
//! let schedule = Schedule::from_str("0 30 11 * * FRI *").unwrap();
23-
//!
24-
//! let new_job = JobSchedule {
25-
//! name: "send_zulip_message".to_owned(),
26-
//! schedule: schedule,
27-
//! metadata: metadata
28-
//! }
29-
//!
30-
//! and include it in the below vector in jobs():
31+
//! (Imagine that this job requires a channel and a message in the metadata.)
3132
//!
32-
//! jobs.push(new_job);
33-
//!
34-
//! ... fianlly, add the corresponding "send_zulip_message" handler in src/handlers/jobs.rs
33+
//! If we wanted to have a default scheduled message, we could add the following to
34+
//! `default_jobs`:
35+
//! JobSchedule {
36+
//! name: ZulipMessageJob.name(),
37+
//! schedule: Schedule::from_str("0 30 11 * * FRI *").unwrap(),
38+
//! metadata: serde_json::value::to_value(ZulipMetadata {
39+
//! message: "@T-release meeting!".to_string()
40+
//! channel: "T-release".to_string(),
41+
//! }).unwrap(),
42+
//! }
43+
44+
use std::str::FromStr;
3545

36-
use crate::db::jobs::JobSchedule;
46+
use async_trait::async_trait;
47+
use cron::Schedule;
48+
49+
use crate::{
50+
db::jobs::JobSchedule,
51+
handlers::{docs_update::DocsUpdateJob, rustc_commits::RustcCommitsJob, Context},
52+
};
3753

3854
// How often new cron-based jobs will be placed in the queue.
3955
// This is the minimum period *between* a single cron task's executions.
@@ -43,16 +59,50 @@ pub const JOB_SCHEDULING_CADENCE_IN_SECS: u64 = 1800;
4359
// This is the granularity at which events will occur.
4460
pub const JOB_PROCESSING_CADENCE_IN_SECS: u64 = 60;
4561

46-
pub fn jobs() -> Vec<JobSchedule> {
47-
// Add to this vector any new cron task you want (as explained above)
62+
// The default jobs to schedule, repeatedly.
63+
pub fn jobs() -> Vec<Box<dyn Job + Send + Sync>> {
64+
vec![Box::new(DocsUpdateJob), Box::new(RustcCommitsJob)]
65+
}
66+
67+
pub fn default_jobs() -> Vec<JobSchedule> {
4868
vec![
49-
crate::handlers::docs_update::job(),
50-
crate::handlers::rustc_commits::job(),
69+
JobSchedule {
70+
name: DocsUpdateJob.name(),
71+
// Around 9am Pacific time on every Monday.
72+
schedule: Schedule::from_str("0 00 17 * * Mon *").unwrap(),
73+
metadata: serde_json::Value::Null,
74+
},
75+
JobSchedule {
76+
name: RustcCommitsJob.name(),
77+
// Every 30 minutes...
78+
schedule: Schedule::from_str("* 0,30 * * * * *").unwrap(),
79+
metadata: serde_json::Value::Null,
80+
},
5181
]
5282
}
5383

84+
#[async_trait]
85+
pub trait Job {
86+
fn name(&self) -> &str;
87+
88+
async fn run(&self, ctx: &Context, metadata: &serde_json::Value) -> anyhow::Result<()>;
89+
}
90+
5491
#[test]
5592
fn jobs_defined() {
93+
// This checks that we don't panic (during schedule parsing) and that all names are unique
5694
// Checks we don't panic here, mostly for the schedule parsing.
57-
drop(jobs());
95+
let all_jobs = jobs();
96+
let mut all_job_names: Vec<_> = all_jobs.into_iter().map(|j| j.name().to_string()).collect();
97+
all_job_names.sort();
98+
let mut unique_all_job_names = all_job_names.clone();
99+
unique_all_job_names.sort();
100+
unique_all_job_names.dedup();
101+
assert_eq!(all_job_names, unique_all_job_names);
102+
103+
// Also ensure that our defalt jobs are release jobs
104+
let default_jobs = default_jobs();
105+
default_jobs
106+
.iter()
107+
.for_each(|j| assert!(all_job_names.contains(&j.name.to_string())));
58108
}

src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use tokio::{task, time};
1111
use tower::{Service, ServiceExt};
1212
use tracing as log;
1313
use tracing::Instrument;
14-
use triagebot::jobs::{jobs, JOB_PROCESSING_CADENCE_IN_SECS, JOB_SCHEDULING_CADENCE_IN_SECS};
14+
use triagebot::jobs::{
15+
default_jobs, JOB_PROCESSING_CADENCE_IN_SECS, JOB_SCHEDULING_CADENCE_IN_SECS,
16+
};
1517
use triagebot::{db, github, handlers::Context, notification_listing, payload, EventName};
1618

1719
async fn handle_agenda_request(req: String) -> anyhow::Result<String> {
@@ -320,7 +322,7 @@ fn spawn_job_scheduler() {
320322

321323
loop {
322324
interval.tick().await;
323-
db::schedule_jobs(&*pool.get().await, jobs())
325+
db::schedule_jobs(&*pool.get().await, default_jobs())
324326
.await
325327
.context("database schedule jobs")
326328
.unwrap();

0 commit comments

Comments
 (0)