| 29 | 29 | | use g1t_kit::now_ms; |
| 30 | 30 | | use serde::Deserialize; |
| 31 | 31 | | use serde_json::{Value, json}; |
| 32 | + | use std::cell::RefCell; |
| 33 | + | use std::collections::HashMap; |
| 32 | 34 | | use worker::Result; |
| 33 | 35 | | |
| 34 | 36 | | use crate::plan::{JobRow, RunRow}; |
| ⋯ |
| 322 | 324 | | } |
| 323 | 325 | | |
| 324 | 326 | | /// Whether `repo`'s workflows run at all: as chosen, else on, and off |
| 325 | | − | /// for a fork. Nothing starts a run while they are off. |
| 327 | + | /// for a fork. Nothing starts a run while they are off. Read once per |
| 328 | + | /// request ([`ActionsOn`]), however many runs it starts. |
| 326 | 329 | | pub(crate) async fn actions_on(&self, repo: &Repo) -> Result<bool> { |
| 327 | 330 | | #[derive(Deserialize)] |
| 328 | 331 | | struct Enabled { |
| 329 | 332 | | enabled: Option<u32>, |
| 330 | 333 | | } |
| 331 | | − | let row = self |
| 332 | | − | .db |
| 333 | | − | .prepare("SELECT enabled FROM repo_settings WHERE repo_id = ?") |
| 334 | | − | .bind(&[repo.id.as_str().into()])? |
| 335 | | − | .first::<Enabled>(None) |
| 336 | | − | .await?; |
| 337 | | − | Ok(actions_enabled(row.and_then(|row| row.enabled).map(|on| on == 1), repo.forked_from.is_some())) |
| 334 | + | self.actions_on |
| 335 | + | .get(&repo.id, || async { |
| 336 | + | let row = self |
| 337 | + | .db |
| 338 | + | .prepare("SELECT enabled FROM repo_settings WHERE repo_id = ?") |
| 339 | + | .bind(&[repo.id.as_str().into()])? |
| 340 | + | .first::<Enabled>(None) |
| 341 | + | .await?; |
| 342 | + | Ok(actions_enabled(row.and_then(|row| row.enabled).map(|on| on == 1), repo.forked_from.is_some())) |
| 343 | + | }) |
| 344 | + | .await |
| 338 | 345 | | } |
| 339 | 346 | | |
| 340 | 347 | | /// What a workflow without `permissions:` gets in the repository, and |
| ⋯ |
| 433 | 440 | | ])? |
| 434 | 441 | | .run() |
| 435 | 442 | | .await?; |
| 443 | + | self.actions_on.forget(&repo.id); |
| 436 | 444 | | Ok(Outcome::Ok(self.repo_settings(&repo).await?)) |
| 437 | 445 | | } |
| 438 | 446 | | |
| ⋯ |
| 1178 | 1186 | | } |
| 1179 | 1187 | | } |
| 1180 | 1188 | | |
| 1189 | + | /// Whether repositories' Actions are on, as this request read them: a push |
| 1190 | + | /// or a minute's schedules that start several runs read each repository's |
| 1191 | + | /// setting once. The service is made afresh for each request, and |
| 1192 | + | /// `Actions::on_event` clears it for each queued event, so a change is |
| 1193 | + | /// seen by the next one. |
| 1194 | + | #[derive(Default)] |
| 1195 | + | pub(crate) struct ActionsOn(RefCell<HashMap<String, bool>>); |
| 1196 | + | |
| 1197 | + | impl ActionsOn { |
| 1198 | + | /// `repo_id`'s, from `read` the first time it is asked for. |
| 1199 | + | pub(crate) async fn get<F, Fut>(&self, repo_id: &str, read: F) -> Result<bool> |
| 1200 | + | where |
| 1201 | + | F: FnOnce() -> Fut, |
| 1202 | + | Fut: Future<Output = Result<bool>>, |
| 1203 | + | { |
| 1204 | + | if let Some(on) = self.0.borrow().get(repo_id).copied() { |
| 1205 | + | return Ok(on); |
| 1206 | + | } |
| 1207 | + | let on = read().await?; |
| 1208 | + | self.0.borrow_mut().insert(repo_id.to_owned(), on); |
| 1209 | + | Ok(on) |
| 1210 | + | } |
| 1211 | + | |
| 1212 | + | /// Reads `repo_id`'s again next time, after it was changed. |
| 1213 | + | pub(crate) fn forget(&self, repo_id: &str) { |
| 1214 | + | self.0.borrow_mut().remove(repo_id); |
| 1215 | + | } |
| 1216 | + | |
| 1217 | + | /// Reads every repository's again. |
| 1218 | + | pub(crate) fn clear(&self) { |
| 1219 | + | self.0.borrow_mut().clear(); |
| 1220 | + | } |
| 1221 | + | } |
| 1222 | + | |
| 1181 | 1223 | | /// A workflow job's own token may not approve or change what keeps runs |
| 1182 | 1224 | | /// safe: a workflow could otherwise let itself through. |
| 1183 | 1225 | | fn refuse_job_token<T>(actor: &User) -> Option<Outcome<T>> { |
| ⋯ |
| 1208 | 1250 | | ); |
| 1209 | 1251 | | } |
| 1210 | 1252 | | |
| 1253 | + | /// Runs a future that never waits, as `ActionsOn`'s do here. |
| 1254 | + | fn ready<T>(future: impl Future<Output = T>) -> T { |
| 1255 | + | let mut future = std::pin::pin!(future); |
| 1256 | + | let mut cx = std::task::Context::from_waker(std::task::Waker::noop()); |
| 1257 | + | match future.as_mut().poll(&mut cx) { |
| 1258 | + | std::task::Poll::Ready(value) => value, |
| 1259 | + | std::task::Poll::Pending => panic!("the future waited"), |
| 1260 | + | } |
| 1261 | + | } |
| 1262 | + | |
| 1263 | + | #[test] |
| 1264 | + | fn a_request_reads_each_repositorys_setting_once() { |
| 1265 | + | let on = ActionsOn::default(); |
| 1266 | + | let reads = std::cell::Cell::new(0); |
| 1267 | + | let read = |enabled: bool| { |
| 1268 | + | reads.set(reads.get() + 1); |
| 1269 | + | async move { Ok(enabled) } |
| 1270 | + | }; |
| 1271 | + | // A fork with Actions off: every run of one push is refused, from |
| 1272 | + | // one read. |
| 1273 | + | for _ in 0..5 { |
| 1274 | + | assert!(!ready(on.get("rep_fork", || read(actions_enabled(None, true)))).unwrap()); |
| 1275 | + | } |
| 1276 | + | assert_eq!(reads.get(), 1); |
| 1277 | + | // Another repository is its own read. |
| 1278 | + | for _ in 0..3 { |
| 1279 | + | assert!(ready(on.get("rep_main", || read(actions_enabled(None, false)))).unwrap()); |
| 1280 | + | } |
| 1281 | + | assert_eq!(reads.get(), 2); |
| 1282 | + | // Turned on in Settings: read again, and runs start. |
| 1283 | + | on.forget("rep_fork"); |
| 1284 | + | assert!(ready(on.get("rep_fork", || read(actions_enabled(Some(true), true)))).unwrap()); |
| 1285 | + | assert_eq!(reads.get(), 3); |
| 1286 | + | // The next queued event reads both afresh. |
| 1287 | + | on.clear(); |
| 1288 | + | assert!(!ready(on.get("rep_fork", || read(false))).unwrap()); |
| 1289 | + | assert!(ready(on.get("rep_main", || read(true))).unwrap()); |
| 1290 | + | assert_eq!(reads.get(), 5); |
| 1291 | + | } |
| 1292 | + | |
| 1293 | + | #[test] |
| 1294 | + | fn a_failed_read_is_not_kept() { |
| 1295 | + | let on = ActionsOn::default(); |
| 1296 | + | assert!(ready(on.get("rep_a", || async { Err(worker::Error::RustError("D1 is down".into())) })).is_err()); |
| 1297 | + | assert!(ready(on.get("rep_a", || async { Ok(true) })).unwrap()); |
| 1298 | + | } |
| 1299 | + | |
| 1211 | 1300 | | fn env(reviewers: &[(&str, &str)]) -> Environment { |
| 1212 | 1301 | | Environment { |
| 1213 | 1302 | | reviewers: reviewers.iter().map(|(kind, name)| EnvironmentReviewer { kind: (*kind).into(), name: (*name).into() }).collect(), |