Don't suggest service users as reviewers

Service users usually decide based on their own criteria which changes
they are acting on and don't react on review invitations sent by email.
Add a new setting suggest.skipServiceUsers to gate this behavior.

Bug: Issue 14117
Change-Id: If0202fffda8ce173f2317d61c4f5387e18868736
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index c362710..9d7e93b 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5140,6 +5140,13 @@
 +
 By default 50.
 
+[[suggest.skipServiceUsers]]suggest.skipServiceUsers::
++
+If link:access-control.html#service_users[service users] should be skipped when
+suggesting reviewers.
++
+By default true.
+
 [[tracing]]
 === Section tracing
 
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 2a55e41..38be27e 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.account.ServiceUserClassifier;
 import com.google.gerrit.server.change.ReviewerAdder;
 import com.google.gerrit.server.index.account.AccountField;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
@@ -130,6 +131,7 @@
   private final IndexConfig indexConfig;
   private final AccountControl.Factory accountControlFactory;
   private final Provider<CurrentUser> self;
+  private final ServiceUserClassifier serviceUserClassifier;
 
   @Inject
   ReviewersUtil(
@@ -143,7 +145,8 @@
       AccountIndexCollection accountIndexes,
       IndexConfig indexConfig,
       AccountControl.Factory accountControlFactory,
-      Provider<CurrentUser> self) {
+      Provider<CurrentUser> self,
+      ServiceUserClassifier serviceUserClassifier) {
     this.accountLoaderFactory = accountLoaderFactory;
     this.accountQueryBuilder = accountQueryBuilder;
     this.accountIndexRewriter = accountIndexRewriter;
@@ -155,6 +158,7 @@
     this.indexConfig = indexConfig;
     this.accountControlFactory = accountControlFactory;
     this.self = self;
+    this.serviceUserClassifier = serviceUserClassifier;
   }
 
   public interface VisibilityControl {
@@ -200,13 +204,17 @@
             reviewerState, changeNotes, suggestReviewers, projectState, candidateList);
     logger.atFine().log("Sorted recommendations: %s", sortedRecommendations);
 
-    // Filter accounts by visibility and enforce limit
+    // Filter accounts by visibility, skip service users and enforce limit
     List<Account.Id> filteredRecommendations = new ArrayList<>();
     try (Timer0.Context ctx = metrics.filterVisibility.start()) {
       for (Account.Id reviewer : sortedRecommendations) {
         if (filteredRecommendations.size() >= limit) {
           break;
         }
+        if (suggestReviewers.isSkipServiceUsers()
+            && serviceUserClassifier.isServiceUser(reviewer)) {
+          continue;
+        }
         // Check if change is visible to reviewer and if the current user can see reviewer
         if (visibilityControl.isVisibleTo(reviewer)
             && accountControlFactory.get().canSee(reviewer)) {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index e071c89..71ff493 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -31,6 +31,8 @@
 
   private static final int DEFAULT_MAX_SUGGESTED = 10;
 
+  private static final boolean DEFAULT_SKIP_SERVICE_USERS = true;
+
   protected final ReviewersUtil reviewersUtil;
 
   private final boolean suggestAccounts;
@@ -39,6 +41,7 @@
   protected int limit;
   protected String query;
   protected final int maxSuggestedReviewers;
+  protected boolean skipServiceUsers;
 
   @Option(
       name = "--limit",
@@ -78,6 +81,10 @@
     return maxAllowedWithoutConfirmation;
   }
 
+  public boolean isSkipServiceUsers() {
+    return skipServiceUsers;
+  }
+
   @Inject
   public SuggestReviewers(
       AccountVisibility av, @GerritServerConfig Config cfg, ReviewersUtil reviewersUtil) {
@@ -100,6 +107,9 @@
             ReviewerAdder.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
 
     logger.atFine().log("AccountVisibility: %s", av.name());
+
+    this.skipServiceUsers =
+        cfg.getBoolean("suggest", "skipServiceUsers", DEFAULT_SKIP_SERVICE_USERS);
   }
 
   public static GerritConfigListener configListener() {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 888878f..ffde622 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -450,6 +450,23 @@
   }
 
   @Test
+  public void suggestNoServiceAccounts() throws Exception {
+    requestScopeOperations.setApiUser(user.id());
+    String changeIdReviewed = createChangeFromApi();
+    String changeId = createChangeFromApi();
+
+    String name = name("foo");
+    TestAccount foo = accountCreator.create(name);
+    reviewChange(changeIdReviewed, foo);
+
+    assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo), ImmutableList.of());
+
+    groupOperations.group(serviceUsersUUID()).forUpdate().addMember(foo.id()).update();
+
+    assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(), ImmutableList.of());
+  }
+
+  @Test
   public void suggestNoExistingReviewers() throws Exception {
     requestScopeOperations.setApiUser(user.id());
     String changeId = createChangeFromApi();
@@ -608,6 +625,13 @@
     return user(name, fullName, name);
   }
 
+  private AccountGroup.UUID serviceUsersUUID() {
+    return groupCache
+        .get(AccountGroup.nameKey("Service Users"))
+        .orElseThrow(() -> new IllegalStateException("unable to find 'Service Users'"))
+        .getGroupUUID();
+  }
+
   private void reviewChange(String changeId, TestAccount reviewer) throws RestApiException {
     gApi.changes().id(changeId).addReviewer(reviewer.id().toString());
   }