Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 17 additions & 8 deletions src/database/client.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use super::operations::{
approve_pull_request, clear_auto_build, create_build, create_workflow, delegate_pull_request,
delete_tagged_bot_comment, find_build, find_pr_by_build, get_nonclosed_pull_requests,
get_pending_builds, get_prs_with_stale_mergeability_or_approved, get_pull_request,
get_repository, get_repository_by_name, get_tagged_bot_comments, get_workflow_urls_for_build,
get_workflows_for_build, insert_repo_if_not_exists, is_rollup, record_tagged_bot_comment,
set_pr_assignees, set_pr_mergeability_state, set_pr_priority, set_pr_rollup_mode,
set_pr_status, set_stale_mergeability_status_by_base_branch, unapprove_pull_request,
undelegate_pull_request, update_build, update_pr_try_build_id, update_workflow_status,
upsert_pull_request, upsert_repository,
delete_tagged_bot_comment, find_build, find_pr_by_build, get_last_n_successful_auto_builds,
get_nonclosed_pull_requests, get_pending_builds, get_prs_with_stale_mergeability_or_approved,
get_pull_request, get_repository, get_repository_by_name, get_tagged_bot_comments,
get_workflow_urls_for_build, get_workflows_for_build, insert_repo_if_not_exists, is_rollup,
record_tagged_bot_comment, set_pr_assignees, set_pr_mergeability_state, set_pr_priority,
set_pr_rollup_mode, set_pr_status, set_stale_mergeability_status_by_base_branch,
unapprove_pull_request, undelegate_pull_request, update_build, update_pr_try_build_id,
update_workflow_status, upsert_pull_request, upsert_repository,
};
use super::{
ApprovalInfo, DelegatedPermission, MergeableState, PrimaryKey, RunId, UpdateBuildParams,
Expand Down Expand Up @@ -406,6 +406,15 @@ impl PgDbClient {
pub async fn is_rollup(&self, pr: &PullRequestModel) -> anyhow::Result<bool> {
is_rollup(&self.pool, pr.id).await
}

/// Return the last N auto builds that have succeeded on the given repository.
pub async fn get_last_n_successful_auto_builds(
&self,
repo: &GithubRepoName,
n: u32,
) -> anyhow::Result<Vec<BuildModel>> {
get_last_n_successful_auto_builds(&self.pool, repo, n).await
}
}

pub enum ExclusiveOperationOutcome<R> {
Expand Down
37 changes: 37 additions & 0 deletions src/database/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,43 @@ pub(crate) async fn is_rollup(
.await
}

pub(crate) async fn get_last_n_successful_auto_builds(
executor: impl PgExecutor<'_>,
repo: &GithubRepoName,
n: u32,
) -> anyhow::Result<Vec<BuildModel>> {
measure_db_query("get_last_n_successful_auto_builds", || async {
let builds = sqlx::query_as!(
BuildModel,
r#"
SELECT
id,
repository as "repository: GithubRepoName",
branch,
kind as "kind: BuildKind",
commit_sha,
status as "status: BuildStatus",
parent,
created_at as "created_at: DateTime<Utc>",
check_run_id,
duration as "duration: PgDuration"
FROM build
WHERE repository = $1 AND
kind = 'auto' AND
status = 'success'
ORDER BY created_at DESC
LIMIT $2
"#,
repo as &GithubRepoName,
n as i32
)
.fetch_all(executor)
.await?;
Ok(builds)
})
.await
}

#[cfg(test)]
mod tests {
use crate::bors::PullRequestStatus;
Expand Down
88 changes: 81 additions & 7 deletions src/server/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::bors::event::BorsEvent;
use crate::bors::{CommandPrefix, RepositoryState, format_help};
use crate::database::{ApprovalStatus, QueueStatus};
use crate::github::{GithubRepoName, rollup};
use crate::github::{GithubRepoName, PullRequestNumber, rollup};
use crate::templates::{
HelpTemplate, HtmlTemplate, NotFoundTemplate, PullRequestStats, QueueTemplate, RepositoryView,
RollupsInfo,
Expand All @@ -16,12 +16,14 @@ use axum::response::{IntoResponse, Redirect, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use axum_embed::ServeEmbed;
use chrono::Utc;
use http::{Request, StatusCode};
use pulldown_cmark::Parser;
use rust_embed::Embed;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -332,7 +334,17 @@ pub async fn queue_handler(
}
};

let prs = db.get_nonclosed_pull_requests(&repo.name).await?;
// Perform the queries concurrently to save a bit of time
let (prs, rollups, last_ten_builds) = futures::future::join3(
db.get_nonclosed_pull_requests(&repo.name),
db.get_nonclosed_rollups(&repo.name),
db.get_last_n_successful_auto_builds(&repo.name, 10),
)
.await;
let prs = prs?;
let rollups = rollups?;
let last_ten_builds = last_ten_builds?;

let prs = sort_queue_prs(prs);

// Note: this assumed that there is ever at most a single pending build
Expand All @@ -345,19 +357,80 @@ pub async fn queue_handler(
None => None,
};

let (in_queue_count, failed_count) = prs.iter().fold((0, 0), |(in_queue, failed), pr| {
let (in_queue_inc, failed_inc) = match pr.queue_status() {
let average_auto_duration = {
let total_duration = last_ten_builds
.iter()
.filter_map(|build| build.duration)
.map(|d| d.0)
.sum::<Duration>();
let total_duration = if total_duration.is_zero() {
// Default guess of 3 hours
Duration::from_secs(3600 * 3)
} else {
total_duration
};
let count = last_ten_builds.len() as u32;
let count = if count > 0 { count } else { 1 };
total_duration / count
};

let mut in_queue_count = 0;
let mut failed_count = 0;

// PR number -> expected remaining duration
let mut in_queue: HashMap<PullRequestNumber, Duration> = HashMap::new();
for pr in &prs {
let status = pr.queue_status();
let (in_queue_inc, failed_inc) = match &status {
QueueStatus::Approved(..) => (1, 0),
QueueStatus::ReadyForMerge(..) => (1, 0),
QueueStatus::Pending(..) => (1, 0),
QueueStatus::Failed(..) => (0, 1),
QueueStatus::NotApproved | QueueStatus::NotOpen => (0, 0),
};
in_queue_count += in_queue_inc;
failed_count += failed_inc;

match &status {
QueueStatus::Pending(_, _) => {
// Try to guess already elapsed time of the pending workflow
let subtract_duration = if let Some(workflow) = &pending_workflow {
(Utc::now() - workflow.created_at)
.to_std()
.unwrap_or_default()
} else {
Duration::ZERO
};
in_queue.insert(pr.number, average_auto_duration - subtract_duration);
}
// For an approved PR, assume that it will take the average auto build duration
QueueStatus::Approved(_) => {
in_queue.insert(pr.number, average_auto_duration);
}
QueueStatus::Failed(_, _)
| QueueStatus::ReadyForMerge(_, _)
| QueueStatus::NotOpen
| QueueStatus::NotApproved => {}
}
}

(in_queue + in_queue_inc, failed + failed_inc)
});
let mut expected_remaining_duration = Duration::ZERO;

let rollups = db.get_nonclosed_rollups(&repo.name).await?;
// Rollup members whose rollup is in the queue, and thus its duration will be counted
let rollup_members: HashSet<PullRequestNumber> = rollups
.iter()
.filter(|(rollup, _)| in_queue.contains_key(*rollup))
.flat_map(|(_, member)| member)
.copied()
.collect();

for (pr, remaining_duration) in in_queue {
// For a rollup member, we will count its rollup instead
if rollup_members.contains(&pr) {
continue;
}
expected_remaining_duration += remaining_duration;
}

Ok(HtmlTemplate(QueueTemplate {
oauth_client_id: oauth
Expand All @@ -376,6 +449,7 @@ pub async fn queue_handler(
pending_workflow,
selected_rollup_prs: params.pull_requests.map(|prs| prs.0).unwrap_or_default(),
rollups_info: RollupsInfo::from(rollups),
expected_remaining_duration,
})
.into_response())
}
Expand Down
33 changes: 33 additions & 0 deletions src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use axum::response::{Html, IntoResponse, Response};
use http::StatusCode;
use itertools::Itertools;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::time::Duration;

/// Build status to display on the queue page.
pub fn status_text(pr: &PullRequestModel) -> String {
Expand Down Expand Up @@ -84,6 +86,37 @@ pub struct QueueTemplate {
pub selected_rollup_prs: Vec<u32>,
// Active workflow for an active pending auto build
pub pending_workflow: Option<WorkflowModel>,
// Guesstimated duration to merge all current approved/pending PRs in the queue
pub expected_remaining_duration: Duration,
}

impl QueueTemplate {
fn format_duration(&self, duration: Duration) -> String {
let total_seconds = duration.as_secs();
let days = total_seconds / 86400;
let hours = (total_seconds % 86400) / 3600;
let minutes = (total_seconds % 3600) / 60;

let mut output = String::new();
if days > 0 {
write!(output, "{days}d").unwrap();
}

if hours > 0 {
if !output.is_empty() {
output.push(' ');
}
write!(output, "{hours}h").unwrap();
}

if days == 0 && minutes > 0 {
if !output.is_empty() {
output.push(' ');
}
write!(output, "{minutes}m").unwrap();
}
output
}
}

#[derive(Template)]
Expand Down
7 changes: 7 additions & 0 deletions web/templates/queue.html
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ <h1>
{{ stats.total_count }} total, {{ stats.in_queue_count }} in queue,
{{ stats.failed_count }} failed
</p>
{%- if !expected_remaining_duration.is_zero() -%}
<div style="margin: 0 5px;">|</div>
<p style="margin-bottom: 0;"
title="Expected duration to merge all current PRs in the queue, assuming that there are no build failures.">
Estimated time to merge queue: {{ format_duration(*expected_remaining_duration) }}
</p>
{%- endif -%}
</div>

<!-- DOM template that will be cloned into the DataTables top layout -->
Expand Down