Merge branch 'stable-3.4'

* stable-3.4:
  Rename Autoassign to AutoAssign camel-case naming convention
  Add new documentation for the owners-autoassign plugin
  Allow async assignment of reviewers

Change-Id: Iff9b121a6483bc2c5744953371846af281907903
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..6ea35e4
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AsyncReviewerManager.java
@@ -0,0 +1,111 @@
+// 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.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+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 NameKey projectName;
+    private final ChangeApi cApi;
+    private final Collection<Id> reviewers;
+    private final int changeNum;
+    private int retryNum;
+
+    public AddReviewersTask(
+        Project.NameKey projectName, ChangeApi cApi, Collection<Account.Id> reviewers)
+        throws RestApiException {
+      this.projectName = projectName;
+      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(projectName, 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(
+      Project.NameKey projectName, ChangeApi cApi, Collection<Account.Id> reviewers)
+      throws ReviewerManagerException {
+    try {
+      executor.schedule(
+          new AddReviewersTask(projectName, 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..c381f19
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoAssignConfig.java
@@ -0,0 +1,142 @@
+// 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 com.google.gerrit.extensions.client.InheritableBoolean.TRUE;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_FIELD;
+import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+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;
+
+  private final PluginConfigFactory cfgFactory;
+  private final String pluginName;
+
+  private final PluginSettings settings;
+
+  @Inject
+  AutoAssignConfig(
+      PluginSettings settings, PluginConfigFactory configFactory, @PluginName String pluginName) {
+    this.settings = settings;
+    this.pluginName = pluginName;
+    this.cfgFactory = configFactory;
+    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);
+  }
+
+  @VisibleForTesting
+  public AutoAssignConfig() {
+    this.settings = null;
+    this.pluginName = "owners-autoassign";
+    cfgFactory = null;
+    Config config = new Config();
+    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;
+  }
+
+  private PluginConfig cfg(Project.NameKey projectKey) throws NoSuchProjectException {
+    return cfgFactory.getFromProjectConfigWithInheritance(projectKey, pluginName);
+  }
+
+  public boolean autoAssignWip(Project.NameKey projectKey) throws NoSuchProjectException {
+    return settings
+        .projectSpecificConfig(projectKey)
+        .getEnum(PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, TRUE)
+        .equals(TRUE);
+  }
+
+  public ReviewerState autoassignedReviewerState(Project.NameKey projectKey)
+      throws NoSuchProjectException {
+    return settings
+        .projectSpecificConfig(projectKey)
+        .getEnum(PROJECT_CONFIG_AUTOASSIGN_FIELD, ReviewerState.REVIEWER);
+  }
+
+  public boolean isBranchDisabled(String branch) {
+    return settings.isBranchDisabled(branch);
+  }
+
+  public boolean expandGroups() {
+    return settings.expandGroups();
+  }
+}
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
deleted file mode 100644
index 1082212..0000000
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/AutoassignConfig.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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 com.google.gerrit.extensions.client.InheritableBoolean.TRUE;
-import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_FIELD;
-import static com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES;
-
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-@Singleton
-public class AutoassignConfig {
-  private final PluginSettings config;
-
-  @Inject
-  AutoassignConfig(PluginSettings config) {
-    this.config = config;
-  }
-
-  public boolean autoAssignWip(Project.NameKey projectKey) throws NoSuchProjectException {
-    return config
-        .projectSpecificConfig(projectKey)
-        .getEnum(PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES, TRUE)
-        .equals(TRUE);
-  }
-
-  public ReviewerState autoassignedReviewerState(Project.NameKey projectKey)
-      throws NoSuchProjectException {
-    return config
-        .projectSpecificConfig(projectKey)
-        .getEnum(PROJECT_CONFIG_AUTOASSIGN_FIELD, ReviewerState.REVIEWER);
-  }
-
-  public boolean isBranchDisabled(String branch) {
-    return config.isBranchDisabled(branch);
-  }
-
-  public boolean expandGroups() {
-    return config.expandGroups();
-  }
-}
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 931c05a..80a03e0 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
@@ -28,19 +28,29 @@
 public class AutoassignModule extends AbstractModule {
 
   private final Class<? extends OwnersAttentionSet> ownersAttentionSetImpl;
+  private final AutoAssignConfig config;
+
+  @VisibleForTesting
+  AutoassignModule() {
+    this(DefaultAddAllOwnersToAttentionSet.class, new AutoAssignConfig());
+  }
 
   @Inject
-  public AutoassignModule() {
-    this(DefaultAddAllOwnersToAttentionSet.class);
+  AutoassignModule(AutoAssignConfig config) {
+    this(DefaultAddAllOwnersToAttentionSet.class, config);
   }
 
   @VisibleForTesting
-  public AutoassignModule(Class<? extends OwnersAttentionSet> ownersAttentionSetImpl) {
+  public AutoassignModule(
+      Class<? extends OwnersAttentionSet> ownersAttentionSetImpl, AutoAssignConfig config) {
     this.ownersAttentionSetImpl = ownersAttentionSetImpl;
+    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);
     DynamicItem.bind(binder(), OwnersAttentionSet.class)
         .to(ownersAttentionSetImpl)
diff --git a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
index 5be3aa0..faf5e42 100644
--- a/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/GitRefListener.java
@@ -87,7 +87,7 @@
 
   private ChangeNotes.Factory notesFactory;
 
-  private final AutoassignConfig cfg;
+  private final AutoAssignConfig cfg;
 
   @Inject
   public GitRefListener(
@@ -100,7 +100,7 @@
       OneOffRequestContext oneOffReqCtx,
       Provider<CurrentUser> currentUserProvider,
       ChangeNotes.Factory notesFactory,
-      AutoassignConfig cfg) {
+      AutoAssignConfig cfg) {
     this.api = api;
     this.patchListCache = patchListCache;
     this.projectCache = projectCache;
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 34d6e49..bf56e26 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,142 +16,14 @@
 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.entities.Project.NameKey;
-import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.extensions.api.changes.AttentionSetInput;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.extensions.api.changes.ReviewerInput;
-import com.google.gerrit.extensions.client.ReviewerState;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.registration.DynamicItem;
-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.project.NoSuchProjectException;
-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 com.googlesource.gerrit.owners.api.OwnersAttentionSet;
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-@Singleton
-public class ReviewerManager {
-  private static final Logger log = LoggerFactory.getLogger(ReviewerManager.class);
-
-  private final OneOffRequestContext requestContext;
-  private final GerritApi gApi;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeData.Factory changeDataFactory;
-  private final PermissionBackend permissionBackend;
-  private DynamicItem<OwnersAttentionSet> ownersForAttentionSet;
-
-  private final AutoassignConfig cfg;
-
-  @Inject
-  public ReviewerManager(
-      OneOffRequestContext requestContext,
-      GerritApi gApi,
-      IdentifiedUser.GenericFactory userFactory,
-      PermissionBackend permissionBackend,
-      ChangeData.Factory changeDataFactory,
-      DynamicItem<OwnersAttentionSet> ownersForAttentionSet,
-      AutoassignConfig cfg) {
-    this.requestContext = requestContext;
-    this.gApi = gApi;
-    this.userFactory = userFactory;
-    this.changeDataFactory = changeDataFactory;
-    this.permissionBackend = permissionBackend;
-    this.ownersForAttentionSet = ownersForAttentionSet;
-    this.cfg = cfg;
-  }
+public interface ReviewerManager {
 
   public void addReviewers(
       NameKey projectNameKey, ChangeApi cApi, Collection<Account.Id> accountsIds)
-      throws ReviewerManagerException, NoSuchProjectException {
-    try {
-      ChangeInfo changeInfo = cApi.get();
-      Set<Integer> currentReviewers =
-          changeInfo.reviewers.values().stream()
-              .flatMap(Collection::stream)
-              .map(ri -> ri._accountId)
-              .collect(Collectors.toSet());
-      ReviewerState reviewerState = cfg.autoassignedReviewerState(projectNameKey);
-      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<>(accountsIds.size());
-        Collection<Account.Id> validOwnersForAttentionSet = new ArrayList<>(accountsIds.size());
-        for (Account.Id account : accountsIds) {
-          if (!currentReviewers.contains(account.get()) && isVisibleTo(changeInfo, account)) {
-            ReviewerInput addReviewerInput = new ReviewerInput();
-            addReviewerInput.reviewer = account.toString();
-            addReviewerInput.state = reviewerState;
-            in.reviewers.add(addReviewerInput);
-
-            if (reviewerState == ReviewerState.REVIEWER) {
-              validOwnersForAttentionSet.add(account);
-            }
-          } else {
-            log.warn(
-                "Not adding account {} as reviewer to change {} because the associated ref is not"
-                    + " visible",
-                account,
-                changeInfo._number);
-          }
-        }
-
-        Collection<Account.Id> reviewersAccounts;
-        if (validOwnersForAttentionSet.isEmpty()) {
-          reviewersAccounts = Collections.emptyList();
-        } else {
-          reviewersAccounts =
-              Optional.ofNullable(ownersForAttentionSet)
-                  .map(DynamicItem::get)
-                  .filter(Objects::nonNull)
-                  .map(owners -> owners.addToAttentionSet(changeInfo, validOwnersForAttentionSet))
-                  .orElse(validOwnersForAttentionSet);
-        }
-
-        in.ignoreAutomaticAttentionSetRules = true;
-        in.addToAttentionSet =
-            ownersForAttentionSet.get().addToAttentionSet(changeInfo, reviewersAccounts).stream()
-                .map(
-                    (reviewer) ->
-                        new AttentionSetInput(
-                            reviewer.toString(), "Selected as member of the OWNERS file"))
-                .collect(Collectors.toList());
-
-        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);
-  }
+      throws ReviewerManagerException, NoSuchProjectException;
 }
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..83ba531
--- /dev/null
+++ b/owners-autoassign/src/main/java/com/googlesource/gerrit/owners/common/SyncReviewerManager.java
@@ -0,0 +1,165 @@
+// 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.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewerInput;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.registration.DynamicItem;
+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.project.NoSuchProjectException;
+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 com.googlesource.gerrit.owners.api.OwnersAttentionSet;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+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;
+
+  /**
+   * TODO: The optional injection here is needed for keeping backward compatibility with existing
+   * setups that do not have the owners-api.jar configured as Gerrit libModule.
+   *
+   * <p>Once merged to master, the optional injection can go and this can be moved as extra argument
+   * in the constructor.
+   */
+  @Inject(optional = true)
+  private DynamicItem<OwnersAttentionSet> ownersForAttentionSet;
+
+  private final AutoAssignConfig cfg;
+
+  @Inject
+  public SyncReviewerManager(
+      OneOffRequestContext requestContext,
+      GerritApi gApi,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeData.Factory changeDataFactory,
+      PermissionBackend permissionBackend,
+      AutoAssignConfig cfg) {
+    this.requestContext = requestContext;
+    this.gApi = gApi;
+    this.userFactory = userFactory;
+    this.changeDataFactory = changeDataFactory;
+    this.permissionBackend = permissionBackend;
+    this.cfg = cfg;
+  }
+
+  public void addReviewers(
+      NameKey projectNameKey, ChangeApi cApi, Collection<Account.Id> accountsIds)
+      throws ReviewerManagerException, NoSuchProjectException {
+    try {
+      ChangeInfo changeInfo = cApi.get();
+      Set<Integer> currentReviewers =
+          changeInfo.reviewers.values().stream()
+              .flatMap(Collection::stream)
+              .map(ri -> ri._accountId)
+              .collect(Collectors.toSet());
+      ReviewerState reviewerState = cfg.autoassignedReviewerState(projectNameKey);
+      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<>(accountsIds.size());
+        Collection<Account.Id> validOwnersForAttentionSet = new ArrayList<>(accountsIds.size());
+        for (Account.Id account : accountsIds) {
+          if (!currentReviewers.contains(account.get()) && isVisibleTo(changeInfo, account)) {
+            ReviewerInput addReviewerInput = new ReviewerInput();
+            addReviewerInput.reviewer = account.toString();
+            addReviewerInput.state = reviewerState;
+            in.reviewers.add(addReviewerInput);
+
+            if (reviewerState == ReviewerState.REVIEWER) {
+              validOwnersForAttentionSet.add(account);
+            }
+          } else {
+            log.warn(
+                "Not adding account {} as reviewer to change {} because the associated ref is not"
+                    + " visible",
+                account,
+                changeInfo._number);
+          }
+        }
+
+        Collection<Account.Id> reviewersAccounts;
+        if (validOwnersForAttentionSet.isEmpty()) {
+          reviewersAccounts = Collections.emptyList();
+        } else {
+          reviewersAccounts =
+              Optional.ofNullable(ownersForAttentionSet)
+                  .map(DynamicItem::get)
+                  .filter(Objects::nonNull)
+                  .map(owners -> owners.addToAttentionSet(changeInfo, validOwnersForAttentionSet))
+                  .orElse(validOwnersForAttentionSet);
+        }
+
+        in.ignoreAutomaticAttentionSetRules = true;
+        in.addToAttentionSet =
+            ownersForAttentionSet.get().addToAttentionSet(changeInfo, reviewersAccounts).stream()
+                .map(
+                    (reviewer) ->
+                        new AttentionSetInput(
+                            reviewer.toString(), "Selected as member of the OWNERS file"))
+                .collect(Collectors.toList());
+
+        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);
+  }
+}
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 97%
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 884c986..e5be7b5 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 com.googlesource.gerrit.owners.common.AutoassignConfigModule.PROJECT_CONFIG_AUTOASSIGN_WIP_CHANGES;
 import static org.junit.Assert.assertEquals;
@@ -41,12 +41,14 @@
 import java.util.HashMap;
 import java.util.Map;
 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 {
   private static final String PLUGIN_NAME = "owners-autoassign";
 
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 92%
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 bc093be..9cc6e96 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.entities.Project;
@@ -28,7 +28,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.owners.common.Accounts;
-import com.googlesource.gerrit.owners.common.AutoassignConfig;
+import com.googlesource.gerrit.owners.common.AutoAssignConfig;
 import com.googlesource.gerrit.owners.common.GitRefListener;
 import com.googlesource.gerrit.owners.common.ReviewerManager;
 import org.eclipse.jgit.lib.Repository;
@@ -46,11 +46,11 @@
       ProjectCache projectCache,
       GitRepositoryManager repositoryManager,
       Accounts accounts,
-      ReviewerManager reviewerManager,
+      SyncReviewerManager reviewerManager,
       OneOffRequestContext oneOffReqCtx,
       Provider<CurrentUser> currentUserProvider,
       ChangeNotes.Factory notesFactory,
-      AutoassignConfig cfg) {
+      AutoAssignConfig cfg) {
     super(
         api,
         patchListCache,
diff --git a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java
index 56cf33a..1cfcf04 100644
--- a/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java
+++ b/owners-autoassign/src/test/java/com/googlesource/gerrit/owners/common/OwnersAutoassignWithAttentionSetIT.java
@@ -56,7 +56,7 @@
   public static class TestModule extends AbstractModule {
     @Override
     protected void configure() {
-      install(new AutoassignModule(SelectFirstOwnerForAttentionSet.class));
+      install(new AutoassignModule(SelectFirstOwnerForAttentionSet.class, new AutoAssignConfig()));
     }
   }
 
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;