Render JSON results for the change table only using the index

Define a new type of field, STORED_ONLY, for storing arbitrary raw
bytes that cannot be searched. Add two fields of this type storing the
Change object and the list of SubmitRecord.Labels, which is exactly
the set of data needed to render a ChangeTable2. This avoids hitting
the database in the common case of rendering dashboards and search
results in the web UI, which may be costly on non-SQL implementations
Similarly we can avoid running and re-running the submit rule
evaluator.

This implementation has a minor bug in that changes to submit rules
may invalidate previously indexed labels, as can changing the set of
plugins with PredicateProviders.

There is substantial collateral damage to Reindex stemming from the
fact that indexing now depends on the submit rule evaluator, which in
turn requires pulling in plugins and hence a lot more Guice glue.

Change-Id: I3c10ed936eb9258e453c46f947abf35f5b8fb308
This commit is contained in:
Dave Borowitz 2013-09-10 15:23:57 -07:00
parent 2474f211d4
commit 05b254feb2
10 changed files with 373 additions and 52 deletions

View File

@ -29,6 +29,8 @@ import com.google.gerrit.reviewdb.client.SubmitRecord;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.ChangeField.ChangeProtoField;
import com.google.gerrit.server.index.ChangeField.SubmitLabelProtoField;
import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldDef.FillArgs;
@ -51,6 +53,7 @@ import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.LongField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
@ -64,6 +67,7 @@ import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.Version;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@ -99,6 +103,9 @@ public class LuceneChangeIndex implements ChangeIndex {
public static final String CHANGES_OPEN = "open";
public static final String CHANGES_CLOSED = "closed";
private static final String ID_FIELD = ChangeField.LEGACY_ID.getName();
private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
private static final String SUBMIT_LABEL_FIELD =
ChangeField.SUBMIT_RECORD_LABEL.getName();
static interface Factory {
LuceneChangeIndex create(Schema<ChangeData> schema, String base);
@ -257,7 +264,8 @@ public class LuceneChangeIndex implements ChangeIndex {
}
private static class QuerySource implements ChangeDataSource {
private static final ImmutableSet<String> FIELDS = ImmutableSet.of(ID_FIELD);
private static final ImmutableSet<String> FIELDS =
ImmutableSet.of(ID_FIELD, CHANGE_FIELD, SUBMIT_LABEL_FIELD);
private final List<SubIndex> indexes;
private final Query query;
@ -304,8 +312,7 @@ public class LuceneChangeIndex implements ChangeIndex {
Lists.newArrayListWithCapacity(docs.scoreDocs.length);
for (ScoreDoc sd : docs.scoreDocs) {
Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
Number v = doc.getField(ID_FIELD).numericValue();
result.add(new ChangeData(new Change.Id(v.intValue())));
result.add(toChangeData(doc));
}
final List<ChangeData> r = Collections.unmodifiableList(result);
@ -341,6 +348,30 @@ public class LuceneChangeIndex implements ChangeIndex {
}
}
private static ChangeData toChangeData(Document doc) {
BytesRef cb = doc.getBinaryValue(CHANGE_FIELD);
if (cb == null) {
int id = doc.getField(ID_FIELD).numericValue().intValue();
return new ChangeData(new Change.Id(id));
}
Change change = ChangeProtoField.CODEC.decode(
cb.bytes, cb.offset, cb.length);
ChangeData cd = new ChangeData(change);
BytesRef[] labelsBytes = doc.getBinaryValues(SUBMIT_LABEL_FIELD);
if (labelsBytes != null) {
List<SubmitRecord.Label> labels =
Lists.newArrayListWithCapacity(labelsBytes.length);
for (BytesRef lb : labelsBytes) {
labels.add(SubmitLabelProtoField.CODEC.decode(
lb.bytes, lb.offset, lb.length));
}
cd.setSubmitRecordLabels(labels);
}
return cd;
}
private Document toDocument(ChangeData cd) throws IOException {
try {
Document result = new Document();
@ -387,6 +418,10 @@ public class LuceneChangeIndex implements ChangeIndex {
for (Object value : values) {
doc.add(new TextField(name, (String) value, store));
}
} else if (f.getType() == FieldType.STORED_ONLY) {
for (Object value : values) {
doc.add(new StoredField(name, (byte[]) value));
}
} else {
throw QueryBuilder.badFieldType(f.getType());
}

View File

@ -15,9 +15,13 @@
package com.google.gerrit.pgm;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.DisabledChangeHooks;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleManager;
@ -27,8 +31,28 @@ import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologModule;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.EmailExpander;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCacheImpl;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.IncludingGroupMembership;
import com.google.gerrit.server.account.InternalGroupBackend;
import com.google.gerrit.server.account.UniversalGroupBackend;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.EmailExpanderProvider;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.index.ChangeBatchIndexer;
import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.ChangeSchemas;
@ -36,14 +60,25 @@ import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.index.NoIndexModule;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.EmailHeader;
import com.google.gerrit.server.mail.EmailSender;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.plugins.PluginModule;
import com.google.gerrit.server.project.AccessControlModule;
import com.google.gerrit.server.project.CommentLinkInfo;
import com.google.gerrit.server.project.CommentLinkProvider;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.ssh.NoSshKeyCache;
import com.google.gerrit.solr.SolrIndexModule;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.TypeLiteral;
@ -53,8 +88,10 @@ import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.util.io.NullOutputStream;
import org.kohsuke.args4j.Option;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -110,33 +147,99 @@ public class Reindex extends SiteProgram {
}
private Injector createSysInjector() {
List<Module> modules = Lists.newArrayList();
modules.add(PatchListCacheImpl.module());
AbstractModule changeIndexModule;
switch (IndexModule.getIndexType(dbInjector)) {
case LUCENE:
changeIndexModule = new LuceneIndexModule(version, threads, outputBase);
break;
case SOLR:
changeIndexModule = new SolrIndexModule(false, threads, outputBase);
break;
default:
changeIndexModule = new NoIndexModule();
return dbInjector.createChildInjector(new GlobalModule());
}
modules.add(changeIndexModule);
modules.add(new ReviewDbModule());
modules.add(new AbstractModule() {
/**
* Subset of GerritGlobalModule including just bindings needed for indexing.
*
* Does <b>not</b> include thread local request scoping behavior of ReviewDbs;
* uses a custom {@link ReviewDbModule} instead.
*/
private class GlobalModule extends FactoryModule {
@SuppressWarnings("rawtypes")
@Override
protected void configure() {
// Plugins are not loaded and we're just running through each change
// once, so don't worry about cache removal.
bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
.toInstance(DynamicSet.<CacheRemovalListener> emptySet());
install(PatchListCacheImpl.module());
install(dbInjector.getInstance(AuthModule.class));
install(AccountByEmailCacheImpl.module());
install(AccountCacheImpl.module());
install(new ReviewDbModule());
install(ProjectCacheImpl.module());
install(new PluginModule());
install(new PrologModule());
install(new DefaultCacheFactory.Module());
install(NoSshKeyCache.module());
install(new SignedTokenEmailTokenVerifier.Module());
install(GroupCacheImpl.module());
install(new AccessControlModule());
install(SectionSortCache.module());
install(new CanonicalWebUrlModule() {
@Override
protected Class<? extends Provider<String>> provider() {
return CanonicalWebUrlProvider.class;
}
});
return dbInjector.createChildInjector(modules);
switch (IndexModule.getIndexType(dbInjector)) {
case LUCENE:
install(new LuceneIndexModule(version, threads, outputBase));
break;
case SOLR:
install(new SolrIndexModule(false, threads, outputBase));
break;
default:
install(new NoIndexModule());
}
// No need to support live plugin removal or other live updates of
// caches behind our back, so don't worry about cache removal.
bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
.toInstance(DynamicSet.<CacheRemovalListener> emptySet());
// Assume the current user is always the Gerrit internal user; no
// indexed data should depend on what user is doing the indexing.
bind(CurrentUser.class).to(InternalUser.class);
bind(ChangeHooks.class).to(DisabledChangeHooks.class);
bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
SINGLETON);
bind(new TypeLiteral<List<CommentLinkInfo>>() {})
.toProvider(CommentLinkProvider.class).in(SINGLETON);
bind(ProjectControl.GenericFactory.class);
factory(CapabilityControl.Factory.class);
factory(IncludingGroupMembership.Factory.class);
factory(InternalUser.Factory.class);
factory(PluginUser.Factory.class);
factory(ProjectState.Factory.class);
bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
install(GroupIncludeCacheImpl.module());
bind(GroupControl.Factory.class).in(SINGLETON);
bind(GroupControl.GenericFactory.class).in(SINGLETON);
bind(InternalGroupBackend.class).in(SINGLETON);
DynamicSet.setOf(binder(), GroupBackend.class);
DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
bind(EmailSender.class).toInstance(new EmailSender() {
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean canEmail(String address) {
return false;
}
@Override
public void send(Address from, Collection<Address> rcpt,
Map<String, EmailHeader> headers, String body)
throws EmailException {
}
});
}
}
private class ReviewDbModule extends LifecycleModule {

View File

@ -32,6 +32,7 @@ java_library2(
'//lib:ow2-asm-util',
'//lib:parboiled-core',
'//lib:pegdown',
'//lib:protobuf',
'//lib:velocity',
'//lib/antlr:java_runtime',
'//lib/commons:codec',

View File

@ -380,6 +380,10 @@ public class ChangeJson {
if (!standard && !detailed) {
return null;
}
if (cd.getSubmitRecordLabels() != null && standard && !detailed) {
// Use saved labels to avoid recomputing on the fly.
return labelsFromSavedState(cd.getSubmitRecordLabels());
}
ChangeControl ctl = control(cd);
if (ctl == null) {
@ -394,6 +398,15 @@ public class ChangeJson {
}
}
private Map<String, LabelInfo> labelsFromSavedState(
List<SubmitRecord.Label> savedLabels) {
Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
for (SubmitRecord.Label r : savedLabels) {
labels.put(r.label, newLabelFromSubmitRecord(r, true));
}
return labels;
}
private Map<String, LabelInfo> labelsForOpenChange(ChangeData cd,
LabelTypes labelTypes, boolean standard, boolean detailed)
throws OrmException {
@ -434,6 +447,15 @@ public class ChangeJson {
for (SubmitRecord.Label r : rec.labels) {
LabelInfo p = labels.get(r.label);
if (p == null || p._status.compareTo(r.status) < 0) {
labels.put(r.label, newLabelFromSubmitRecord(r, standard));
}
}
}
return labels;
}
private LabelInfo newLabelFromSubmitRecord(SubmitRecord.Label r,
boolean standard) {
LabelInfo n = new LabelInfo();
n._status = r.status;
if (standard) {
@ -450,11 +472,7 @@ public class ChangeJson {
}
n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
labels.put(r.label, n);
}
}
}
return labels;
return n;
}
private void setLabelScores(LabelType type,

View File

@ -21,7 +21,7 @@ import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
class EmailExpanderProvider implements Provider<EmailExpander> {
public class EmailExpanderProvider implements Provider<EmailExpander> {
private final EmailExpander expander;
@Inject

View File

@ -14,21 +14,35 @@
package com.google.gerrit.server.index;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.SubmitRecord;
import com.google.gerrit.reviewdb.client.TrackingId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gwtorm.protobuf.CodecFactory;
import com.google.gwtorm.protobuf.ProtobufCodec;
import com.google.gwtorm.server.OrmException;
import com.google.protobuf.CodedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -233,6 +247,82 @@ public class ChangeField {
}
};
public static class ChangeProtoField extends FieldDef.Single<ChangeData, byte[]> {
public static final ProtobufCodec<Change> CODEC =
CodecFactory.encoder(Change.class);
private ChangeProtoField() {
super("_change", FieldType.STORED_ONLY, true);
}
@Override
public byte[] get(ChangeData input, FieldDef.FillArgs args)
throws OrmException {
return CODEC.encodeToByteArray(input.change(args.db));
}
}
/** Serialized change object, used for pre-populating results. */
public static final ChangeProtoField CHANGE = new ChangeProtoField();
public static class SubmitLabelProtoField
extends FieldDef.Repeatable<ChangeData, byte[]> {
public static final ProtobufCodec<SubmitRecord.Label> CODEC =
CodecFactory.encoder(SubmitRecord.Label.class);
private SubmitLabelProtoField() {
super("_label", FieldType.STORED_ONLY, true);
}
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
// Flatten the highest-valued labels to mimic the results from ChangeJson
// with standard labels.
Map<String, SubmitRecord.Label> labels = Maps.newLinkedHashMap();
for (SubmitRecord rec : getSubmitRecords(input, args)) {
if (rec.labels == null) {
continue;
}
for (SubmitRecord.Label r : rec.labels) {
SubmitRecord.Label p = labels.get(r.label);
if (p == null || p.status.compareTo(r.status) < 0) {
labels.put(r.label, r);
}
}
}
return toProtos(CODEC, labels.values());
}
private List<SubmitRecord> getSubmitRecords(ChangeData input,
FillArgs args) throws OrmException {
ChangeControl ctl;
try {
// Use the ChangeControl for InternalUser. This will give bogus
// results for whether or not the change is submittable, but does
// not affect label calculation.
ctl = args.changeControlFor(input.change(args.db));
} catch (NoSuchChangeException e) {
throw new OrmException(e);
}
if (ctl == null) {
return ImmutableList.of();
}
PatchSet ps = input.currentPatchSet(args.db);
if (ps == null) {
return ImmutableList.of();
}
return ctl.canSubmit(args.db.get(), ps, input, true, true, true);
}
}
/**
* Serialized labels from the submit rule evaluator, used for pre-populating
* results.
*/
public static final SubmitLabelProtoField SUBMIT_RECORD_LABEL =
new SubmitLabelProtoField();
public static String formatLabel(String label, int value) {
return formatLabel(label, value, null);
}
@ -273,4 +363,22 @@ public class ChangeField {
return r;
}
};
private static <T> List<byte[]> toProtos(ProtobufCodec<T> codec, Collection<T> objs)
throws OrmException {
List<byte[]> result = Lists.newArrayListWithCapacity(objs.size());
ByteArrayOutputStream out = new ByteArrayOutputStream(256);
try {
for (T obj : objs) {
out.reset();
CodedOutputStream cos = CodedOutputStream.newInstance(out);
codec.encode(obj, cos);
cos.flush();
result.add(out.toByteArray());
}
} catch (IOException e) {
throw new OrmException(e);
}
return result;
}
}

View File

@ -49,6 +49,28 @@ public class ChangeSchemas {
ChangeField.COMMIT_MESSAGE,
ChangeField.COMMENT);
@SuppressWarnings({"unchecked", "deprecation"})
static final Schema<ChangeData> V2 = release(
ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
ChangeField.PROJECT,
ChangeField.REF,
ChangeField.TOPIC,
ChangeField.UPDATED,
ChangeField.SORTKEY,
ChangeField.FILE,
ChangeField.OWNER,
ChangeField.REVIEWER,
ChangeField.COMMIT,
ChangeField.TR,
ChangeField.LABEL,
ChangeField.REVIEWED,
ChangeField.COMMIT_MESSAGE,
ChangeField.COMMENT,
ChangeField.CHANGE,
ChangeField.SUBMIT_RECORD_LABEL);
private static Schema<ChangeData> release(FieldDef<ChangeData, ?>... fields) {
return new Schema<ChangeData>(true, Arrays.asList(fields));
}

View File

@ -14,9 +14,13 @@
package com.google.gerrit.server.index;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -60,14 +64,31 @@ public abstract class FieldDef<I, T> {
final Provider<ReviewDb> db;
final GitRepositoryManager repoManager;
final PatchListCache patchListCache;
final ChangeControl.GenericFactory changeControlFactory;
private final InternalUser internalUser;
@Inject
FillArgs(Provider<ReviewDb> db,
GitRepositoryManager repoManager,
PatchListCache patchListCache) {
FillArgs(Provider<ReviewDb> db, GitRepositoryManager repoManager,
PatchListCache patchListCache,
ChangeControl.GenericFactory changeControlFactory,
InternalUser internalUser) {
this.db = db;
this.repoManager = repoManager;
this.patchListCache = patchListCache;
this.changeControlFactory = changeControlFactory;
this.internalUser = internalUser;
}
/**
* Create a change control.
*
* @param change change object.
* @return change control as seen by Gerrit's internal user; index fields
* may not depend on any per-user state.
*/
ChangeControl changeControlFor(Change change) throws NoSuchChangeException {
return changeControlFactory.controlFor(change, internalUser);
}
}

View File

@ -43,6 +43,10 @@ public class FieldType<T> {
public static final FieldType<String> FULL_TEXT =
new FieldType<String>("FULL_TEXT");
/** A field that is only stored as raw bytes and cannot be queried. */
public static final FieldType<byte[]> STORED_ONLY =
new FieldType<byte[]>("STORED_ONLY");
private final String name;
private FieldType(String name) {

View File

@ -148,6 +148,7 @@ public class ChangeData {
private ChangeControl changeControl;
private List<ChangeMessage> messages;
private List<SubmitRecord> submitRecords;
private List<SubmitRecord.Label> submitRecordLabels;
private boolean patchesLoaded;
public ChangeData(final Change.Id id) {
@ -459,4 +460,12 @@ public class ChangeData {
public List<SubmitRecord> getSubmitRecords() {
return submitRecords;
}
public void setSubmitRecordLabels(List<SubmitRecord.Label> labels) {
submitRecordLabels = labels;
}
public List<SubmitRecord.Label> getSubmitRecordLabels() {
return submitRecordLabels;
}
}