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 : }
|