blob: 403e52631e603e2b193230b92d3e85e3c691f56d [file] [log] [blame]
// 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.google.gerrit.server.project;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.metrics.Counter1;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.StorageConstraint;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A utility class for different operations related to {@link
* com.google.gerrit.entities.SubmitRequirement}s.
*/
@Singleton
public class SubmitRequirementsUtil {
/**
* Submit requirement name can only contain alphanumeric characters or hyphen. Name cannot start
* with a hyphen or number.
*/
private static final Pattern SUBMIT_REQ_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9\\-]*");
@Singleton
static class Metrics {
final Counter1<String> submitRequirementsMatchingWithLegacy;
final Counter1<String> submitRequirementsMismatchingWithLegacy;
final Counter1<String> legacyNotInSrs;
final Counter1<String> srsNotInLegacy;
@Inject
Metrics(MetricMaker metricMaker) {
submitRequirementsMatchingWithLegacy =
metricMaker.newCounter(
"change/submit_requirements/matching_with_legacy",
new Description(
"Total number of times there was a legacy and non-legacy "
+ "submit requirements with the same name for a change, "
+ "and the evaluation of both requirements had the same result "
+ "w.r.t. change submittability.")
.setRate()
.setUnit("count"),
Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
.description("Submit requirement name")
.build());
submitRequirementsMismatchingWithLegacy =
metricMaker.newCounter(
"change/submit_requirements/mismatching_with_legacy",
new Description(
"Total number of times there was a legacy and non-legacy "
+ "submit requirements with the same name for a change, "
+ "and the evaluation of both requirements had a different result "
+ "w.r.t. change submittability.")
.setRate()
.setUnit("count"),
Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
.description("Submit requirement name")
.build());
legacyNotInSrs =
metricMaker.newCounter(
"change/submit_requirements/legacy_not_in_srs",
new Description(
"Total number of times there was a legacy submit requirement result "
+ "but not a project config requirement with the same name for a change.")
.setRate()
.setUnit("count"),
Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
.description("Submit requirement name")
.build());
srsNotInLegacy =
metricMaker.newCounter(
"change/submit_requirements/srs_not_in_legacy",
new Description(
"Total number of times there was a project config submit requirement "
+ "result but not a legacy requirement with the same name for a change.")
.setRate()
.setUnit("count"),
Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
.description("Submit requirement name")
.build());
}
}
private final Metrics metrics;
@Inject
public SubmitRequirementsUtil(Metrics metrics) {
this.metrics = metrics;
}
/**
* Merge legacy and non-legacy submit requirement results. If both input maps have submit
* requirements with the same name and fulfillment status (according to {@link
* SubmitRequirementResult#fulfilled()}), we eliminate the entry from the {@code
* legacyRequirements} input map and only include the one from the {@code
* projectConfigRequirements} in the result.
*
* @param projectConfigRequirements map of {@link SubmitRequirement} to {@link
* SubmitRequirementResult} containing results for submit requirements stored in the
* project.config.
* @param legacyRequirements map of {@link SubmitRequirement} to {@link SubmitRequirementResult}
* containing the results of converting legacy submit records to submit requirements.
* @return a map that is the result of merging both input maps, while eliminating requirements
* with the same name and status.
*/
public ImmutableMap<SubmitRequirement, SubmitRequirementResult>
mergeLegacyAndNonLegacyRequirements(
Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements,
ChangeData cd) {
// Cannot use ImmutableMap.Builder here since entries in the map may be overridden.
Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
result.putAll(projectConfigRequirements);
Map<String, SubmitRequirementResult> requirementsByName =
projectConfigRequirements.entrySet().stream()
// filter out legacy entries as a safety guard for duplicate entries
// (projectConfigRequirements should not contain legacy entries)
// TODO(ghareeb): remove the filter statement
.filter(entry -> !entry.getValue().isLegacy())
.collect(
Collectors.toMap(
sr -> sr.getKey().name().toLowerCase(Locale.US), sr -> sr.getValue()));
for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
legacyRequirements.entrySet()) {
String srName = legacy.getKey().name().toLowerCase(Locale.US);
SubmitRequirementResult projectConfigResult = requirementsByName.get(srName);
SubmitRequirementResult legacyResult = legacy.getValue();
// If there's no project config requirement with the same name as the legacy requirement
// then add the legacy SR to the result. There is no mismatch in results in this case.
if (projectConfigResult == null) {
result.put(legacy.getKey(), legacy.getValue());
if (shouldReportMetric(cd)) {
metrics.legacyNotInSrs.increment(srName);
}
continue;
}
if (matchByStatus(projectConfigResult, legacyResult)) {
// There exists a project config SR with the same name as the legacy SR, and they are
// matching in result. No need to include the legacy SR in the output since the project
// config SR is already there.
if (shouldReportMetric(cd)) {
metrics.submitRequirementsMatchingWithLegacy.increment(srName);
}
continue;
}
// There exists a project config SR with the same name as the legacy SR but they are not
// matching in their result. Increment the mismatch count and add the legacy SR to the result.
if (shouldReportMetric(cd)) {
metrics.submitRequirementsMismatchingWithLegacy.increment(srName);
}
result.put(legacy.getKey(), legacy.getValue());
}
Set<String> legacyNames =
legacyRequirements.keySet().stream()
.map(SubmitRequirement::name)
.map(String::toLowerCase)
.collect(Collectors.toSet());
for (String projectConfigSrName : requirementsByName.keySet()) {
if (!legacyNames.contains(projectConfigSrName) && shouldReportMetric(cd)) {
metrics.srsNotInLegacy.increment(projectConfigSrName);
}
}
return ImmutableMap.copyOf(result);
}
/** Validates the name of submit requirements. */
public static void validateName(@Nullable String name) throws IllegalArgumentException {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Empty submit requirement name");
}
if (!SUBMIT_REQ_NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException(
String.format(
"Illegal submit requirement name \"%s\". Name can only consist of "
+ "alphanumeric characters and '-'. Name cannot start with '-' or number.",
name));
}
}
private static boolean shouldReportMetric(ChangeData cd) {
// We only care about recording differences in old and new requirements for open changes
// that did not have their data retrieved from the (potentially stale) change index.
return cd.change().isNew() && cd.getStorageConstraint() == StorageConstraint.NOTEDB_ONLY;
}
/** Returns true if both input results are equal in allowing/disallowing change submission. */
private static boolean matchByStatus(SubmitRequirementResult r1, SubmitRequirementResult r2) {
return r1.fulfilled() == r2.fulfilled();
}
}