Store in index the state of all refs that contribute to a change

The primary storage (ReviewDb or NoteDb) and the secondary index
(Lucene, etc.) are not updated atomically, so they can get out of sync
in various ways. For example, the server may crash before writing to
the index, or index updates for concurrent primary storage writes may
happen out of order. We want to be able to detect when this has
happened.

One component of the state of a change is the row version field in the
ReviewDb entity; this is already stored in the index in the serialized
Change entity. The remainder of a change's state comprises various
refs:

 * NoteDb refs, if NoteDb is the primary storage:
    * Change meta ref.
    * Draft comment refs.
    * Robot comment ref.
 * Edit refs in the project repo.
 * Star refs in All-Users.

Store each of these in the index in a field REF_STATE, with the simple
format "<project>:<ref>:<sha>".

In addition to storing the refs that *are* present, we also need to
keep track of which refs are *not* present. For example, if only one
user is listed in the EDITBY field, then there must exist an edit ref
for that user and only that user, and no other users. This is
represented by storing a list of "<project>:<pattern>" values in the
REF_STATE_PATTERN field. A change is up to date only if all refs in
each <project> matching each <pattern> are also listed explicitly in
REF_STATE. The pattern format is a slightly extended git refspec
pattern which allows arbitrarily many '*'s, to deal with the tricky
case of "refs/users/UU/UUUU/edit-C/P" => "refs/users/*/edit-C/*".

This change implements the REF_STATE and REF_STATE_PATTERN change
fields, which require some additional ChangeData methods, as well as
structured data types for these fields. These data types live in a new
class StalenessChecker, which will be later extended to use them to
check for staleness.

Change-Id: I622dbbb334b5aca4d58f770ad453e034a5bc9529
This commit is contained in:
Dave Borowitz
2016-11-22 08:49:53 -05:00
parent 47fee2a156
commit 7d0e67c6ec
10 changed files with 509 additions and 22 deletions

View File

@@ -23,6 +23,7 @@ import static org.apache.commons.codec.binary.Base64.decodeBase64;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties; import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
@@ -398,6 +399,21 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(), ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd); ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
if (source.get(ChangeField.REF_STATE.getName()) != null) {
JsonArray refStates =
source.get(ChangeField.REF_STATE.getName()).getAsJsonArray();
cd.setRefStates(
Iterables.transform(
refStates, e -> Base64.decodeBase64(e.getAsString())));
}
if (source.get(ChangeField.REF_STATE_PATTERN.getName()) != null) {
JsonArray refStatePatterns = source.get(
ChangeField.REF_STATE_PATTERN.getName()).getAsJsonArray();
cd.setRefStatePatterns(
Iterables.transform(
refStatePatterns, e -> Base64.decodeBase64(e.getAsString())));
}
return cd; return cd;
} }

View File

@@ -22,6 +22,8 @@ import static com.google.gerrit.server.index.change.ChangeField.PROJECT;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES; import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES; import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
@@ -120,6 +122,9 @@ public class LuceneChangeIndex implements ChangeIndex {
private static final String DELETED_FIELD = ChangeField.DELETED.getName(); private static final String DELETED_FIELD = ChangeField.DELETED.getName();
private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName(); private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName(); private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName();
private static final String REF_STATE_PATTERN_FIELD =
ChangeField.REF_STATE_PATTERN.getName();
private static final String REVIEWEDBY_FIELD = private static final String REVIEWEDBY_FIELD =
ChangeField.REVIEWEDBY.getName(); ChangeField.REVIEWEDBY.getName();
private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName(); private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName();
@@ -472,6 +477,12 @@ public class LuceneChangeIndex implements ChangeIndex {
ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd); ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD, decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD,
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd); ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
if (fields.contains(REF_STATE_FIELD)) {
decodeRefStates(doc, cd);
}
if (fields.contains(REF_STATE_PATTERN_FIELD)) {
decodeRefStatePatterns(doc, cd);
}
return cd; return cd;
} }
@@ -572,6 +583,16 @@ public class LuceneChangeIndex implements ChangeIndex {
opts, cd); opts, cd);
} }
private void decodeRefStates(Multimap<String, IndexableField> doc,
ChangeData cd) {
cd.setRefStates(copyAsBytes(doc.get(REF_STATE_FIELD)));
}
private void decodeRefStatePatterns(Multimap<String, IndexableField> doc,
ChangeData cd) {
cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD)));
}
private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc, private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
String fieldName, ProtobufCodec<T> codec) { String fieldName, ProtobufCodec<T> codec) {
Collection<IndexableField> fields = doc.get(fieldName); Collection<IndexableField> fields = doc.get(fieldName);
@@ -586,4 +607,16 @@ public class LuceneChangeIndex implements ChangeIndex {
} }
return result; return result;
} }
private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
return fields.stream()
.map(
f -> {
BytesRef ref = f.binaryValue();
byte[] b = new byte[ref.length];
System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length);
return b;
})
.collect(toList());
}
} }

View File

@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.config.TrackingFooters;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@@ -65,14 +66,17 @@ public abstract class FieldDef<I, T> {
public static class FillArgs { public static class FillArgs {
public final TrackingFooters trackingFooters; public final TrackingFooters trackingFooters;
public final boolean allowsDrafts; public final boolean allowsDrafts;
public final AllUsersName allUsers;
@Inject @Inject
FillArgs(TrackingFooters trackingFooters, FillArgs(TrackingFooters trackingFooters,
@GerritServerConfig Config cfg) { @GerritServerConfig Config cfg,
AllUsersName allUsers) {
this.trackingFooters = trackingFooters; this.trackingFooters = trackingFooters;
this.allowsDrafts = cfg == null this.allowsDrafts = cfg == null
? true ? true
: cfg.getBoolean("change", "allowDrafts", true); : cfg.getBoolean("change", "allowDrafts", true);
this.allUsers = allUsers;
} }
} }

View File

@@ -36,13 +36,20 @@ import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil; import com.google.gerrit.server.index.SchemaUtil;
import com.google.gerrit.server.index.change.StalenessChecker.RefState;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.notedb.RobotCommentNotes;
import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder; import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -945,6 +952,74 @@ public class ChangeField {
return result; return result;
} }
/**
* All values of all refs that were used in the course of indexing this
* document.
* <p>
* Emitted as UTF-8 encoded strings of the form
* {@code project:ref/name:[hex sha]}.
*/
public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
new FieldDef.Repeatable<ChangeData, byte[]>(
"ref_state", FieldType.STORED_ONLY, true) {
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
List<byte[]> result = new ArrayList<>();
Project.NameKey project = input.change().getProject();
input.editRefs().values().forEach(
r -> result.add(RefState.of(r).toByteArray(project)));
input.starRefs().values().forEach(
r -> result.add(RefState.of(r.ref()).toByteArray(project)));
if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
ChangeNotes notes = input.notes();
result.add(RefState.create(notes.getRefName(), notes.getMetaId())
.toByteArray(project));
notes.getRobotComments(); // Force loading robot comments.
RobotCommentNotes robotNotes = notes.getRobotCommentNotes();
result.add(
RefState.create(robotNotes.getRefName(), robotNotes.getMetaId())
.toByteArray(project));
input.draftRefs().values().forEach(
r -> result.add(RefState.of(r).toByteArray(args.allUsers)));
}
return result;
} };
/**
* All ref wildcard patterns that were used in the course of indexing this
* document.
* <p>
* Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}.
* See {@link RefStatePattern} for the pattern format.
*/
public static final FieldDef<ChangeData, Iterable<byte[]>>
REF_STATE_PATTERN = new FieldDef.Repeatable<ChangeData, byte[]>(
"ref_state_pattern", FieldType.STORED_ONLY, true) {
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
Change.Id id = input.getId();
Project.NameKey project = input.change().getProject();
List<byte[]> result = new ArrayList<>(3);
result.add(RefStatePattern.create(
RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
.toByteArray(project));
if (PrimaryStorage.of(input.change()) == PrimaryStorage.NOTE_DB) {
result.add(
RefStatePattern.create(RefNames.refsStarredChangesPrefix(id) + "*")
.toByteArray(args.allUsers));
result.add(
RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
.toByteArray(args.allUsers));
}
return result;
}
};
public static final Integer NOT_REVIEWED = -1; public static final Integer NOT_REVIEWED = -1;
private static String getTopic(ChangeData input) throws OrmException { private static String getTopic(ChangeData input) throws OrmException {

View File

@@ -73,12 +73,18 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
.add(ChangeField.LABEL2) .add(ChangeField.LABEL2)
.build(); .build();
@Deprecated
static final Schema<ChangeData> V35 = static final Schema<ChangeData> V35 =
schema(V34, schema(V34,
ChangeField.SUBMIT_RECORD, ChangeField.SUBMIT_RECORD,
ChangeField.STORED_SUBMIT_RECORD_LENIENT, ChangeField.STORED_SUBMIT_RECORD_LENIENT,
ChangeField.STORED_SUBMIT_RECORD_STRICT); ChangeField.STORED_SUBMIT_RECORD_STRICT);
static final Schema<ChangeData> V36 =
schema(V35,
ChangeField.REF_STATE,
ChangeField.REF_STATE_PATTERN);
public static final String NAME = "changes"; public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = public static final ChangeSchemaDefinitions INSTANCE =
new ChangeSchemaDefinitions(); new ChangeSchemaDefinitions();

View File

@@ -0,0 +1,151 @@
// 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.index.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.SetMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Project;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.StreamSupport;
public class StalenessChecker {
public static SetMultimap<Project.NameKey, RefState> parseStates(
Iterable<byte[]> states) {
RefState.check(states != null, null);
SetMultimap<Project.NameKey, RefState> result = HashMultimap.create();
for (byte[] b : states) {
RefState.check(b != null, null);
String s = new String(b, UTF_8);
List<String> parts = Splitter.on(':').splitToList(s);
RefState.check(
parts.size() == 3
&& !parts.get(0).isEmpty()
&& !parts.get(1).isEmpty(),
s);
result.put(
new Project.NameKey(parts.get(0)),
RefState.create(parts.get(1), parts.get(2)));
}
return result;
}
public static ListMultimap<Project.NameKey, RefStatePattern> parsePatterns(
Iterable<byte[]> patterns) {
RefStatePattern.check(patterns != null, null);
ListMultimap<Project.NameKey, RefStatePattern> result =
ArrayListMultimap.create();
for (byte[] b : patterns) {
RefStatePattern.check(b != null, null);
String s = new String(b, UTF_8);
List<String> parts = Splitter.on(':').splitToList(s);
RefStatePattern.check(parts.size() == 2, s);
result.put(
new Project.NameKey(parts.get(0)),
RefStatePattern.create(parts.get(1)));
}
return result;
}
@AutoValue
public abstract static class RefState {
static RefState create(String ref, String sha) {
return new AutoValue_StalenessChecker_RefState(
ref, ObjectId.fromString(sha));
}
static RefState create(String ref, @Nullable ObjectId id) {
return new AutoValue_StalenessChecker_RefState(
ref, firstNonNull(id, ObjectId.zeroId()));
}
static RefState of(Ref ref) {
return new AutoValue_StalenessChecker_RefState(
ref.getName(), ref.getObjectId());
}
byte[] toByteArray(Project.NameKey project) {
byte[] a = (project.toString() + ':' + ref() + ':').getBytes(UTF_8);
byte[] b = new byte[a.length + Constants.OBJECT_ID_STRING_LENGTH];
System.arraycopy(a, 0, b, 0, a.length);
id().copyTo(b, a.length);
return b;
}
private static void check(boolean condition, String str) {
checkArgument(condition, "invalid RefState: %s", str);
}
abstract String ref();
abstract ObjectId id();
}
/**
* Pattern for matching refs.
* <p>
* Similar to '*' syntax for native Git refspecs, but slightly more powerful:
* the pattern may contain arbitrarily many asterisks. There must be at least
* one '*' and the first one must immediately follow a '/'.
*/
@AutoValue
public abstract static class RefStatePattern {
static RefStatePattern create(String pattern) {
int star = pattern.indexOf('*');
check(star > 0 && pattern.charAt(star - 1) == '/', pattern);
String prefix = pattern.substring(0, star);
check(Repository.isValidRefName(pattern.replace('*', 'x')), pattern);
// Quote everything except the '*'s, which become ".*".
String regex =
StreamSupport.stream(Splitter.on('*').split(pattern).spliterator(), false)
.map(Pattern::quote)
.collect(joining(".*", "^", "$"));
return new AutoValue_StalenessChecker_RefStatePattern(
pattern, prefix, Pattern.compile(regex));
}
byte[] toByteArray(Project.NameKey project) {
return (project.toString() + ':' + pattern()).getBytes(UTF_8);
}
private static void check(boolean condition, String str) {
checkArgument(condition, "invalid RefStatePattern: %s", str);
}
abstract String pattern();
abstract String prefix();
abstract Pattern regex();
boolean match(String refName) {
return regex().matcher(refName).find();
}
}
}

View File

@@ -372,6 +372,10 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
return change; return change;
} }
public ObjectId getMetaId() {
return state.metaId();
}
public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() { public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() {
if (patchSets == null) { if (patchSets == null) {
ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b =
@@ -510,7 +514,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
return draftCommentNotes; return draftCommentNotes;
} }
RobotCommentNotes getRobotCommentNotes() { public RobotCommentNotes getRobotCommentNotes() {
return robotCommentNotes; return robotCommentNotes;
} }
@@ -532,7 +536,7 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> {
} }
@Override @Override
protected String getRefName() { public String getRefName() {
return changeMetaRef(getChangeId()); return changeMetaRef(getChangeId());
} }

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.notedb;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
@@ -43,6 +44,7 @@ public class RobotCommentNotes extends AbstractChangeNotes<RobotCommentNotes> {
private ImmutableListMultimap<RevId, RobotComment> comments; private ImmutableListMultimap<RevId, RobotComment> comments;
private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap; private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
private ObjectId metaId;
@AssistedInject @AssistedInject
RobotCommentNotes( RobotCommentNotes(
@@ -70,20 +72,26 @@ public class RobotCommentNotes extends AbstractChangeNotes<RobotCommentNotes> {
} }
@Override @Override
protected String getRefName() { public String getRefName() {
return RefNames.robotCommentsRef(getChangeId()); return RefNames.robotCommentsRef(getChangeId());
} }
@Nullable
public ObjectId getMetaId() {
return metaId;
}
@Override @Override
protected void onLoad(LoadHandle handle) protected void onLoad(LoadHandle handle)
throws IOException, ConfigInvalidException { throws IOException, ConfigInvalidException {
ObjectId rev = handle.id(); metaId = handle.id();
if (rev == null) { if (metaId == null) {
loadDefaults(); loadDefaults();
return; return;
} }
metaId = metaId.copy();
RevCommit tipCommit = handle.walk().parseCommit(rev); RevCommit tipCommit = handle.walk().parseCommit(metaId);
ObjectReader reader = handle.walk().getObjectReader(); ObjectReader reader = handle.walk().getObjectReader();
revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader, revisionNoteMap = RevisionNoteMap.parseRobotComments(args.noteUtil, reader,
NoteMap.read(reader, tipCommit)); NoteMap.read(reader, tipCommit));

View File

@@ -89,7 +89,6 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -348,9 +347,9 @@ public class ChangeData {
private SubmitTypeRecord submitTypeRecord; private SubmitTypeRecord submitTypeRecord;
private Boolean mergeable; private Boolean mergeable;
private Set<String> hashtags; private Set<String> hashtags;
private Set<Account.Id> editsByUser; private Map<Account.Id, Ref> editsByUser;
private Set<Account.Id> reviewedBy; private Set<Account.Id> reviewedBy;
private Set<Account.Id> draftsByUser; private Map<Account.Id, Ref> draftsByUser;
@Deprecated @Deprecated
private Set<Account.Id> starredByUser; private Set<Account.Id> starredByUser;
private ImmutableMultimap<Account.Id, String> stars; private ImmutableMultimap<Account.Id, String> stars;
@@ -360,6 +359,9 @@ public class ChangeData {
private PersonIdent author; private PersonIdent author;
private PersonIdent committer; private PersonIdent committer;
private ImmutableList<byte[]> refStates;
private ImmutableList<byte[]> refStatePatterns;
@AssistedInject @AssistedInject
private ChangeData( private ChangeData(
GitRepositoryManager repoManager, GitRepositoryManager repoManager,
@@ -1098,21 +1100,25 @@ public class ChangeData {
} }
public Set<Account.Id> editsByUser() throws OrmException { public Set<Account.Id> editsByUser() throws OrmException {
return editRefs().keySet();
}
public Map<Account.Id, Ref> editRefs() throws OrmException {
if (editsByUser == null) { if (editsByUser == null) {
if (!lazyLoad) { if (!lazyLoad) {
return Collections.emptySet(); return Collections.emptyMap();
} }
Change c = change(); Change c = change();
if (c == null) { if (c == null) {
return Collections.emptySet(); return Collections.emptyMap();
} }
editsByUser = new HashSet<>(); editsByUser = new HashMap<>();
Change.Id id = checkNotNull(change.getId()); Change.Id id = checkNotNull(change.getId());
try (Repository repo = repoManager.openRepository(project())) { try (Repository repo = repoManager.openRepository(project())) {
for (String ref for (Map.Entry<String, Ref> e
: repo.getRefDatabase().getRefs(RefNames.REFS_USERS).keySet()) { : repo.getRefDatabase().getRefs(RefNames.REFS_USERS).entrySet()) {
if (id.equals(Change.Id.fromEditRefPart(ref))) { if (id.equals(Change.Id.fromEditRefPart(e.getKey()))) {
editsByUser.add(Account.Id.fromRefPart(ref)); editsByUser.put(Account.Id.fromRefPart(e.getKey()), e.getValue());
} }
} }
} catch (IOException e) { } catch (IOException e) {
@@ -1123,17 +1129,31 @@ public class ChangeData {
} }
public Set<Account.Id> draftsByUser() throws OrmException { public Set<Account.Id> draftsByUser() throws OrmException {
return draftRefs().keySet();
}
public Map<Account.Id, Ref> draftRefs() throws OrmException {
if (draftsByUser == null) { if (draftsByUser == null) {
if (!lazyLoad) { if (!lazyLoad) {
return Collections.emptySet(); return Collections.emptyMap();
} }
Change c = change(); Change c = change();
if (c == null) { if (c == null) {
return Collections.emptySet(); return Collections.emptyMap();
} }
draftsByUser = new HashSet<>();
for (Comment sc : commentsUtil.draftByChange(db, notes())) { draftsByUser = new HashMap<>();
draftsByUser.add(sc.author.getId()); if (notesMigration.readChanges()) {
for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
Account.Id account = Account.Id.fromRefSuffix(ref.getName());
if (account != null) {
draftsByUser.put(account, ref);
}
}
} else {
for (Comment sc : commentsUtil.draftByChange(db, notes())) {
draftsByUser.put(sc.author.getId(), null);
}
} }
} }
return draftsByUser; return draftsByUser;
@@ -1262,4 +1282,20 @@ public class ChangeData {
this.deletions = deletions; this.deletions = deletions;
} }
} }
public ImmutableList<byte[]> getRefStates() {
return refStates;
}
public void setRefStates(Iterable<byte[]> refStates) {
this.refStates = ImmutableList.copyOf(refStates);
}
public ImmutableList<byte[]> getRefStatePatterns() {
return refStatePatterns;
}
public void setRefStatePatterns(Iterable<byte[]> refStatePatterns) {
this.refStatePatterns = ImmutableList.copyOf(refStatePatterns);
}
} }

View File

@@ -0,0 +1,154 @@
// 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.index.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assert_;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.index.change.StalenessChecker.RefState;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
import com.google.gerrit.testutil.GerritBaseTests;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
import java.util.stream.Stream;
public class StalenessCheckerTest extends GerritBaseTests {
private static final String SHA1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
private static final String SHA2 = "badc0feebadc0feebadc0feebadc0feebadc0fee";
private static final Project.NameKey P1 = new Project.NameKey("project1");
private static final Project.NameKey P2 = new Project.NameKey("project2");
@Test
public void parseStates() {
assertInvalidState(null);
assertInvalidState("");
assertInvalidState("project1:refs/heads/foo");
assertInvalidState("project1:refs/heads/foo:notasha");
assertInvalidState("project1:refs/heads/foo:");
assertThat(
StalenessChecker.parseStates(
byteArrays(
P1 + ":refs/heads/foo:" + SHA1,
P1 + ":refs/heads/bar:" + SHA2,
P2 + ":refs/heads/baz:" + SHA1)))
.isEqualTo(
ImmutableSetMultimap.of(
P1, RefState.create("refs/heads/foo", SHA1),
P1, RefState.create("refs/heads/bar", SHA2),
P2, RefState.create("refs/heads/baz", SHA1)));
}
private static void assertInvalidState(String state) {
try {
StalenessChecker.parseStates(byteArrays(state));
assert_().fail("expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
}
}
@Test
public void refStateToByteArray() {
assertThat(
new String(
RefState.create("refs/heads/foo", ObjectId.fromString(SHA1))
.toByteArray(P1),
UTF_8))
.isEqualTo(P1 + ":refs/heads/foo:" + SHA1);
assertThat(
new String(
RefState.create("refs/heads/foo", (ObjectId) null)
.toByteArray(P1),
UTF_8))
.isEqualTo(P1 + ":refs/heads/foo:" + ObjectId.zeroId().name());
}
@Test
public void parsePatterns() {
assertInvalidPattern(null);
assertInvalidPattern("");
assertInvalidPattern("project:");
assertInvalidPattern("project:refs/heads/foo");
assertInvalidPattern("project:refs/he*ds/bar");
assertInvalidPattern("project:refs/(he)*ds/bar");
assertInvalidPattern("project:invalidrefname");
ListMultimap<Project.NameKey, RefStatePattern> r =
StalenessChecker.parsePatterns(
byteArrays(
P1 + ":refs/heads/*",
P2 + ":refs/heads/foo/*/bar",
P2 + ":refs/heads/foo/*-baz/*/quux"));
assertThat(r.keySet()).containsExactly(P1, P2);
RefStatePattern p = r.get(P1).get(0);
assertThat(p.pattern()).isEqualTo("refs/heads/*");
assertThat(p.prefix()).isEqualTo("refs/heads/");
assertThat(p.regex().pattern()).isEqualTo("^\\Qrefs/heads/\\E.*\\Q\\E$");
assertThat(p.match("refs/heads/foo")).isTrue();
assertThat(p.match("xrefs/heads/foo")).isFalse();
assertThat(p.match("refs/tags/foo")).isFalse();
p = r.get(P2).get(0);
assertThat(p.pattern()).isEqualTo("refs/heads/foo/*/bar");
assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
assertThat(p.regex().pattern())
.isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q/bar\\E$");
assertThat(p.match("refs/heads/foo//bar")).isTrue();
assertThat(p.match("refs/heads/foo/x/bar")).isTrue();
assertThat(p.match("refs/heads/foo/x/y/bar")).isTrue();
assertThat(p.match("refs/heads/foo/x/baz")).isFalse();
p = r.get(P2).get(1);
assertThat(p.pattern()).isEqualTo("refs/heads/foo/*-baz/*/quux");
assertThat(p.prefix()).isEqualTo("refs/heads/foo/");
assertThat(p.regex().pattern())
.isEqualTo("^\\Qrefs/heads/foo/\\E.*\\Q-baz/\\E.*\\Q/quux\\E$");
assertThat(p.match("refs/heads/foo/-baz//quux")).isTrue();
assertThat(p.match("refs/heads/foo/x-baz/x/quux")).isTrue();
assertThat(p.match("refs/heads/foo/x/y-baz/x/y/quux")).isTrue();
assertThat(p.match("refs/heads/foo/x-baz/x/y")).isFalse();
}
@Test
public void refStatePatternToByteArray() {
assertThat(
new String(RefStatePattern.create("refs/*").toByteArray(P1), UTF_8))
.isEqualTo(P1 + ":refs/*");
}
private static void assertInvalidPattern(String state) {
try {
StalenessChecker.parsePatterns(byteArrays(state));
assert_().fail("expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// Expected.
}
}
private static Iterable<byte[]> byteArrays(String... strs) {
return Stream.of(strs).map(s -> s != null ? s.getBytes(UTF_8) : null)
.collect(toList());
}
}