Skip to content

Commit b7d6444

Browse files
committed
Sort and retrieve PRs in merge queue
1 parent f7a3ad3 commit b7d6444

File tree

6 files changed

+366
-1
lines changed

6 files changed

+366
-1
lines changed

.sqlx/query-b1fe02180d44f3cdc04ca0be76163606a84a5ed390079e4786e471b2e8850e49.json

Lines changed: 193 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bors/merge_queue.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,38 @@ use tokio::sync::mpsc;
44
use tracing::Instrument;
55

66
use crate::BorsContext;
7+
use crate::bors::RepositoryState;
8+
use crate::utils::sort_queue::sort_queue_prs;
79

810
type MergeQueueEvent = ();
911
pub type MergeQueueSender = mpsc::Sender<MergeQueueEvent>;
1012

11-
pub async fn merge_queue_tick(_ctx: Arc<BorsContext>) -> anyhow::Result<()> {
13+
pub async fn merge_queue_tick(ctx: Arc<BorsContext>) -> anyhow::Result<()> {
14+
let repos: Vec<Arc<RepositoryState>> =
15+
ctx.repositories.read().unwrap().values().cloned().collect();
16+
17+
for repo in repos {
18+
let repo_name = repo.repository();
19+
let repo_db = match ctx.db.repo_db(repo_name).await? {
20+
Some(repo) => repo,
21+
None => {
22+
tracing::error!("Repository {repo_name} not found");
23+
continue;
24+
}
25+
};
26+
let priority = repo_db.tree_state.priority();
27+
let prs = ctx.db.get_merge_queue_prs(repo_name, priority).await?;
28+
29+
// Sort PRs according to merge queue priority rules.
30+
// Successful builds come first so they can be merged immediately,
31+
// then pending builds (which block the queue to prevent starting simultaneous auto-builds).
32+
let prs = sort_queue_prs(prs);
33+
34+
for _ in prs {
35+
// Process PRs...
36+
}
37+
}
38+
1239
Ok(())
1340
}
1441

src/database/client.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use sqlx::PgPool;
22

33
use crate::bors::{PullRequestStatus, RollupMode};
4+
use crate::database::operations::get_merge_queue_prs;
45
use crate::database::{
56
BuildModel, BuildStatus, PullRequestModel, RepoModel, TreeState, WorkflowModel, WorkflowStatus,
67
WorkflowType,
@@ -308,4 +309,12 @@ impl PgDbClient {
308309
pub async fn delete_auto_build(&self, pr: &PullRequestModel) -> anyhow::Result<()> {
309310
delete_auto_build(&self.pool, pr.id).await
310311
}
312+
313+
pub async fn get_merge_queue_prs(
314+
&self,
315+
repo: &GithubRepoName,
316+
tree_priority: Option<u32>,
317+
) -> anyhow::Result<Vec<PullRequestModel>> {
318+
get_merge_queue_prs(&self.pool, repo, tree_priority.map(|p| p as i32)).await
319+
}
311320
}

src/database/operations.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,3 +991,58 @@ pub(crate) async fn delete_auto_build(
991991
})
992992
.await
993993
}
994+
995+
/// Fetches pull requests eligible for merge:
996+
/// - Only approved PRs that are open and mergeable
997+
/// - Includes only PRs with pending or successful auto builds
998+
/// - Excludes PRs that do not meet the tree closure priority threshold (if tree closed)
999+
pub(crate) async fn get_merge_queue_prs(
1000+
executor: impl PgExecutor<'_>,
1001+
repo: &GithubRepoName,
1002+
tree_priority: Option<i32>,
1003+
) -> anyhow::Result<Vec<PullRequestModel>> {
1004+
measure_db_query("get_merge_queue_prs", || async {
1005+
let records = sqlx::query_as!(
1006+
PullRequestModel,
1007+
r#"
1008+
SELECT
1009+
pr.id,
1010+
pr.repository as "repository: GithubRepoName",
1011+
pr.number as "number!: i64",
1012+
pr.title,
1013+
pr.author,
1014+
pr.assignees as "assignees: Assignees",
1015+
(
1016+
pr.approved_by,
1017+
pr.approved_sha
1018+
) AS "approval_status!: ApprovalStatus",
1019+
pr.status as "pr_status: PullRequestStatus",
1020+
pr.priority,
1021+
pr.rollup as "rollup: RollupMode",
1022+
pr.delegated_permission as "delegated_permission: DelegatedPermission",
1023+
pr.base_branch,
1024+
pr.mergeable_state as "mergeable_state: MergeableState",
1025+
pr.created_at as "created_at: DateTime<Utc>",
1026+
try_build AS "try_build: BuildModel",
1027+
auto_build AS "auto_build: BuildModel"
1028+
FROM pull_request as pr
1029+
LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id
1030+
LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id
1031+
WHERE pr.repository = $1
1032+
AND pr.status = 'open'
1033+
AND pr.approved_by IS NOT NULL
1034+
AND pr.mergeable_state = 'mergeable'
1035+
-- Include only PRs with pending or successful auto builds
1036+
AND (auto_build.status IS NULL OR auto_build.status IN ('pending', 'success'))
1037+
-- Tree closure check (if tree_priority is set)
1038+
AND ($2::int IS NULL OR pr.priority >= $2)
1039+
"#,
1040+
repo as &GithubRepoName,
1041+
tree_priority
1042+
)
1043+
.fetch_all(executor)
1044+
.await?;
1045+
Ok(records)
1046+
})
1047+
.await
1048+
}

src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod logging;
2+
pub mod sort_queue;
23
pub mod text;
34
pub mod timing;

src/utils/sort_queue.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::bors::RollupMode;
2+
use crate::database::{BuildStatus, MergeableState, PullRequestModel};
3+
4+
/// Sorts pull requests according to merge queue priority rules.
5+
/// Ordered by pending builds > success builds > approval > mergeability > priority value > rollup > age.
6+
pub fn sort_queue_prs(mut prs: Vec<PullRequestModel>) -> Vec<PullRequestModel> {
7+
prs.sort_by(|a, b| {
8+
// 1. Pending builds come first (to block merge queue)
9+
get_queue_blocking_priority(a)
10+
.cmp(&get_queue_blocking_priority(b))
11+
// 2. Compare approval status (approved PRs should come first)
12+
.then_with(|| a.is_approved().cmp(&b.is_approved()).reverse())
13+
// 3. Compare build status within approval groups
14+
.then_with(|| get_status_priority(a).cmp(&get_status_priority(b)))
15+
// 4. Compare mergeable state (0 = mergeable, 1 = conflicts/unknown)
16+
.then_with(|| get_mergeable_priority(a).cmp(&get_mergeable_priority(b)))
17+
// 5. Compare priority numbers (higher priority should come first)
18+
.then_with(|| {
19+
a.priority
20+
.unwrap_or(0)
21+
.cmp(&b.priority.unwrap_or(0))
22+
.reverse()
23+
})
24+
// 6. Compare rollup mode (-1 = never/iffy, 0 = maybe, 1 = always)
25+
.then_with(|| {
26+
get_rollup_priority(a.rollup.as_ref()).cmp(&get_rollup_priority(b.rollup.as_ref()))
27+
})
28+
// 7. Compare PR numbers (older first)
29+
.then_with(|| a.number.cmp(&b.number))
30+
});
31+
prs
32+
}
33+
34+
fn get_queue_blocking_priority(pr: &PullRequestModel) -> u32 {
35+
match &pr.auto_build {
36+
Some(build) => match build.status {
37+
// Pending builds must come first to block the merge queue
38+
BuildStatus::Pending => 0,
39+
// All other statuses come after
40+
_ => 1,
41+
},
42+
None => 1, // No build - can potentially start new build
43+
}
44+
}
45+
46+
fn get_status_priority(pr: &PullRequestModel) -> u32 {
47+
match &pr.auto_build {
48+
Some(build) => match build.status {
49+
BuildStatus::Success => 1,
50+
BuildStatus::Pending => 1, // Same as success since queue blocking is handled separately
51+
BuildStatus::Failure => 3,
52+
BuildStatus::Cancelled | BuildStatus::Timeouted => 2,
53+
},
54+
None => {
55+
if pr.is_approved() {
56+
1 // Approved but no build - should be prioritized
57+
} else {
58+
2 // No status
59+
}
60+
}
61+
}
62+
}
63+
64+
fn get_mergeable_priority(pr: &PullRequestModel) -> u32 {
65+
match pr.mergeable_state {
66+
MergeableState::Mergeable => 0,
67+
MergeableState::HasConflicts => 1,
68+
MergeableState::Unknown => 1,
69+
}
70+
}
71+
72+
fn get_rollup_priority(rollup: Option<&RollupMode>) -> u32 {
73+
match rollup {
74+
Some(RollupMode::Always) => 3,
75+
Some(RollupMode::Maybe) => 2,
76+
None => 2, // Default case
77+
Some(RollupMode::Iffy) => 1,
78+
Some(RollupMode::Never) => 0,
79+
}
80+
}

0 commit comments

Comments
 (0)