| 1 | //! What follows for coordination when a blocker resolves, and what an |
| 2 | //! agent picking up a work item sees of its blockers. What a work item is |
| 3 | //! blocked by, and changing it, is items/dependencies.rs. |
| 4 | //! |
| 5 | //! Each blocker that closes or has a pull request merged for it is a |
| 6 | //! `dependency.changed` event on the work item it blocks. |
| 7 | |
| 8 | use g1t_contracts::work::*; |
| 9 | use g1t_contracts::User; |
| 10 | use serde::Deserialize; |
| 11 | use worker::Result; |
| 12 | |
| 13 | use super::{CLAIM_COLUMNS, ClaimRow}; |
| 14 | use crate::Work; |
| 15 | use crate::items::{self, Held}; |
| 16 | |
| 17 | #[derive(Deserialize)] |
| 18 | struct PullBriefRow { |
| 19 | number: u32, |
| 20 | title: String, |
| 21 | status: PullStatus, |
| 22 | item_id: String, |
| 23 | } |
| 24 | |
| 25 | impl Work { |
| 26 | /// A work item closed (`closed`) or had a pull request merged for it |
| 27 | /// (`merged_pull`): its claims end if it closed, and each open work |
| 28 | /// item it blocks hears that it cleared. Never fails what caused it: a |
| 29 | /// problem is logged. |
| 30 | pub(crate) async fn item_resolved(&self, held: &Held, actor: &User, closed: bool, merged_pull: Option<u32>) { |
| 31 | let number = held.item.number; |
| 32 | let resolved = async { |
| 33 | if closed { |
| 34 | self.release_claims_on_close(held, Some(actor)).await?; |
| 35 | } |
| 36 | for dependent in items::dependents(self, held).await? { |
| 37 | let blocked_by = dependent.item.blocked_by.clone(); |
| 38 | let remaining = items::still_open(self, &dependent.item.workspace, &blocked_by, Some(number)).await?; |
| 39 | let change = DependencyChanged { |
| 40 | workspace: dependent.item.workspace.clone(), |
| 41 | number: dependent.item.number, |
| 42 | item: dependent.item.number, |
| 43 | repo_ids: dependent.repo_ids(), |
| 44 | blocked_by, |
| 45 | cleared: vec![number], |
| 46 | remaining, |
| 47 | merged_pull, |
| 48 | ..DependencyChanged::default() |
| 49 | }; |
| 50 | // Follows from a close or merge already made: named by it, so |
| 51 | // a repeat of the same close or merge tells each session once. |
| 52 | let how = merged_pull.map_or_else(|| "closed".to_owned(), |pull| format!("merged:{pull}")); |
| 53 | let key = format!("{}:cleared:{}:{how}", dependent.item.id, held.item.id); |
| 54 | let data = g1t_contracts::events::marked(change, Some(actor)); |
| 55 | self.publish_keyed_to("dependency.changed", &dependent.repo_ids(), Some(actor.id.clone()), data, &key).await?; |
| 56 | } |
| 57 | Ok::<(), worker::Error>(()) |
| 58 | }; |
| 59 | if let Err(error) = resolved.await { |
| 60 | worker::console_error!("#{number} resolved, but its dependents were not told: {error}"); |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | /// The work items of `workspace` that `numbers` names, each with its |
| 65 | /// active claims and its pull requests, in the order given. |
| 66 | pub(crate) async fn blockers(&self, workspace: &str, numbers: &[u32]) -> Result<Vec<Blocker>> { |
| 67 | let items = items::briefs(self, workspace, numbers).await?; |
| 68 | if items.is_empty() { |
| 69 | return Ok(Vec::new()); |
| 70 | } |
| 71 | let ids = serde_json::to_string(&items.iter().map(|item| item.id.as_str()).collect::<Vec<_>>())?; |
| 72 | let bind = |sql: String| -> Result<worker::D1PreparedStatement> { self.db.prepare(sql).bind(&[ids.as_str().into()]) }; |
| 73 | let results = self |
| 74 | .db |
| 75 | .batch(vec![ |
| 76 | bind(format!( |
| 77 | "SELECT {CLAIM_COLUMNS} FROM claims |
| 78 | WHERE state = 'active' AND item_id IN (SELECT value FROM json_each(?1)) |
| 79 | ORDER BY created_at, id" |
| 80 | ))?, |
| 81 | bind( |
| 82 | "SELECT number, title, status, item_id FROM pulls |
| 83 | WHERE item_id IN (SELECT value FROM json_each(?1)) |
| 84 | ORDER BY created_at" |
| 85 | .to_owned(), |
| 86 | )?, |
| 87 | ]) |
| 88 | .await?; |
| 89 | let mut results = results.into_iter(); |
| 90 | let mut next = || results.next(); |
| 91 | let claims: Vec<(String, Claim)> = match next() { |
| 92 | Some(result) => result.results::<ClaimRow>()?.into_iter().map(|row| (row.item_id.clone(), Claim::from(row))).collect(), |
| 93 | None => Vec::new(), |
| 94 | }; |
| 95 | let pulls: Vec<PullBriefRow> = match next() { |
| 96 | Some(result) => result.results()?, |
| 97 | None => Vec::new(), |
| 98 | }; |
| 99 | Ok(numbers |
| 100 | .iter() |
| 101 | .filter_map(|number| { |
| 102 | let item = items.iter().find(|item| item.number == *number)?; |
| 103 | Some(Blocker { |
| 104 | number: item.number, |
| 105 | title: item.title.clone(), |
| 106 | state: item.state, |
| 107 | claims: claims.iter().filter(|(id, _)| *id == item.id).map(|(_, claim)| claim.clone()).collect(), |
| 108 | pulls: pulls |
| 109 | .iter() |
| 110 | .filter(|pull| pull.item_id == item.id) |
| 111 | .map(|pull| PullBrief { number: pull.number, title: pull.title.clone(), status: pull.status }) |
| 112 | .collect(), |
| 113 | }) |
| 114 | }) |
| 115 | .collect()) |
| 116 | } |
| 117 | } |