LCOV - code coverage report
Current view: top level - server/index/change - StalenessChecker.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 75 81 92.6 %
Date: 2022-11-19 15:00:39 Functions: 14 14 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2016 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.index.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : import static java.util.stream.Collectors.joining;
      20             : 
      21             : import com.google.auto.value.AutoValue;
      22             : import com.google.common.annotations.VisibleForTesting;
      23             : import com.google.common.base.Splitter;
      24             : import com.google.common.collect.ImmutableSet;
      25             : import com.google.common.collect.ListMultimap;
      26             : import com.google.common.collect.MultimapBuilder;
      27             : import com.google.common.collect.SetMultimap;
      28             : import com.google.common.collect.Sets;
      29             : import com.google.common.flogger.FluentLogger;
      30             : import com.google.gerrit.common.UsedAt;
      31             : import com.google.gerrit.entities.Change;
      32             : import com.google.gerrit.entities.Project;
      33             : import com.google.gerrit.extensions.restapi.Url;
      34             : import com.google.gerrit.index.IndexConfig;
      35             : import com.google.gerrit.index.RefState;
      36             : import com.google.gerrit.server.git.GitRepositoryManager;
      37             : import com.google.gerrit.server.index.StalenessCheckResult;
      38             : import com.google.gerrit.server.query.change.ChangeData;
      39             : import com.google.inject.Inject;
      40             : import com.google.inject.Singleton;
      41             : import java.io.IOException;
      42             : import java.util.List;
      43             : import java.util.Optional;
      44             : import java.util.Set;
      45             : import java.util.regex.Pattern;
      46             : import org.eclipse.jgit.lib.Ref;
      47             : import org.eclipse.jgit.lib.Repository;
      48             : 
      49             : /**
      50             :  * Checker that compares values stored in the change index to metadata in NoteDb to detect index
      51             :  * documents that should have been updated (= stale).
      52             :  */
      53             : @Singleton
      54             : public class StalenessChecker {
      55         148 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      56             : 
      57         148 :   public static final ImmutableSet<String> FIELDS =
      58         148 :       ImmutableSet.of(
      59         148 :           ChangeField.CHANGE.getName(),
      60         148 :           ChangeField.REF_STATE.getName(),
      61         148 :           ChangeField.REF_STATE_PATTERN.getName());
      62             : 
      63             :   private final ChangeIndexCollection indexes;
      64             :   private final GitRepositoryManager repoManager;
      65             :   private final IndexConfig indexConfig;
      66             : 
      67             :   @Inject
      68             :   StalenessChecker(
      69         148 :       ChangeIndexCollection indexes, GitRepositoryManager repoManager, IndexConfig indexConfig) {
      70         148 :     this.indexes = indexes;
      71         148 :     this.repoManager = repoManager;
      72         148 :     this.indexConfig = indexConfig;
      73         148 :   }
      74             : 
      75             :   /**
      76             :    * Returns a {@link StalenessCheckResult} with structured information about staleness of the
      77             :    * provided {@link com.google.gerrit.entities.Change.Id}.
      78             :    */
      79             :   public StalenessCheckResult check(Change.Id id) {
      80           4 :     ChangeIndex i = indexes.getSearchIndex();
      81           4 :     if (i == null) {
      82           0 :       return StalenessCheckResult
      83           0 :           .notStale(); // No index; caller couldn't do anything if it is stale.
      84             :     }
      85           4 :     if (!i.getSchema().hasField(ChangeField.REF_STATE)
      86           4 :         || !i.getSchema().hasField(ChangeField.REF_STATE_PATTERN)) {
      87           0 :       return StalenessCheckResult.notStale(); // Index version not new enough for this check.
      88             :     }
      89             : 
      90           4 :     Optional<ChangeData> result =
      91           4 :         i.get(id, IndexedChangeQuery.createOptions(indexConfig, 0, 1, FIELDS));
      92           4 :     if (!result.isPresent()) {
      93           0 :       return StalenessCheckResult.stale("Document %s missing from index", id);
      94             :     }
      95           4 :     ChangeData cd = result.get();
      96           4 :     return check(repoManager, id, cd.getRefStates(), parsePatterns(cd));
      97             :   }
      98             : 
      99             :   /**
     100             :    * Returns a {@link StalenessCheckResult} with structured information about staleness of the
     101             :    * provided change.
     102             :    */
     103             :   @UsedAt(UsedAt.Project.GOOGLE)
     104             :   public static StalenessCheckResult check(
     105             :       GitRepositoryManager repoManager,
     106             :       Change.Id id,
     107             :       SetMultimap<Project.NameKey, RefState> states,
     108             :       ListMultimap<Project.NameKey, RefStatePattern> patterns) {
     109           4 :     return refsAreStale(repoManager, id, states, patterns);
     110             :   }
     111             : 
     112             :   @VisibleForTesting
     113             :   static StalenessCheckResult refsAreStale(
     114             :       GitRepositoryManager repoManager,
     115             :       Change.Id id,
     116             :       SetMultimap<Project.NameKey, RefState> states,
     117             :       ListMultimap<Project.NameKey, RefStatePattern> patterns) {
     118           5 :     Set<Project.NameKey> projects = Sets.union(states.keySet(), patterns.keySet());
     119             : 
     120           5 :     for (Project.NameKey p : projects) {
     121           5 :       StalenessCheckResult result = refsAreStale(repoManager, id, p, states, patterns);
     122           5 :       if (result.isStale()) {
     123           5 :         return result;
     124             :       }
     125           5 :     }
     126             : 
     127           5 :     return StalenessCheckResult.notStale();
     128             :   }
     129             : 
     130             :   private ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(ChangeData cd) {
     131           4 :     return parsePatterns(cd.getRefStatePatterns());
     132             :   }
     133             : 
     134             :   /**
     135             :    * Returns a map containing the parsed version of {@link RefStatePattern}. See {@link
     136             :    * RefStatePattern}.
     137             :    */
     138             :   public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(
     139             :       Iterable<byte[]> patterns) {
     140           5 :     RefStatePattern.check(patterns != null, null);
     141             :     ListMultimap<Project.NameKey, RefStatePattern> result =
     142           5 :         MultimapBuilder.hashKeys().arrayListValues().build();
     143           5 :     for (byte[] b : patterns) {
     144           5 :       RefStatePattern.check(b != null, null);
     145           5 :       String s = new String(b, UTF_8);
     146           5 :       List<String> parts = Splitter.on(':').splitToList(s);
     147           5 :       RefStatePattern.check(parts.size() == 2, s);
     148           5 :       result.put(Project.nameKey(Url.decode(parts.get(0))), RefStatePattern.create(parts.get(1)));
     149           5 :     }
     150           5 :     return result;
     151             :   }
     152             : 
     153             :   private static StalenessCheckResult refsAreStale(
     154             :       GitRepositoryManager repoManager,
     155             :       Change.Id id,
     156             :       Project.NameKey project,
     157             :       SetMultimap<Project.NameKey, RefState> allStates,
     158             :       ListMultimap<Project.NameKey, RefStatePattern> allPatterns) {
     159           5 :     try (Repository repo = repoManager.openRepository(project)) {
     160           5 :       Set<RefState> states = allStates.get(project);
     161           5 :       for (RefState state : states) {
     162           5 :         if (!state.match(repo)) {
     163           5 :           return StalenessCheckResult.stale(
     164             :               "Ref states don't match for document %s (%s != %s)",
     165           5 :               id, state, repo.exactRef(state.ref()));
     166             :         }
     167           5 :       }
     168           5 :       for (RefStatePattern pattern : allPatterns.get(project)) {
     169           5 :         if (!pattern.match(repo, states)) {
     170           1 :           return StalenessCheckResult.stale(
     171             :               "Ref patterns don't match for document %s. Pattern: %s States: %s",
     172             :               id, pattern, states);
     173             :         }
     174           5 :       }
     175           5 :       return StalenessCheckResult.notStale();
     176           5 :     } catch (IOException e) {
     177           0 :       logger.atWarning().withCause(e).log("error checking staleness of %s in %s", id, project);
     178           0 :       return StalenessCheckResult.stale("Exceptions while processing document %s", e.getMessage());
     179             :     }
     180             :   }
     181             : 
     182             :   /**
     183             :    * Pattern for matching refs.
     184             :    *
     185             :    * <p>Similar to '*' syntax for native Git refspecs, but slightly more powerful: the pattern may
     186             :    * contain arbitrarily many asterisks. There must be at least one '*' and the first one must
     187             :    * immediately follow a '/'.
     188             :    */
     189             :   @AutoValue
     190         103 :   public abstract static class RefStatePattern {
     191             :     static RefStatePattern create(String pattern) {
     192         103 :       int star = pattern.indexOf('*');
     193         103 :       check(star > 0 && pattern.charAt(star - 1) == '/', pattern);
     194         103 :       String prefix = pattern.substring(0, star);
     195         103 :       check(Repository.isValidRefName(pattern.replace('*', 'x')), pattern);
     196             : 
     197             :       // Quote everything except the '*'s, which become ".*".
     198         103 :       String regex =
     199         103 :           Splitter.on('*')
     200         103 :               .splitToStream(pattern)
     201         103 :               .map(Pattern::quote)
     202         103 :               .collect(joining(".*", "^", "$"));
     203         103 :       return new AutoValue_StalenessChecker_RefStatePattern(
     204         103 :           pattern, prefix, Pattern.compile(regex));
     205             :     }
     206             : 
     207             :     byte[] toByteArray(Project.NameKey project) {
     208         103 :       return (project.toString() + ':' + pattern()).getBytes(UTF_8);
     209             :     }
     210             : 
     211             :     private static void check(boolean condition, String str) {
     212         103 :       checkArgument(condition, "invalid RefStatePattern: %s", str);
     213         103 :     }
     214             : 
     215             :     abstract String pattern();
     216             : 
     217             :     abstract String prefix();
     218             : 
     219             :     abstract Pattern regex();
     220             : 
     221             :     boolean match(String refName) {
     222           5 :       return regex().matcher(refName).find();
     223             :     }
     224             : 
     225             :     private boolean match(Repository repo, Set<RefState> expected) throws IOException {
     226           5 :       for (Ref r : repo.getRefDatabase().getRefsByPrefix(prefix())) {
     227           5 :         if (!match(r.getName())) {
     228           1 :           continue;
     229             :         }
     230           5 :         if (!expected.contains(RefState.of(r))) {
     231           1 :           return false;
     232             :         }
     233           5 :       }
     234           5 :       return true;
     235             :     }
     236             :   }
     237             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750