Line data Source code
1 : // Copyright (C) 2021 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.project; 16 : 17 : import com.google.common.collect.ImmutableMap; 18 : import com.google.gerrit.common.Nullable; 19 : import com.google.gerrit.entities.SubmitRequirement; 20 : import com.google.gerrit.entities.SubmitRequirementResult; 21 : import com.google.gerrit.metrics.Counter1; 22 : import com.google.gerrit.metrics.Description; 23 : import com.google.gerrit.metrics.Field; 24 : import com.google.gerrit.metrics.MetricMaker; 25 : import com.google.gerrit.server.logging.Metadata; 26 : import com.google.gerrit.server.query.change.ChangeData; 27 : import com.google.gerrit.server.query.change.ChangeData.StorageConstraint; 28 : import com.google.inject.Inject; 29 : import com.google.inject.Singleton; 30 : import java.util.HashMap; 31 : import java.util.Map; 32 : import java.util.Set; 33 : import java.util.regex.Pattern; 34 : import java.util.stream.Collectors; 35 : 36 : /** 37 : * A utility class for different operations related to {@link 38 : * com.google.gerrit.entities.SubmitRequirement}s. 39 : */ 40 : @Singleton 41 : public class SubmitRequirementsUtil { 42 : 43 : /** 44 : * Submit requirement name can only contain alphanumeric characters or hyphen. Name cannot start 45 : * with a hyphen or number. 46 : */ 47 103 : private static final Pattern SUBMIT_REQ_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9\\-]*"); 48 : 49 : @Singleton 50 : static class Metrics { 51 : final Counter1<String> submitRequirementsMatchingWithLegacy; 52 : final Counter1<String> submitRequirementsMismatchingWithLegacy; 53 : final Counter1<String> legacyNotInSrs; 54 : final Counter1<String> srsNotInLegacy; 55 : 56 : @Inject 57 103 : Metrics(MetricMaker metricMaker) { 58 103 : submitRequirementsMatchingWithLegacy = 59 103 : metricMaker.newCounter( 60 : "change/submit_requirements/matching_with_legacy", 61 : new Description( 62 : "Total number of times there was a legacy and non-legacy " 63 : + "submit requirements with the same name for a change, " 64 : + "and the evaluation of both requirements had the same result " 65 : + "w.r.t. change submittability.") 66 103 : .setRate() 67 103 : .setUnit("count"), 68 103 : Field.ofString("sr_name", Metadata.Builder::submitRequirementName) 69 103 : .description("Submit requirement name") 70 103 : .build()); 71 103 : submitRequirementsMismatchingWithLegacy = 72 103 : metricMaker.newCounter( 73 : "change/submit_requirements/mismatching_with_legacy", 74 : new Description( 75 : "Total number of times there was a legacy and non-legacy " 76 : + "submit requirements with the same name for a change, " 77 : + "and the evaluation of both requirements had a different result " 78 : + "w.r.t. change submittability.") 79 103 : .setRate() 80 103 : .setUnit("count"), 81 103 : Field.ofString("sr_name", Metadata.Builder::submitRequirementName) 82 103 : .description("Submit requirement name") 83 103 : .build()); 84 103 : legacyNotInSrs = 85 103 : metricMaker.newCounter( 86 : "change/submit_requirements/legacy_not_in_srs", 87 : new Description( 88 : "Total number of times there was a legacy submit requirement result " 89 : + "but not a project config requirement with the same name for a change.") 90 103 : .setRate() 91 103 : .setUnit("count"), 92 103 : Field.ofString("sr_name", Metadata.Builder::submitRequirementName) 93 103 : .description("Submit requirement name") 94 103 : .build()); 95 103 : srsNotInLegacy = 96 103 : metricMaker.newCounter( 97 : "change/submit_requirements/srs_not_in_legacy", 98 : new Description( 99 : "Total number of times there was a project config submit requirement " 100 : + "result but not a legacy requirement with the same name for a change.") 101 103 : .setRate() 102 103 : .setUnit("count"), 103 103 : Field.ofString("sr_name", Metadata.Builder::submitRequirementName) 104 103 : .description("Submit requirement name") 105 103 : .build()); 106 103 : } 107 : } 108 : 109 : private final Metrics metrics; 110 : 111 : @Inject 112 103 : public SubmitRequirementsUtil(Metrics metrics) { 113 103 : this.metrics = metrics; 114 103 : } 115 : 116 : /** 117 : * Merge legacy and non-legacy submit requirement results. If both input maps have submit 118 : * requirements with the same name and fulfillment status (according to {@link 119 : * SubmitRequirementResult#fulfilled()}), we eliminate the entry from the {@code 120 : * legacyRequirements} input map and only include the one from the {@code 121 : * projectConfigRequirements} in the result. 122 : * 123 : * @param projectConfigRequirements map of {@link SubmitRequirement} to {@link 124 : * SubmitRequirementResult} containing results for submit requirements stored in the 125 : * project.config. 126 : * @param legacyRequirements map of {@link SubmitRequirement} to {@link SubmitRequirementResult} 127 : * containing the results of converting legacy submit records to submit requirements. 128 : * @return a map that is the result of merging both input maps, while eliminating requirements 129 : * with the same name and status. 130 : */ 131 : public ImmutableMap<SubmitRequirement, SubmitRequirementResult> 132 : mergeLegacyAndNonLegacyRequirements( 133 : Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements, 134 : Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements, 135 : ChangeData cd) { 136 : // Cannot use ImmutableMap.Builder here since entries in the map may be overridden. 137 103 : Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>(); 138 103 : result.putAll(projectConfigRequirements); 139 103 : Map<String, SubmitRequirementResult> requirementsByName = 140 103 : projectConfigRequirements.entrySet().stream() 141 : // filter out legacy entries as a safety guard for duplicate entries 142 : // (projectConfigRequirements should not contain legacy entries) 143 : // TODO(ghareeb): remove the filter statement 144 103 : .filter(entry -> !entry.getValue().isLegacy()) 145 103 : .collect(Collectors.toMap(sr -> sr.getKey().name().toLowerCase(), sr -> sr.getValue())); 146 : for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy : 147 103 : legacyRequirements.entrySet()) { 148 103 : String srName = legacy.getKey().name().toLowerCase(); 149 103 : SubmitRequirementResult projectConfigResult = requirementsByName.get(srName); 150 103 : SubmitRequirementResult legacyResult = legacy.getValue(); 151 : // If there's no project config requirement with the same name as the legacy requirement 152 : // then add the legacy SR to the result. There is no mismatch in results in this case. 153 103 : if (projectConfigResult == null) { 154 103 : result.put(legacy.getKey(), legacy.getValue()); 155 103 : if (shouldReportMetric(cd)) { 156 103 : metrics.legacyNotInSrs.increment(srName); 157 : } 158 : continue; 159 : } 160 2 : if (matchByStatus(projectConfigResult, legacyResult)) { 161 : // There exists a project config SR with the same name as the legacy SR, and they are 162 : // matching in result. No need to include the legacy SR in the output since the project 163 : // config SR is already there. 164 2 : if (shouldReportMetric(cd)) { 165 2 : metrics.submitRequirementsMatchingWithLegacy.increment(srName); 166 : } 167 : continue; 168 : } 169 : // There exists a project config SR with the same name as the legacy SR but they are not 170 : // matching in their result. Increment the mismatch count and add the legacy SR to the result. 171 2 : if (shouldReportMetric(cd)) { 172 1 : metrics.submitRequirementsMismatchingWithLegacy.increment(srName); 173 : } 174 2 : result.put(legacy.getKey(), legacy.getValue()); 175 2 : } 176 103 : Set<String> legacyNames = 177 103 : legacyRequirements.keySet().stream() 178 103 : .map(SubmitRequirement::name) 179 103 : .map(String::toLowerCase) 180 103 : .collect(Collectors.toSet()); 181 103 : for (String projectConfigSrName : requirementsByName.keySet()) { 182 2 : if (!legacyNames.contains(projectConfigSrName) && shouldReportMetric(cd)) { 183 1 : metrics.srsNotInLegacy.increment(projectConfigSrName); 184 : } 185 2 : } 186 : 187 103 : return ImmutableMap.copyOf(result); 188 : } 189 : 190 : /** Validates the name of submit requirements. */ 191 : public static void validateName(@Nullable String name) throws IllegalArgumentException { 192 4 : if (name == null || name.isEmpty()) { 193 0 : throw new IllegalArgumentException("Empty submit requirement name"); 194 : } 195 4 : if (!SUBMIT_REQ_NAME_PATTERN.matcher(name).matches()) { 196 2 : throw new IllegalArgumentException( 197 2 : String.format( 198 : "Illegal submit requirement name \"%s\". Name can only consist of " 199 : + "alphanumeric characters and '-'. Name cannot start with '-' or number.", 200 : name)); 201 : } 202 4 : } 203 : 204 : private static boolean shouldReportMetric(ChangeData cd) { 205 : // We only care about recording differences in old and new requirements for open changes 206 : // that did not have their data retrieved from the (potentially stale) change index. 207 103 : return cd.change().isNew() && cd.getStorageConstraint() == StorageConstraint.NOTEDB_ONLY; 208 : } 209 : 210 : /** Returns true if both input results are equal in allowing/disallowing change submission. */ 211 : private static boolean matchByStatus(SubmitRequirementResult r1, SubmitRequirementResult r2) { 212 2 : return r1.fulfilled() == r2.fulfilled(); 213 : } 214 : }