Merge branch 'stable-3.1' into stable-3.2

* stable-3.1:
  Add new documentation for the owners-autoassign plugin
  Allow async assignment of reviewers

Change-Id: I032a06624ff327952d667f786d52f3a5e9261a28
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
new file mode 100644
index 0000000..64607e6
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class AsyncReviewerManager implements ReviewerManager {
+
+  private static final Logger log = LoggerFactory.getLogger(AsyncReviewerManager.class);
+
+  private final SyncReviewerManager syncReviewerManager;
+  private final AutoAssignConfig config;
+  private final ScheduledExecutorService executor;
+  private final OneOffRequestContext requestContext;
+
+  class AddReviewersTask implements Runnable {
+    private final ChangeApi cApi;
+    private final Collection<Id> reviewers;
+    private final int changeNum;
+    private int retryNum;
+
+    public AddReviewersTask(ChangeApi cApi, Collection<Account.Id> reviewers)
+        throws RestApiException {
+      this.cApi = cApi;
+      this.changeNum = cApi.get()._number;
+      this.reviewers = reviewers;
+    }
+
+    @Override
+    public String toString() {
+      return "auto-assign reviewers to change "
+          + changeNum
+          + (retryNum > 0 ? "(retry #" + retryNum + ")" : "");
+    }
+
+    @Override
+    public void run() {
+      try (ManualRequestContext ctx = requestContext.open()) {
+        syncReviewerManager.addReviewers(cApi, reviewers);
+      } catch (Exception e) {
+        retryNum++;
+
+        if (retryNum > config.retryCount()) {
+          log.error("{} *FAILED*", this, e);
+        } else {
+          long retryInterval = config.retryInterval();
+          log.warn("{} *FAILED*: retrying after {} msec", this, retryInterval, e);
+          executor.schedule(this, retryInterval, TimeUnit.MILLISECONDS);
+        }
+      }
+    }
+  }
+
+  @Inject
+  public AsyncReviewerManager(
+      AutoAssignConfig config,
+      WorkQueue executorFactory,
+      SyncReviewerManager syncReviewerManager,
+      OneOffRequestContext requestContext) {
+    this.config = config;
+    this.syncReviewerManager = syncReviewerManager;
+    this.executor = executorFactory.createQueue(config.asyncThreads(), "AsyncReviewerManager");
+    this.requestContext = requestContext;
+  }
+
+  @Override
+  public void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
+      throws ReviewerManagerException {
+    try {
+      executor.schedule(
+          new AddReviewersTask(cApi, reviewers), config.asyncDelay(), TimeUnit.MILLISECONDS);
+    } catch (RestApiException e) {
+      throw new ReviewerManagerException(e);
+    }
+  }
+}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
new file mode 100644
index 0000000..32bdedc
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class AutoAssignConfig {
+  public static final String REVIEWERS_SECTION = "reviewers";
+  public static final String ASYNC = "async";
+  public static final boolean ASYNC_DEF = false;
+  public static final String DELAY = "delay";
+  public static final long DELAY_MSEC_DEF = 1500L;
+  public static final String RETRY_COUNT = "retryCount";
+  public static final int RETRY_COUNT_DEF = 2;
+  public static final String RETRY_INTERVAL = "retryInterval";
+  public static final long RETRY_INTERVAL_MSEC_DEF = DELAY_MSEC_DEF;
+  public static final String THREADS = "threads";
+  public static final int THREADS_DEF = 1;
+
+  private final boolean asyncReviewers;
+  private final int asyncThreads;
+  private final int retryCount;
+  private final long retryInterval;
+  private final long asyncDelay;
+
+  @Inject
+  AutoAssignConfig(PluginConfigFactory configFactory, @PluginName String pluginName) {
+    Config config = configFactory.getGlobalPluginConfig(pluginName);
+    asyncReviewers = config.getBoolean(REVIEWERS_SECTION, ASYNC, ASYNC_DEF);
+    asyncThreads = config.getInt(REVIEWERS_SECTION, THREADS, THREADS_DEF);
+    asyncDelay =
+        ConfigUtil.getTimeUnit(
+            config, REVIEWERS_SECTION, null, DELAY, DELAY_MSEC_DEF, MILLISECONDS);
+
+    retryCount = config.getInt(REVIEWERS_SECTION, RETRY_COUNT, RETRY_COUNT_DEF);
+    retryInterval =
+        ConfigUtil.getTimeUnit(
+            config, REVIEWERS_SECTION, null, RETRY_INTERVAL, asyncDelay, MILLISECONDS);
+  }
+
+  public boolean isAsyncReviewers() {
+    return asyncReviewers;
+  }
+
+  public int asyncThreads() {
+    return asyncThreads;
+  }
+
+  public int retryCount() {
+    return retryCount;
+  }
+
+  public long retryInterval() {
+    return retryInterval;
+  }
+
+  public long asyncDelay() {
+    return asyncDelay;
+  }
+}
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
index d6bdaef..0278c88 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignModule.java
@@ -19,10 +19,20 @@
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 
 public class AutoassignModule extends AbstractModule {
+  private final AutoAssignConfig config;
+
+  @Inject
+  AutoassignModule(AutoAssignConfig config) {
+    this.config = config;
+  }
+
   @Override
   protected void configure() {
+    bind(ReviewerManager.class)
+        .to(config.isAsyncReviewers() ? AsyncReviewerManager.class : SyncReviewerManager.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(GitRefListener.class);
   }
 }
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
index c3708b5..1347d33 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/ReviewerManager.java
@@ -1,5 +1,4 @@
-// Copyright (c) 2013 VMware, Inc. All Rights Reserved.
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2021 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,88 +16,11 @@
 package com.googlesource.gerrit.owners.common;
 
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.permissions.ChangePermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.util.ManualRequestContext;
-import com.google.gerrit.server.util.OneOffRequestContext;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.util.ArrayList;
 import java.util.Collection;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-@Singleton
-public class ReviewerManager {
-  private static final Logger log = LoggerFactory.getLogger(ReviewerManager.class);
+public interface ReviewerManager {
 
-  private final OneOffRequestContext requestContext;
-  private final GerritApi gApi;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeData.Factory changeDataFactory;
-  private final PermissionBackend permissionBackend;
-
-  @Inject
-  public ReviewerManager(
-      OneOffRequestContext requestContext,
-      GerritApi gApi,
-      IdentifiedUser.GenericFactory userFactory,
-      ChangeData.Factory changeDataFactory,
-      PermissionBackend permissionBackend) {
-    this.requestContext = requestContext;
-    this.gApi = gApi;
-    this.userFactory = userFactory;
-    this.changeDataFactory = changeDataFactory;
-    this.permissionBackend = permissionBackend;
-  }
-
-  public void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
-      throws ReviewerManagerException {
-    try {
-      ChangeInfo changeInfo = cApi.get();
-      try (ManualRequestContext ctx =
-          requestContext.openAs(Account.id(changeInfo.owner._accountId))) {
-        // TODO(davido): Switch back to using changes API again,
-        // when it supports batch mode for adding reviewers
-        ReviewInput in = new ReviewInput();
-        in.reviewers = new ArrayList<>(reviewers.size());
-        for (Account.Id account : reviewers) {
-          if (isVisibleTo(changeInfo, account)) {
-            AddReviewerInput addReviewerInput = new AddReviewerInput();
-            addReviewerInput.reviewer = account.toString();
-            in.reviewers.add(addReviewerInput);
-          } else {
-            log.warn(
-                "Not adding account {} as reviewer to change {} because the associated ref is not visible",
-                account,
-                changeInfo._number);
-          }
-        }
-        gApi.changes().id(changeInfo.id).current().review(in);
-      }
-    } catch (RestApiException e) {
-      log.error("Couldn't add reviewers to the change", e);
-      throw new ReviewerManagerException(e);
-    }
-  }
-
-  private boolean isVisibleTo(ChangeInfo changeInfo, Account.Id account) {
-    ChangeData changeData =
-        changeDataFactory.create(
-            Project.nameKey(changeInfo.project), Change.id(changeInfo._number));
-    return permissionBackend
-        .user(userFactory.create(account))
-        .change(changeData)
-        .testOrFalse(ChangePermission.READ);
-  }
+  void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
+      throws ReviewerManagerException;
 }
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
new file mode 100644
index 0000000..71c1ac2
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2013 VMware, Inc. All Rights Reserved.
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.owners.common;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Account.Id;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class SyncReviewerManager implements ReviewerManager {
+  private static final Logger log = LoggerFactory.getLogger(SyncReviewerManager.class);
+
+  private final OneOffRequestContext requestContext;
+  private final GerritApi gApi;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeData.Factory changeDataFactory;
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  public SyncReviewerManager(
+      OneOffRequestContext requestContext,
+      GerritApi gApi,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeData.Factory changeDataFactory,
+      PermissionBackend permissionBackend) {
+    this.requestContext = requestContext;
+    this.gApi = gApi;
+    this.userFactory = userFactory;
+    this.changeDataFactory = changeDataFactory;
+    this.permissionBackend = permissionBackend;
+  }
+
+  @Override
+  public void addReviewers(ChangeApi cApi, Collection<Account.Id> reviewers)
+      throws ReviewerManagerException {
+    try {
+      ChangeInfo changeInfo = cApi.get();
+      try (ManualRequestContext ctx =
+          requestContext.openAs(Account.id(changeInfo.owner._accountId))) {
+        // TODO(davido): Switch back to using changes API again,
+        // when it supports batch mode for adding reviewers
+        ReviewInput in = new ReviewInput();
+        in.reviewers = new ArrayList<>(reviewers.size());
+        for (Account.Id account : reviewers) {
+          if (isVisibleTo(changeInfo, account)) {
+            AddReviewerInput addReviewerInput = new AddReviewerInput();
+            addReviewerInput.reviewer = account.toString();
+            in.reviewers.add(addReviewerInput);
+          } else {
+            log.warn(
+                "Not adding account {} as reviewer to change {} because the associated ref is not visible",
+                account,
+                changeInfo._number);
+          }
+        }
+        gApi.changes().id(changeInfo.id).current().review(in);
+      }
+    } catch (RestApiException e) {
+      log.error("Couldn't add reviewers to the change", e);
+      throw new ReviewerManagerException(e);
+    }
+  }
+
+  private boolean isVisibleTo(ChangeInfo changeInfo, Id account) {
+    ChangeData changeData =
+        changeDataFactory.create(
+            Project.nameKey(changeInfo.project), Change.id(changeInfo._number));
+    return permissionBackend
+        .user(userFactory.create(account))
+        .change(changeData)
+        .testOrFalse(ChangePermission.READ);
+  }
+}
diff --git a/owners-autoassign/src/main/resources/Documentation/auto-assign.md b/owners-autoassign/src/main/resources/Documentation/auto-assign.md
new file mode 100644
index 0000000..506a176
--- /dev/null
+++ b/owners-autoassign/src/main/resources/Documentation/auto-assign.md
@@ -0,0 +1,49 @@
+## Reviewers auto-assign configuration
+
+The OWNERS file is processed by the @PLUGIN@ for automatically
+assigning all relevant owners to a change for every new patch-set
+uploaded.
+
+The way that the reviewers are added is controlled by the
+$GERRIT_SITE/etc/@PLUGIN@.config file.
+
+By default, all reviewers are added synchronously when a patch-set
+is uploaded. However, you may want to delay the assignment of additional
+reviewers to a later stage for lowering the pressure on the Git
+repository associated with concurrent updates.
+
+For example, the following configuration would delay the assignment of
+reviewers by 5 seconds:
+
+```
+[reviewers]
+  async = true
+  delay = 5 sec
+```
+
+See below the full list of configuration settings available:
+
+- `reviewers.async`: assign reviewers asynchronously. When set to `false`, all
+  the other settings in @PLUGIN@.config are ignored. By default, set to `false`.
+
+- `reviewers.delay`: delay of the assignment of reviewers since the upload
+  of a new patch-set, expressed in <number> <unit>. By default, set to `0`.
+
+  Values should use common unit suffixes to express their setting:
+
+  - ms, milliseconds
+
+  - s, sec, second, seconds
+
+  - m, min, minute, minutes
+
+   - h, hr, hour, hours
+
+- `reviewers.retryCount`: number of retries for attempting to assign reviewers
+  to a change. By default, set to `2`.
+
+- `reviewers.retryInterval`: delay between retries. Expressed in the same format
+  of the `reviewers.delay`. By default, set to the same value of `reviewers.delay`.
+
+- `reviewers.threads`: maximum concurrency of threads for assigning reviewers to
+  changes. By default, set to 1.
\ No newline at end of file
diff --git a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
similarity index 96%
rename from owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerIT.java
rename to owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
index 1e0f8ec..18dea29 100644
--- a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerIT.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerIT.java
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.vmware.gerrit.owners.common;
+package com.googlesource.gerrit.owners.common;
 
 import static org.junit.Assert.assertEquals;
 
@@ -29,12 +29,14 @@
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import java.util.stream.StreamSupport;
+import com.googlesource.gerrit.owners.common.ReviewerManager;
+
 import org.eclipse.jgit.transport.ReceiveCommand.Type;
 import org.junit.Test;
 
 @TestPlugin(
     name = "owners-autoassign",
-    sysModule = "com.vmware.gerrit.owners.common.GitRefListenerIT$TestModule")
+    sysModule = "com.googlesource.gerrit.owners.common.GitRefListenerIT$TestModule")
 public class GitRefListenerIT extends LightweightPluginDaemonTest {
 
   @Inject DynamicSet<GitReferenceUpdatedListener> allRefUpdateListeners;
diff --git a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerTest.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
similarity index 95%
rename from owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerTest.java
rename to owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
index 75a9189..fef3f29 100644
--- a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/GitRefListenerTest.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/GitRefListenerTest.java
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.vmware.gerrit.owners.common;
+package com.googlesource.gerrit.owners.common;
 
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.extensions.api.GerritApi;
@@ -43,7 +43,7 @@
       PatchListCache patchListCache,
       GitRepositoryManager repositoryManager,
       Accounts accounts,
-      ReviewerManager reviewerManager,
+      SyncReviewerManager reviewerManager,
       OneOffRequestContext oneOffReqCtx,
       Provider<CurrentUser> currentUserProvider,
       ChangeNotes.Factory notesFactory) {
diff --git a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/ReferenceUpdatedEventTest.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReferenceUpdatedEventTest.java
similarity index 97%
rename from owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/ReferenceUpdatedEventTest.java
rename to owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReferenceUpdatedEventTest.java
index 7024543..61a485f 100644
--- a/owners-autoassign/src/test/java/com/vmware/gerrit/owners/common/ReferenceUpdatedEventTest.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/ReferenceUpdatedEventTest.java
@@ -13,7 +13,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.vmware.gerrit.owners.common;
+package com.googlesource.gerrit.owners.common;
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;