blob: a848c6c38e1922b54e1afed195ba8fe1f6c73494 [file] [log] [blame]
// Copyright (C) 2016 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.google.gerrit.server;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
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.change.PostReviewers;
import com.google.gerrit.server.change.SuggestReviewers;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.QueryResult;
import com.google.gerrit.server.query.account.AccountQueryBuilder;
import com.google.gerrit.server.query.account.AccountQueryProcessor;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class ReviewersUtil {
@Singleton
private static class Metrics {
final Timer0 queryAccountsLatency;
final Timer0 recommendAccountsLatency;
final Timer0 loadAccountsLatency;
final Timer0 queryGroupsLatency;
@Inject
Metrics(MetricMaker metricMaker) {
queryAccountsLatency = metricMaker.newTimer(
"reviewer_suggestion/query_accounts",
new Description(
"Latency for querying accounts for reviewer suggestion")
.setCumulative()
.setUnit(Units.MILLISECONDS));
recommendAccountsLatency = metricMaker.newTimer(
"reviewer_suggestion/recommend_accounts",
new Description(
"Latency for recommending accounts for reviewer suggestion")
.setCumulative()
.setUnit(Units.MILLISECONDS));
loadAccountsLatency = metricMaker.newTimer(
"reviewer_suggestion/load_accounts",
new Description(
"Latency for loading accounts for reviewer suggestion")
.setCumulative()
.setUnit(Units.MILLISECONDS));
queryGroupsLatency = metricMaker.newTimer(
"reviewer_suggestion/query_groups",
new Description(
"Latency for querying groups for reviewer suggestion")
.setCumulative()
.setUnit(Units.MILLISECONDS));
}
}
// Generate a candidate list at 3x the size of what the user wants to see to
// give the ranking algorithm a good set of candidates it can work with
private static final int CANDIDATE_LIST_MULTIPLIER = 3;
private final AccountLoader accountLoader;
private final AccountQueryBuilder accountQueryBuilder;
private final AccountQueryProcessor accountQueryProcessor;
private final GroupBackend groupBackend;
private final GroupMembers.Factory groupMembersFactory;
private final Provider<CurrentUser> currentUser;
private final ReviewerRecommender reviewerRecommender;
private final Metrics metrics;
@Inject
ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
AccountQueryBuilder accountQueryBuilder,
AccountQueryProcessor accountQueryProcessor,
GroupBackend groupBackend,
GroupMembers.Factory groupMembersFactory,
Provider<CurrentUser> currentUser,
ReviewerRecommender reviewerRecommender,
Metrics metrics) {
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
this.accountLoader = accountLoaderFactory.create(fillOptions);
this.accountQueryBuilder = accountQueryBuilder;
this.accountQueryProcessor = accountQueryProcessor;
this.currentUser = currentUser;
this.groupBackend = groupBackend;
this.groupMembersFactory = groupMembersFactory;
this.reviewerRecommender = reviewerRecommender;
this.metrics = metrics;
}
public interface VisibilityControl {
boolean isVisibleTo(Account.Id account) throws OrmException;
}
public List<SuggestedReviewerInfo> suggestReviewers(ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, ProjectControl projectControl,
VisibilityControl visibilityControl, boolean excludeGroups)
throws IOException, OrmException {
String query = suggestReviewers.getQuery();
int limit = suggestReviewers.getLimit();
if (!suggestReviewers.getSuggestAccounts()) {
return Collections.emptyList();
}
List<Account.Id> candidateList = new ArrayList<>();
if (!Strings.isNullOrEmpty(query)) {
candidateList = suggestAccounts(suggestReviewers, visibilityControl);
}
List<Account.Id> sortedRecommendations = recommendAccounts(changeNotes,
suggestReviewers, projectControl, candidateList);
List<SuggestedReviewerInfo> suggestedReviewer =
loadAccounts(sortedRecommendations);
if (!excludeGroups && suggestedReviewer.size() < limit
&& !Strings.isNullOrEmpty(query)) {
// Add groups at the end as individual accounts are usually more
// important.
suggestedReviewer.addAll(suggestAccountGroups(suggestReviewers,
projectControl, visibilityControl, limit - suggestedReviewer.size()));
}
if (suggestedReviewer.size() <= limit) {
return suggestedReviewer;
}
return suggestedReviewer.subList(0, limit);
}
private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers,
VisibilityControl visibilityControl) throws OrmException {
try (Timer0.Context ctx = metrics.queryAccountsLatency.start()) {
try {
Set<Account.Id> matches = new HashSet<>();
QueryResult<AccountState> result = accountQueryProcessor
.setLimit(suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER)
.query(accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
for (AccountState accountState : result.entities()) {
Account.Id id = accountState.getAccount().getId();
if (visibilityControl.isVisibleTo(id)) {
matches.add(id);
}
}
return new ArrayList<>(matches);
} catch (QueryParseException e) {
return ImmutableList.of();
}
}
}
private List<Account.Id> recommendAccounts(ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, ProjectControl projectControl,
List<Account.Id> candidateList) throws OrmException {
try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) {
return reviewerRecommender.suggestReviewers(changeNotes, suggestReviewers,
projectControl, candidateList);
}
}
private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
throws OrmException {
try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
List<SuggestedReviewerInfo> reviewer = accountIds.stream()
.map(accountLoader::get)
.filter(Objects::nonNull)
.map(a -> {
SuggestedReviewerInfo info = new SuggestedReviewerInfo();
info.account = a;
info.count = 1;
return info;
}).collect(toList());
accountLoader.fill();
return reviewer;
}
}
private List<SuggestedReviewerInfo> suggestAccountGroups(
SuggestReviewers suggestReviewers, ProjectControl projectControl,
VisibilityControl visibilityControl, int limit)
throws OrmException, IOException {
try (Timer0.Context ctx = metrics.queryGroupsLatency.start()) {
List<SuggestedReviewerInfo> groups = new ArrayList<>();
for (GroupReference g : suggestAccountGroups(suggestReviewers,
projectControl)) {
GroupAsReviewer result = suggestGroupAsReviewer(suggestReviewers,
projectControl.getProject(), g, visibilityControl);
if (result.allowed || result.allowedWithConfirmation) {
GroupBaseInfo info = new GroupBaseInfo();
info.id = Url.encode(g.getUUID().get());
info.name = g.getName();
SuggestedReviewerInfo suggestedReviewerInfo =
new SuggestedReviewerInfo();
suggestedReviewerInfo.group = info;
suggestedReviewerInfo.count = result.size;
if (result.allowedWithConfirmation) {
suggestedReviewerInfo.confirm = true;
}
groups.add(suggestedReviewerInfo);
if (groups.size() >= limit) {
break;
}
}
}
return groups;
}
}
private List<GroupReference> suggestAccountGroups(
SuggestReviewers suggestReviewers, ProjectControl ctl) {
return Lists.newArrayList(
Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl),
suggestReviewers.getLimit()));
}
private static class GroupAsReviewer {
boolean allowed;
boolean allowedWithConfirmation;
int size;
}
private GroupAsReviewer suggestGroupAsReviewer(
SuggestReviewers suggestReviewers,
Project project, GroupReference group,
VisibilityControl visibilityControl) throws OrmException, IOException {
GroupAsReviewer result = new GroupAsReviewer();
int maxAllowed = suggestReviewers.getMaxAllowed();
int maxAllowedWithoutConfirmation =
suggestReviewers.getMaxAllowedWithoutConfirmation();
if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
return result;
}
try {
Set<Account> members = groupMembersFactory
.create(currentUser.get())
.listAccounts(group.getUUID(), project.getNameKey());
if (members.isEmpty()) {
return result;
}
result.size = members.size();
if (maxAllowed > 0 && result.size > maxAllowed) {
return result;
}
boolean needsConfirmation = result.size > maxAllowedWithoutConfirmation;
// require that at least one member in the group can see the change
for (Account account : members) {
if (visibilityControl.isVisibleTo(account.getId())) {
if (needsConfirmation) {
result.allowedWithConfirmation = true;
} else {
result.allowed = true;
}
return result;
}
}
} catch (NoSuchGroupException e) {
return result;
} catch (NoSuchProjectException e) {
return result;
}
return result;
}
}