Selaa lähdekoodia

Add agenda endpoints to triagebot

This lets users easily fetch up-to-date agendas from triagebot

Currently this endpoint is pretty seriously rate-limited, but we can switch
that to serving a stale copy instead (refreshing in the background) if this is a
problem in practice. I suspect the usage will be rare enough that it won't be a
problem.
Mark Rousskov 2 vuotta sitten
vanhempi
commit
07b58332b3
9 muutettua tiedostoa jossa 263 lisäystä ja 129 poistoa
  1. 43 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 44 35
      src/actions.rs
  4. 80 73
      src/agenda.rs
  5. 6 4
      src/bin/compiler.rs
  6. 8 6
      src/bin/lang.rs
  7. 3 2
      src/bin/prioritization-agenda.rs
  8. 14 5
      src/github.rs
  9. 64 4
      src/main.rs

+ 43 - 0
Cargo.lock

@@ -1247,6 +1247,26 @@ dependencies = [
  "siphasher",
 ]
 
+[[package]]
+name = "pin-project"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.8"
@@ -1911,6 +1931,27 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62"
+
 [[package]]
 name = "tower-service"
 version = "0.3.1"
@@ -1924,6 +1965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3"
 dependencies = [
  "cfg-if",
+ "log",
  "pin-project-lite",
  "tracing-attributes",
  "tracing-core",
@@ -2012,6 +2054,7 @@ dependencies = [
  "tokio",
  "tokio-postgres",
  "toml",
+ "tower",
  "tracing",
  "tracing-subscriber",
  "url",

+ 1 - 0
Cargo.toml

@@ -38,6 +38,7 @@ comrak = "0.8.2"
 route-recognizer = "0.3.0"
 cynic = { version = "0.14" }
 itertools = "0.10.2"
+tower = { version = "0.4.13", features = ["util", "limit", "buffer", "load-shed"] }
 
 [dependencies.serde]
 version = "1"

+ 44 - 35
src/actions.rs

@@ -1,5 +1,6 @@
 use chrono::{DateTime, Utc};
 use std::collections::HashMap;
+use std::sync::Arc;
 
 use async_trait::async_trait;
 use reqwest::Client;
@@ -10,7 +11,7 @@ use crate::github::{self, GithubClient, Repository};
 
 #[async_trait]
 pub trait Action {
-    async fn call(&self) -> String;
+    async fn call(&self) -> anyhow::Result<String>;
 }
 
 pub struct Step<'a> {
@@ -24,6 +25,7 @@ pub struct Query<'a> {
     pub queries: Vec<QueryMap<'a>>,
 }
 
+#[derive(Copy, Clone)]
 pub enum QueryKind {
     List,
     Count,
@@ -32,7 +34,7 @@ pub enum QueryKind {
 pub struct QueryMap<'a> {
     pub name: &'a str,
     pub kind: QueryKind,
-    pub query: Box<dyn github::IssuesQuery + Send + Sync>,
+    pub query: Arc<dyn github::IssuesQuery + Send + Sync>,
 }
 
 #[derive(Debug, serde::Serialize)]
@@ -81,12 +83,16 @@ pub fn to_human(d: DateTime<Utc>) -> String {
 
 #[async_trait]
 impl<'a> Action for Step<'a> {
-    async fn call(&self) -> String {
+    async fn call(&self) -> anyhow::Result<String> {
         let gh = GithubClient::new_with_default_token(Client::new());
 
         let mut context = Context::new();
         let mut results = HashMap::new();
 
+        let mut handles: Vec<tokio::task::JoinHandle<anyhow::Result<(String, QueryKind, Vec<_>)>>> =
+            Vec::new();
+        let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(5));
+
         for Query { repos, queries } in &self.actions {
             for repo in repos {
                 let repository = Repository {
@@ -94,45 +100,48 @@ impl<'a> Action for Step<'a> {
                 };
 
                 for QueryMap { name, kind, query } in queries {
-                    let issues = query.query(&repository, name == &"proposed_fcp", &gh).await;
-
-                    match issues {
-                        Ok(issues_decorator) => match kind {
-                            QueryKind::List => {
-                                results
-                                    .entry(*name)
-                                    .or_insert(Vec::new())
-                                    .extend(issues_decorator);
-                            }
-                            QueryKind::Count => {
-                                let count = issues_decorator.len();
-                                let result = if let Some(value) = context.get(*name) {
-                                    value.as_u64().unwrap() + count as u64
-                                } else {
-                                    count as u64
-                                };
-
-                                context.insert(*name, &result);
-                            }
-                        },
-                        Err(err) => {
-                            eprintln!("ERROR: {}", err);
-                            err.chain()
-                                .skip(1)
-                                .for_each(|cause| eprintln!("because: {}", cause));
-                            std::process::exit(1);
-                        }
-                    }
+                    let semaphore = semaphore.clone();
+                    let name = String::from(*name);
+                    let kind = *kind;
+                    let repository = repository.clone();
+                    let gh = gh.clone();
+                    let query = query.clone();
+                    handles.push(tokio::task::spawn(async move {
+                        let _permit = semaphore.acquire().await?;
+                        let issues = query
+                            .query(&repository, name == "proposed_fcp", &gh)
+                            .await?;
+                        Ok((name, kind, issues))
+                    }));
+                }
+            }
+        }
+
+        for handle in handles {
+            let (name, kind, issues) = handle.await.unwrap()?;
+            match kind {
+                QueryKind::List => {
+                    results.entry(name).or_insert(Vec::new()).extend(issues);
+                }
+                QueryKind::Count => {
+                    let count = issues.len();
+                    let result = if let Some(value) = context.get(&name) {
+                        value.as_u64().unwrap() + count as u64
+                    } else {
+                        count as u64
+                    };
+
+                    context.insert(name, &result);
                 }
             }
         }
 
         for (name, issues) in &results {
-            context.insert(*name, issues);
+            context.insert(name, issues);
         }
 
-        TEMPLATES
+        Ok(TEMPLATES
             .render(&format!("{}.tt", self.name), &context)
-            .unwrap()
+            .unwrap())
     }
 }

+ 80 - 73
src/agenda.rs

@@ -1,5 +1,6 @@
 use crate::actions::{Action, Query, QueryKind, QueryMap, Step};
 use crate::github;
+use std::sync::Arc;
 
 pub fn prioritization<'a>() -> Box<dyn Action> {
     Box::new(Step {
@@ -12,7 +13,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "mcp_new_not_seconded",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["major-change", "to-announce"],
                             exclude_labels: vec![
@@ -28,7 +29,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "mcp_old_not_seconded",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["major-change"],
                             exclude_labels: vec![
@@ -44,7 +45,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_pre_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["proposed-final-comment-period"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -53,7 +54,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["final-comment-period"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -62,7 +63,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "mcp_accepted",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "all")],
                             include_labels: vec!["major-change-accepted", "to-announce"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -71,7 +72,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "fcp_finished",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "all")],
                             include_labels: vec![
                                 "finished-final-comment-period",
@@ -89,7 +90,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_pre_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["proposed-final-comment-period", "T-compiler"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -98,7 +99,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["final-comment-period", "T-compiler"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -107,7 +108,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "fcp_finished",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "all")],
                             include_labels: vec![
                                 "finished-final-comment-period",
@@ -125,7 +126,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_pre_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["proposed-final-comment-period"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -134,7 +135,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["final-comment-period"],
                             exclude_labels: vec!["t-libs", "t-libs-api"],
@@ -143,7 +144,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "fcp_finished",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "all")],
                             include_labels: vec![
                                 "finished-final-comment-period",
@@ -162,7 +163,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "beta_nominated_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![],
                             include_labels: vec!["beta-nominated", "T-compiler"],
                             exclude_labels: vec!["beta-accepted"],
@@ -171,7 +172,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "beta_nominated_t_rustdoc",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![],
                             include_labels: vec!["beta-nominated", "T-rustdoc"],
                             exclude_labels: vec!["beta-accepted"],
@@ -181,7 +182,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "stable_nominated_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![],
                             include_labels: vec!["stable-nominated", "T-compiler"],
                             exclude_labels: vec!["stable-accepted"],
@@ -190,7 +191,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "stable_nominated_t_rustdoc",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![],
                             include_labels: vec!["stable-nominated", "T-rustdoc"],
                             exclude_labels: vec!["stable-accepted"],
@@ -200,7 +201,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "prs_waiting_on_team_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["S-waiting-on-team", "T-compiler"],
                             exclude_labels: vec![],
@@ -210,7 +211,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_p_critical",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-compiler", "P-critical"],
                             exclude_labels: vec![],
@@ -219,7 +220,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_unassigned_p_critical",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("no", "assignee")],
                             include_labels: vec!["T-compiler", "P-critical"],
                             exclude_labels: vec![],
@@ -228,7 +229,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_p_high",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-compiler", "P-high"],
                             exclude_labels: vec![],
@@ -237,7 +238,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_unassigned_p_high",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("no", "assignee")],
                             include_labels: vec!["T-compiler", "P-high"],
                             exclude_labels: vec![],
@@ -246,7 +247,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_beta_p_critical",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-beta", "P-critical"],
                             exclude_labels: vec![],
@@ -255,7 +256,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_beta_p_high",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-beta", "P-high"],
                             exclude_labels: vec![],
@@ -264,7 +265,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_beta_p_medium",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-beta", "P-medium"],
                             exclude_labels: vec![],
@@ -273,7 +274,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_beta_p_low",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-beta", "P-low"],
                             exclude_labels: vec![],
@@ -282,7 +283,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_nightly_p_critical",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-nightly", "P-critical"],
                             exclude_labels: vec![],
@@ -291,7 +292,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_nightly_p_high",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-nightly", "P-high"],
                             exclude_labels: vec![],
@@ -300,7 +301,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_nightly_p_medium",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-nightly", "P-medium"],
                             exclude_labels: vec![],
@@ -309,7 +310,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_nightly_p_low",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-nightly", "P-low"],
                             exclude_labels: vec![],
@@ -318,7 +319,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_stable_p_critical",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-stable", "P-critical"],
                             exclude_labels: vec![],
@@ -327,7 +328,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_stable_p_high",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-stable", "P-high"],
                             exclude_labels: vec![],
@@ -336,7 +337,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_stable_p_medium",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-stable", "P-medium"],
                             exclude_labels: vec![],
@@ -345,7 +346,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "issues_of_note_regression_from_stable_to_stable_p_low",
                         kind: QueryKind::Count,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-stable", "P-low"],
                             exclude_labels: vec![],
@@ -354,7 +355,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "p_critical_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-compiler", "P-critical"],
                             exclude_labels: vec![],
@@ -363,7 +364,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "p_critical_t_rustdoc",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-rustdoc", "P-critical"],
                             exclude_labels: vec![],
@@ -372,7 +373,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "beta_regressions_p_high",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["regression-from-stable-to-beta", "P-high"],
                             exclude_labels: vec![
@@ -388,7 +389,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "nightly_regressions_unassigned_p_high",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("no", "assignee")],
                             include_labels: vec!["regression-from-stable-to-nightly", "P-high"],
                             exclude_labels: vec![
@@ -404,7 +405,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "nominated_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["I-compiler-nominated"],
                             exclude_labels: vec![],
@@ -413,7 +414,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "top_unreviewed_prs",
                         kind: QueryKind::List,
-                        query: Box::new(github::graphql::LeastRecentlyReviewedPullRequests),
+                        query: Arc::new(github::graphql::LeastRecentlyReviewedPullRequests),
                     },
                 ],
             },
@@ -425,7 +426,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "nominated_rfcs_t_compiler",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["I-compiler-nominated"],
                             exclude_labels: vec![],
@@ -437,7 +438,7 @@ pub fn prioritization<'a>() -> Box<dyn Action> {
     })
 }
 
-pub fn lang<'a>() -> Box<dyn Action> {
+pub fn lang<'a>() -> Box<dyn Action + Send + Sync> {
     Box::new(Step {
         name: "lang_agenda",
         actions: vec![
@@ -447,7 +448,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "pending_project_proposals",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "issue")],
                             include_labels: vec!["major-change"],
                             exclude_labels: vec!["charter-needed", "proposed-final-comment-period"],
@@ -456,7 +457,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "pending_lang_team_prs",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "pull-request")],
                             include_labels: vec![],
                             exclude_labels: vec![],
@@ -465,7 +466,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "scheduled_meetings",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "issue")],
                             include_labels: vec!["meeting-proposal", "meeting-scheduled"],
                             exclude_labels: vec![],
@@ -478,7 +479,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                 queries: vec![QueryMap {
                     name: "rfcs_waiting_to_be_merged",
                     kind: QueryKind::List,
-                    query: Box::new(github::Query {
+                    query: Arc::new(github::Query {
                         filters: vec![("state", "open"), ("is", "pr")],
                         include_labels: vec![
                             "disposition-merge",
@@ -501,7 +502,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "p_critical",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-lang", "P-critical"],
                             exclude_labels: vec![],
@@ -510,7 +511,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "nominated",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["I-lang-nominated"],
                             exclude_labels: vec![],
@@ -519,7 +520,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "proposed_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-lang", "proposed-final-comment-period"],
                             exclude_labels: vec!["finished-final-comment-period"],
@@ -528,7 +529,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "in_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-lang", "final-comment-period"],
                             exclude_labels: vec!["finished-final-comment-period"],
@@ -537,7 +538,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "finished_fcp",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open")],
                             include_labels: vec!["T-lang", "finished-final-comment-period"],
                             exclude_labels: vec![],
@@ -549,7 +550,7 @@ pub fn lang<'a>() -> Box<dyn Action> {
     })
 }
 
-pub fn lang_planning<'a>() -> Box<dyn Action> {
+pub fn lang_planning<'a>() -> Box<dyn Action + Send + Sync> {
     Box::new(Step {
         name: "lang_planning_agenda",
         actions: vec![
@@ -559,7 +560,7 @@ pub fn lang_planning<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "pending_project_proposals",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "issue")],
                             include_labels: vec!["major-change"],
                             exclude_labels: vec!["charter-needed"],
@@ -568,7 +569,7 @@ pub fn lang_planning<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "pending_lang_team_prs",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "pr")],
                             include_labels: vec![],
                             exclude_labels: vec![],
@@ -577,7 +578,7 @@ pub fn lang_planning<'a>() -> Box<dyn Action> {
                     QueryMap {
                         name: "proposed_meetings",
                         kind: QueryKind::List,
-                        query: Box::new(github::Query {
+                        query: Arc::new(github::Query {
                             filters: vec![("state", "open"), ("is", "issue")],
                             include_labels: vec!["meeting-proposal"],
                             exclude_labels: vec!["meeting-scheduled"],
@@ -590,7 +591,7 @@ pub fn lang_planning<'a>() -> Box<dyn Action> {
                 queries: vec![QueryMap {
                     name: "active_initiatives",
                     kind: QueryKind::List,
-                    query: Box::new(github::Query {
+                    query: Arc::new(github::Query {
                         filters: vec![("state", "open"), ("is", "issue")],
                         include_labels: vec!["lang-initiative"],
                         exclude_labels: vec![],
@@ -607,23 +608,29 @@ pub fn lang_planning<'a>() -> Box<dyn Action> {
 pub fn compiler_backlog_bonanza<'a>() -> Box<dyn Action> {
     Box::new(Step {
         name: "compiler_backlog_bonanza",
-        actions: vec![
-            Query {
-                repos: vec![
-                    ("rust-lang", "rust"),
-                ],
-                queries: vec![
-                    QueryMap {
-                        name: "tracking_issues",
-                        kind: QueryKind::List,
-                        query: Box::new(github::Query {
-                            filters: vec![("state", "open")],
-                            include_labels: vec!["C-tracking-issue"],
-                            exclude_labels: vec!["T-libs-api", "T-libs", "T-lang", "T-rustdoc"],
-                        }),
-                    },
-                ],
-            },
-        ],
+        actions: vec![Query {
+            repos: vec![("rust-lang", "rust")],
+            queries: vec![QueryMap {
+                name: "tracking_issues",
+                kind: QueryKind::List,
+                query: Arc::new(github::Query {
+                    filters: vec![("state", "open")],
+                    include_labels: vec!["C-tracking-issue"],
+                    exclude_labels: vec!["T-libs-api", "T-libs", "T-lang", "T-rustdoc"],
+                }),
+            }],
+        }],
     })
 }
+
+// Lists available agenda pages
+pub static INDEX: &str = r#"
+<html>
+<body>
+<ul>
+    <li><a href="/agenda/lang/triage">T-lang triage agenda</a></li>
+    <li><a href="/agenda/lang/planning">T-lang planning agenda</a></li>
+</ul>
+</body>
+</html>
+"#;

+ 6 - 4
src/bin/compiler.rs

@@ -1,7 +1,7 @@
 use triagebot::agenda;
 
 #[tokio::main(flavor = "current_thread")]
-async fn main() {
+async fn main() -> anyhow::Result<()> {
     dotenv::dotenv().ok();
     tracing_subscriber::fmt::init();
 
@@ -10,12 +10,14 @@ async fn main() {
         match &args[1][..] {
             "backlog_bonanza" => {
                 let agenda = agenda::compiler_backlog_bonanza();
-                print!("{}", agenda.call().await);
-                return;
+                print!("{}", agenda.call().await?);
+                return Ok(());
             }
             _ => {}
         }
     }
 
-    eprintln!("Usage: compiler (backlog_bonanza)")
+    eprintln!("Usage: compiler (backlog_bonanza)");
+
+    Ok(())
 }

+ 8 - 6
src/bin/lang.rs

@@ -1,7 +1,7 @@
 use triagebot::agenda;
 
 #[tokio::main(flavor = "current_thread")]
-async fn main() {
+async fn main() -> anyhow::Result<()> {
     dotenv::dotenv().ok();
     tracing_subscriber::fmt::init();
 
@@ -10,17 +10,19 @@ async fn main() {
         match &args[1][..] {
             "agenda" => {
                 let agenda = agenda::lang();
-                print!("{}", agenda.call().await);
-                return;
+                print!("{}", agenda.call().await?);
+                return Ok(());
             }
             "planning" => {
                 let agenda = agenda::lang_planning();
-                print!("{}", agenda.call().await);
-                return;
+                print!("{}", agenda.call().await?);
+                return Ok(());
             }
             _ => {}
         }
     }
 
-    eprintln!("Usage: lang (agenda|planning)")
+    eprintln!("Usage: lang (agenda|planning)");
+
+    Ok(())
 }

+ 3 - 2
src/bin/prioritization-agenda.rs

@@ -1,11 +1,12 @@
 use triagebot::agenda;
 
 #[tokio::main(flavor = "current_thread")]
-async fn main() {
+async fn main() -> anyhow::Result<()> {
     dotenv::dotenv().ok();
     tracing_subscriber::fmt::init();
 
     let agenda = agenda::prioritization();
 
-    print!("{}", agenda.call().await);
+    print!("{}", agenda.call().await?);
+    Ok(())
 }

+ 14 - 5
src/github.rs

@@ -372,7 +372,9 @@ impl IssueRepository {
         match client._send_req(client.get(&url)).await {
             Ok((_, _)) => Ok(true),
             Err(e) => {
-                if e.downcast_ref::<reqwest::Error>().map_or(false, |e| e.status() == Some(StatusCode::NOT_FOUND)) {
+                if e.downcast_ref::<reqwest::Error>()
+                    .map_or(false, |e| e.status() == Some(StatusCode::NOT_FOUND))
+                {
                     Ok(false)
                 } else {
                     Err(e)
@@ -549,7 +551,10 @@ impl Issue {
         }
 
         if !unknown_labels.is_empty() {
-            return Err(UnknownLabels { labels: unknown_labels }.into());
+            return Err(UnknownLabels {
+                labels: unknown_labels,
+            }
+            .into());
         }
 
         #[derive(serde::Serialize)]
@@ -558,7 +563,9 @@ impl Issue {
         }
 
         client
-            ._send_req(client.post(&url).json(&LabelsReq { labels: known_labels }))
+            ._send_req(client.post(&url).json(&LabelsReq {
+                labels: known_labels,
+            }))
             .await
             .context("failed to add labels")?;
 
@@ -873,7 +880,7 @@ pub struct IssueSearchResult {
     pub items: Vec<Issue>,
 }
 
-#[derive(Debug, serde::Deserialize)]
+#[derive(Clone, Debug, serde::Deserialize)]
 pub struct Repository {
     pub full_name: String,
 }
@@ -1460,7 +1467,9 @@ mod tests {
 
     #[test]
     fn display_labels() {
-        let x = UnknownLabels { labels: vec!["A-bootstrap".into(), "xxx".into()] };
+        let x = UnknownLabels {
+            labels: vec!["A-bootstrap".into(), "xxx".into()],
+        };
         assert_eq!(x.to_string(), "Unknown labels: A-bootstrap, xxx");
     }
 

+ 64 - 4
src/main.rs

@@ -7,11 +7,27 @@ use hyper::{header, Body, Request, Response, Server, StatusCode};
 use reqwest::Client;
 use route_recognizer::Router;
 use std::{env, net::SocketAddr, sync::Arc};
+use tower::{Service, ServiceExt};
 use tracing as log;
 use tracing::Instrument;
 use triagebot::{db, github, handlers::Context, notification_listing, payload, EventName};
 
-async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Body>, hyper::Error> {
+async fn handle_agenda_request(req: String) -> anyhow::Result<String> {
+    if req == "/agenda/lang/triage" {
+        return triagebot::agenda::lang().call().await;
+    }
+    if req == "/agenda/lang/planning" {
+        return triagebot::agenda::lang_planning().call().await;
+    }
+
+    anyhow::bail!("Unknown agenda; see /agenda for index.")
+}
+
+async fn serve_req(
+    req: Request<Body>,
+    ctx: Arc<Context>,
+    mut agenda: impl Service<String, Response = String, Error = tower::BoxError>,
+) -> Result<Response<Body>, hyper::Error> {
     log::info!("request = {:?}", req);
     let mut router = Router::new();
     router.add("/triage", "index".to_string());
@@ -28,6 +44,36 @@ async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Bod
             return triagebot::triage::index();
         }
     }
+
+    if req.uri.path() == "/agenda" {
+        return Ok(Response::builder()
+            .status(StatusCode::OK)
+            .body(Body::from(triagebot::agenda::INDEX))
+            .unwrap());
+    }
+    if req.uri.path() == "/agenda/lang/triage" || req.uri.path() == "/agenda/lang/planning" {
+        match agenda
+            .ready()
+            .await
+            .expect("agenda keeps running")
+            .call(req.uri.path().to_owned())
+            .await
+        {
+            Ok(agenda) => {
+                return Ok(Response::builder()
+                    .status(StatusCode::OK)
+                    .body(Body::from(agenda))
+                    .unwrap())
+            }
+            Err(err) => {
+                return Ok(Response::builder()
+                    .status(StatusCode::INTERNAL_SERVER_ERROR)
+                    .body(Body::from(err.to_string()))
+                    .unwrap())
+            }
+        }
+    }
+
     if req.uri.path() == "/" {
         return Ok(Response::builder()
             .status(StatusCode::OK)
@@ -186,8 +232,6 @@ async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Bod
 }
 
 async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
-    log::info!("Listening on http://{}", addr);
-
     let pool = db::ClientPool::new();
     db::run_migrations(&*pool.get().await)
         .await
@@ -206,13 +250,27 @@ async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
         octocrab: oc,
     });
 
+    let agenda = tower::ServiceBuilder::new()
+        .buffer(10)
+        .layer_fn(|input| {
+            tower::util::MapErr::new(
+                tower::load_shed::LoadShed::new(tower::limit::RateLimit::new(
+                    input,
+                    tower::limit::rate::Rate::new(2, std::time::Duration::from_secs(60)),
+                )),
+                |_| anyhow::anyhow!("Rate limit of 2 request / 60 seconds exceeded"),
+            )
+        })
+        .service_fn(handle_agenda_request);
+
     let svc = hyper::service::make_service_fn(move |_conn| {
         let ctx = ctx.clone();
+        let agenda = agenda.clone();
         async move {
             Ok::<_, hyper::Error>(hyper::service::service_fn(move |req| {
                 let uuid = uuid::Uuid::new_v4();
                 let span = tracing::span!(tracing::Level::INFO, "request", ?uuid);
-                serve_req(req, ctx.clone())
+                serve_req(req, ctx.clone(), agenda.clone())
                     .map(move |mut resp| {
                         if let Ok(resp) = &mut resp {
                             resp.headers_mut()
@@ -225,6 +283,8 @@ async fn run_server(addr: SocketAddr) -> anyhow::Result<()> {
             }))
         }
     });
+    log::info!("Listening on http://{}", addr);
+
     let serve_future = Server::bind(&addr).serve(svc);
 
     serve_future.await?;