20
20
//! `assign.owners` config, it will auto-select an assignee based on the files
21
21
//! the PR modifies.
22
22
23
+ use crate :: db:: review_prefs:: get_review_prefs_batch;
24
+ use crate :: github:: UserId ;
25
+ use crate :: handlers:: pr_tracking:: ReviewerWorkqueue ;
23
26
use crate :: {
24
27
config:: AssignConfig ,
25
28
github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
@@ -33,6 +36,8 @@ use rand::seq::IteratorRandom;
33
36
use rust_team_data:: v1:: Teams ;
34
37
use std:: collections:: { HashMap , HashSet } ;
35
38
use std:: fmt;
39
+ use std:: sync:: Arc ;
40
+ use tokio:: sync:: RwLock ;
36
41
use tokio_postgres:: Client as DbClient ;
37
42
use tracing as log;
38
43
@@ -299,7 +304,16 @@ async fn determine_assignee(
299
304
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
300
305
if let Some ( name) = assign_command {
301
306
// User included `r?` in the opening PR body.
302
- match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , & [ name] ) . await {
307
+ match find_reviewer_from_names (
308
+ & db_client,
309
+ ctx. workqueue . clone ( ) ,
310
+ & teams,
311
+ config,
312
+ & event. issue ,
313
+ & [ name] ,
314
+ )
315
+ . await
316
+ {
303
317
Ok ( assignee) => return Ok ( ( Some ( assignee) , true ) ) ,
304
318
Err ( e) => {
305
319
event
@@ -313,8 +327,15 @@ async fn determine_assignee(
313
327
// Errors fall-through to try fallback group.
314
328
match find_reviewers_from_diff ( config, diff) {
315
329
Ok ( candidates) if !candidates. is_empty ( ) => {
316
- match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , & candidates)
317
- . await
330
+ match find_reviewer_from_names (
331
+ & db_client,
332
+ ctx. workqueue . clone ( ) ,
333
+ & teams,
334
+ config,
335
+ & event. issue ,
336
+ & candidates,
337
+ )
338
+ . await
318
339
{
319
340
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
320
341
Err ( FindReviewerError :: TeamNotFound ( team) ) => log:: warn!(
@@ -326,7 +347,9 @@ async fn determine_assignee(
326
347
e @ FindReviewerError :: NoReviewer { .. }
327
348
| e @ FindReviewerError :: ReviewerIsPrAuthor { .. }
328
349
| e @ FindReviewerError :: ReviewerAlreadyAssigned { .. }
329
- | e @ FindReviewerError :: ReviewerOnVacation { .. } ,
350
+ | e @ FindReviewerError :: ReviewerOnVacation { .. }
351
+ | e @ FindReviewerError :: DatabaseError ( _)
352
+ | e @ FindReviewerError :: ReviewerAtMaxCapacity { .. } ,
330
353
) => log:: trace!(
331
354
"no reviewer could be determined for PR {}: {e}" ,
332
355
event. issue. global_id( )
@@ -344,7 +367,16 @@ async fn determine_assignee(
344
367
}
345
368
346
369
if let Some ( fallback) = config. adhoc_groups . get ( "fallback" ) {
347
- match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , fallback) . await {
370
+ match find_reviewer_from_names (
371
+ & db_client,
372
+ ctx. workqueue . clone ( ) ,
373
+ & teams,
374
+ config,
375
+ & event. issue ,
376
+ fallback,
377
+ )
378
+ . await
379
+ {
348
380
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
349
381
Err ( e) => {
350
382
log:: trace!(
@@ -522,6 +554,7 @@ pub(super) async fn handle_command(
522
554
let db_client = ctx. db . get ( ) . await ;
523
555
let assignee = match find_reviewer_from_names (
524
556
& db_client,
557
+ ctx. workqueue . clone ( ) ,
525
558
& teams,
526
559
config,
527
560
issue,
@@ -646,6 +679,10 @@ enum FindReviewerError {
646
679
ReviewerIsPrAuthor { username : String } ,
647
680
/// Requested reviewer is already assigned to that PR
648
681
ReviewerAlreadyAssigned { username : String } ,
682
+ /// Data required for assignment could not be loaded from the DB.
683
+ DatabaseError ( String ) ,
684
+ /// The reviewer has too many PRs alreayd assigned.
685
+ ReviewerAtMaxCapacity { username : String } ,
649
686
}
650
687
651
688
impl std:: error:: Error for FindReviewerError { }
@@ -688,6 +725,17 @@ impl fmt::Display for FindReviewerError {
688
725
REVIEWER_ALREADY_ASSIGNED . replace( "{username}" , username)
689
726
)
690
727
}
728
+ FindReviewerError :: DatabaseError ( error) => {
729
+ write ! ( f, "Database error: {error}" )
730
+ }
731
+ FindReviewerError :: ReviewerAtMaxCapacity { username } => {
732
+ write ! (
733
+ f,
734
+ r"`{username}` has too many PRs assigned to them.
735
+
736
+ Please select a different reviewer." ,
737
+ )
738
+ }
691
739
}
692
740
}
693
741
}
@@ -699,7 +747,8 @@ impl fmt::Display for FindReviewerError {
699
747
/// auto-assign groups, or rust-lang team names. It must have at least one
700
748
/// entry.
701
749
async fn find_reviewer_from_names (
702
- _db : & DbClient ,
750
+ db : & DbClient ,
751
+ workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
703
752
teams : & Teams ,
704
753
config : & AssignConfig ,
705
754
issue : & Issue ,
@@ -712,7 +761,10 @@ async fn find_reviewer_from_names(
712
761
}
713
762
}
714
763
715
- let candidates = candidate_reviewers_from_names ( teams, config, issue, names) ?;
764
+ let candidates =
765
+ candidate_reviewers_from_names ( db, workqueue, teams, config, issue, names) . await ?;
766
+ assert ! ( !candidates. is_empty( ) ) ;
767
+
716
768
// This uses a relatively primitive random choice algorithm.
717
769
// GitHub's CODEOWNERS supports much more sophisticated options, such as:
718
770
//
@@ -816,19 +868,23 @@ fn expand_teams_and_groups(
816
868
817
869
/// Returns a list of candidate usernames (from relevant teams) to choose as a reviewer.
818
870
/// If not reviewer is available, returns an error.
819
- fn candidate_reviewers_from_names < ' a > (
871
+ async fn candidate_reviewers_from_names < ' a > (
872
+ db : & DbClient ,
873
+ workqueue : Arc < RwLock < ReviewerWorkqueue > > ,
820
874
teams : & ' a Teams ,
821
875
config : & ' a AssignConfig ,
822
876
issue : & Issue ,
823
877
names : & ' a [ String ] ,
824
878
) -> Result < HashSet < String > , FindReviewerError > {
879
+ // Step 1: expand teams and groups into candidate names
825
880
let ( expanded, expansion_happened) = expand_teams_and_groups ( teams, issue, config, names) ?;
826
881
let expanded_count = expanded. len ( ) ;
827
882
828
883
// Set of candidate usernames to choose from.
829
884
// We go through each expanded candidate and store either success or an error for them.
830
885
let mut candidates: Vec < Result < String , FindReviewerError > > = Vec :: new ( ) ;
831
886
887
+ // Step 2: pre-filter candidates based on checks that we can perform quickly
832
888
for candidate in expanded {
833
889
let name_lower = candidate. to_lowercase ( ) ;
834
890
let is_pr_author = name_lower == issue. user . login . to_lowercase ( ) ;
@@ -865,9 +921,50 @@ fn candidate_reviewers_from_names<'a>(
865
921
}
866
922
assert_eq ! ( candidates. len( ) , expanded_count) ;
867
923
868
- let valid_candidates: HashSet < String > = candidates
924
+ if config. review_prefs . is_some ( ) {
925
+ // Step 3: gather potential usernames to form a DB query for review preferences
926
+ let usernames: Vec < String > = candidates
927
+ . iter ( )
928
+ . filter_map ( |res| res. as_deref ( ) . ok ( ) . map ( |s| s. to_string ( ) ) )
929
+ . collect ( ) ;
930
+ let usernames: Vec < & str > = usernames. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
931
+ let review_prefs = get_review_prefs_batch ( db, & usernames)
932
+ . await
933
+ . context ( "cannot fetch review preferences" )
934
+ . map_err ( |e| FindReviewerError :: DatabaseError ( e. to_string ( ) ) ) ?;
935
+
936
+ let workqueue = workqueue. read ( ) . await ;
937
+
938
+ // Step 4: check review preferences
939
+ candidates = candidates
940
+ . into_iter ( )
941
+ . map ( |username| {
942
+ // Only consider candidates that did not have an earlier error
943
+ let username = username?;
944
+
945
+ // If no review prefs were found, we assume the default unlimited
946
+ // review capacity.
947
+ let Some ( review_prefs) = review_prefs. get ( username. as_str ( ) ) else {
948
+ return Ok ( username) ;
949
+ } ;
950
+ let Some ( capacity) = review_prefs. max_assigned_prs else {
951
+ return Ok ( username) ;
952
+ } ;
953
+ let assigned_prs = workqueue. assigned_pr_count ( review_prefs. user_id as UserId ) ;
954
+ // Can we assign one more PR?
955
+ if ( assigned_prs as i32 ) < capacity {
956
+ Ok ( username)
957
+ } else {
958
+ Err ( FindReviewerError :: ReviewerAtMaxCapacity { username } )
959
+ }
960
+ } )
961
+ . collect ( ) ;
962
+ }
963
+ assert_eq ! ( candidates. len( ) , expanded_count) ;
964
+
965
+ let valid_candidates: HashSet < & str > = candidates
869
966
. iter ( )
870
- . filter_map ( |res| res. as_ref ( ) . ok ( ) . cloned ( ) )
967
+ . filter_map ( |res| res. as_deref ( ) . ok ( ) )
871
968
. collect ( ) ;
872
969
873
970
if valid_candidates. is_empty ( ) {
@@ -894,6 +991,9 @@ fn candidate_reviewers_from_names<'a>(
894
991
} )
895
992
}
896
993
} else {
897
- Ok ( valid_candidates)
994
+ Ok ( valid_candidates
995
+ . into_iter ( )
996
+ . map ( |s| s. to_string ( ) )
997
+ . collect ( ) )
898
998
}
899
999
}
0 commit comments