Index full sortkey field in secondary index

Using the sort key of the last element for pagination only works as
long as every sort key is unique. This is true for the general
definition of this field in the Change object, which includes the
unique change ID at the end of the hex string. Previously, we were
incorrectly truncating the change ID off, resulting in many changes in
the same sort key bucket and thus broken pagination.

Having two different definitions of sort key in the same running
server makes it a bit ugly to handle SortKeyPredicates, since the
definition of min/max value is now schema dependent, but at least we
can keep the same field name.

Don't @Deprecate the new SORTKEY field. We were originally hoping to
remove this field and depend only on the UPDATED field (with the
change ID as tiebreaker), but as long as this field is used for
pagination, we have to keep it around. This is because we can't be
sure a secondary index will be able to express a query like "(field X,
field Y) > (N, M)" to allow us to restart a query in the middle of an
UPDATED bucket. We may still decide to scrap the current pagination
system but that will probably not happen until we kill the SQL index
code.

Change-Id: Icb760dbacd01939e5e4936ef87165b6dddcacdc0
This commit is contained in:
Dave Borowitz
2013-09-20 14:55:15 -07:00
parent 8830bb53a4
commit 2e0f89d814
8 changed files with 102 additions and 38 deletions

View File

@@ -250,8 +250,8 @@ public class LuceneChangeIndex implements ChangeIndex {
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) { if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(closedIndex); indexes.add(closedIndex);
} }
return new QuerySource(indexes, QueryBuilder.toQuery(p), limit, return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
ChangeQueryBuilder.hasNonTrivialSortKeyAfter(p)); ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
} }
@Override @Override
@@ -300,7 +300,6 @@ public class LuceneChangeIndex implements ChangeIndex {
@Override @Override
public ResultSet<ChangeData> read() throws OrmException { public ResultSet<ChangeData> read() throws OrmException {
IndexSearcher[] searchers = new IndexSearcher[indexes.size()]; IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
@SuppressWarnings("deprecation")
Sort sort = new Sort( Sort sort = new Sort(
new SortField( new SortField(
ChangeField.SORTKEY.getName(), ChangeField.SORTKEY.getName(),

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.index.RegexPredicate; import com.google.gerrit.server.index.RegexPredicate;
import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.index.TimestampRangePredicate; import com.google.gerrit.server.index.TimestampRangePredicate;
import com.google.gerrit.server.query.AndPredicate; import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.NotPredicate; import com.google.gerrit.server.query.NotPredicate;
@@ -54,26 +55,27 @@ public class QueryBuilder {
return intTerm(ID_FIELD, cd.getId().get()); return intTerm(ID_FIELD, cd.getId().get());
} }
public static Query toQuery(Predicate<ChangeData> p) public static Query toQuery(Schema<ChangeData> schema, Predicate<ChangeData> p)
throws QueryParseException { throws QueryParseException {
if (p instanceof AndPredicate) { if (p instanceof AndPredicate) {
return and(p); return and(schema, p);
} else if (p instanceof OrPredicate) { } else if (p instanceof OrPredicate) {
return or(p); return or(schema, p);
} else if (p instanceof NotPredicate) { } else if (p instanceof NotPredicate) {
return not(p); return not(schema, p);
} else if (p instanceof IndexPredicate) { } else if (p instanceof IndexPredicate) {
return fieldQuery((IndexPredicate<ChangeData>) p); return fieldQuery(schema, (IndexPredicate<ChangeData>) p);
} else { } else {
throw new QueryParseException("cannot create query for index: " + p); throw new QueryParseException("cannot create query for index: " + p);
} }
} }
private static Query or(Predicate<ChangeData> p) throws QueryParseException { private static Query or(Schema<ChangeData> schema, Predicate<ChangeData> p)
throws QueryParseException {
try { try {
BooleanQuery q = new BooleanQuery(); BooleanQuery q = new BooleanQuery();
for (int i = 0; i < p.getChildCount(); i++) { for (int i = 0; i < p.getChildCount(); i++) {
q.add(toQuery(p.getChild(i)), SHOULD); q.add(toQuery(schema, p.getChild(i)), SHOULD);
} }
return q; return q;
} catch (BooleanQuery.TooManyClauses e) { } catch (BooleanQuery.TooManyClauses e) {
@@ -81,7 +83,8 @@ public class QueryBuilder {
} }
} }
private static Query and(Predicate<ChangeData> p) throws QueryParseException { private static Query and(Schema<ChangeData> schema, Predicate<ChangeData> p)
throws QueryParseException {
try { try {
BooleanQuery b = new BooleanQuery(); BooleanQuery b = new BooleanQuery();
List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount()); List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
@@ -92,10 +95,10 @@ public class QueryBuilder {
if (n instanceof TimestampRangePredicate) { if (n instanceof TimestampRangePredicate) {
b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST); b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
} else { } else {
not.add(toQuery(n)); not.add(toQuery(schema, n));
} }
} else { } else {
b.add(toQuery(c), MUST); b.add(toQuery(schema, c), MUST);
} }
} }
for (Query q : not) { for (Query q : not) {
@@ -107,7 +110,8 @@ public class QueryBuilder {
} }
} }
private static Query not(Predicate<ChangeData> p) throws QueryParseException { private static Query not(Schema<ChangeData> schema, Predicate<ChangeData> p)
throws QueryParseException {
Predicate<ChangeData> n = p.getChild(0); Predicate<ChangeData> n = p.getChild(0);
if (n instanceof TimestampRangePredicate) { if (n instanceof TimestampRangePredicate) {
return notTimestamp((TimestampRangePredicate<ChangeData>) n); return notTimestamp((TimestampRangePredicate<ChangeData>) n);
@@ -116,12 +120,12 @@ public class QueryBuilder {
// Lucene does not support negation, start with all and subtract. // Lucene does not support negation, start with all and subtract.
BooleanQuery q = new BooleanQuery(); BooleanQuery q = new BooleanQuery();
q.add(new MatchAllDocsQuery(), MUST); q.add(new MatchAllDocsQuery(), MUST);
q.add(toQuery(n), MUST_NOT); q.add(toQuery(schema, n), MUST_NOT);
return q; return q;
} }
private static Query fieldQuery(IndexPredicate<ChangeData> p) private static Query fieldQuery(Schema<ChangeData> schema,
throws QueryParseException { IndexPredicate<ChangeData> p) throws QueryParseException {
if (p.getType() == FieldType.INTEGER) { if (p.getType() == FieldType.INTEGER) {
return intQuery(p); return intQuery(p);
} else if (p.getType() == FieldType.TIMESTAMP) { } else if (p.getType() == FieldType.TIMESTAMP) {
@@ -133,7 +137,7 @@ public class QueryBuilder {
} else if (p.getType() == FieldType.FULL_TEXT) { } else if (p.getType() == FieldType.FULL_TEXT) {
return fullTextQuery(p); return fullTextQuery(p);
} else if (p instanceof SortKeyPredicate) { } else if (p instanceof SortKeyPredicate) {
return sortKeyQuery((SortKeyPredicate) p); return sortKeyQuery(schema, (SortKeyPredicate) p);
} else { } else {
throw badFieldType(p.getType()); throw badFieldType(p.getType());
} }
@@ -158,12 +162,14 @@ public class QueryBuilder {
return new TermQuery(intTerm(p.getField().getName(), value)); return new TermQuery(intTerm(p.getField().getName(), value));
} }
private static Query sortKeyQuery(SortKeyPredicate p) { private static Query sortKeyQuery(Schema<ChangeData> schema, SortKeyPredicate p) {
long min = p.getMinValue(schema);
long max = p.getMaxValue(schema);
return NumericRangeQuery.newLongRange( return NumericRangeQuery.newLongRange(
p.getField().getName(), p.getField().getName(),
p.getMinValue() != Long.MIN_VALUE ? p.getMinValue() : null, min != Long.MIN_VALUE ? min : null,
p.getMaxValue() != Long.MAX_VALUE ? p.getMaxValue() : null, max != Long.MAX_VALUE ? max : null,
true, true); false, false);
} }
private static Query timestampQuery(IndexPredicate<ChangeData> p) private static Query timestampQuery(IndexPredicate<ChangeData> p)

View File

@@ -478,7 +478,7 @@ public class ChangeUtil {
if ("z".equals(sortKey)) { if ("z".equals(sortKey)) {
return Long.MAX_VALUE; return Long.MAX_VALUE;
} }
return Long.parseLong(sortKey.substring(0, 8), 16); return Long.parseLong(sortKey, 16);
} }
public static void computeSortKey(final Change c) { public static void computeSortKey(final Change c) {

View File

@@ -125,8 +125,32 @@ public class ChangeField {
} }
}; };
/** Sort key field, duplicates {@link #UPDATED}. */
@Deprecated @Deprecated
public static long legacyParseSortKey(String sortKey) {
if ("z".equals(sortKey)) {
return Long.MAX_VALUE;
}
return Long.parseLong(sortKey.substring(0, 8), 16);
}
/** Legacy sort key field. */
@Deprecated
public static final FieldDef<ChangeData, Long> LEGACY_SORTKEY =
new FieldDef.Single<ChangeData, Long>(
"sortkey", FieldType.LONG, true) {
@Override
public Long get(ChangeData input, FillArgs args)
throws OrmException {
return legacyParseSortKey(input.change(args.db).getSortKey());
}
};
/**
* Sort key field.
* <p>
* Redundant with {@link #UPDATED} and {@link #LEGACY_ID}, but secondary index
* implementations may not be able to search over tuples of values.
*/
public static final FieldDef<ChangeData, Long> SORTKEY = public static final FieldDef<ChangeData, Long> SORTKEY =
new FieldDef.Single<ChangeData, Long>( new FieldDef.Single<ChangeData, Long>(
"sortkey", FieldType.LONG, true) { "sortkey", FieldType.LONG, true) {

View File

@@ -38,7 +38,7 @@ public class ChangeSchemas {
ChangeField.REF, ChangeField.REF,
ChangeField.TOPIC, ChangeField.TOPIC,
ChangeField.UPDATED, ChangeField.UPDATED,
ChangeField.SORTKEY, ChangeField.LEGACY_SORTKEY,
ChangeField.FILE, ChangeField.FILE,
ChangeField.OWNER, ChangeField.OWNER,
ChangeField.REVIEWER, ChangeField.REVIEWER,
@@ -51,6 +51,28 @@ public class ChangeSchemas {
@SuppressWarnings({"unchecked", "deprecation"}) @SuppressWarnings({"unchecked", "deprecation"})
static final Schema<ChangeData> V2 = release( static final Schema<ChangeData> V2 = release(
ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
ChangeField.PROJECT,
ChangeField.REF,
ChangeField.TOPIC,
ChangeField.UPDATED,
ChangeField.LEGACY_SORTKEY,
ChangeField.FILE,
ChangeField.OWNER,
ChangeField.REVIEWER,
ChangeField.COMMIT,
ChangeField.TR,
ChangeField.LABEL,
ChangeField.REVIEWED,
ChangeField.COMMIT_MESSAGE,
ChangeField.COMMENT,
ChangeField.CHANGE,
ChangeField.APPROVAL);
@SuppressWarnings("unchecked")
static final Schema<ChangeData> V3 = release(
ChangeField.LEGACY_ID, ChangeField.LEGACY_ID,
ChangeField.ID, ChangeField.ID,
ChangeField.STATUS, ChangeField.STATUS,

View File

@@ -34,6 +34,7 @@ import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.ChangeIndex; import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.IndexCollection; import com.google.gerrit.server.index.IndexCollection;
import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
@@ -121,10 +122,11 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue(); return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
} }
public static boolean hasNonTrivialSortKeyAfter(Predicate<ChangeData> p) { public static boolean hasNonTrivialSortKeyAfter(Schema<ChangeData> schema,
Predicate<ChangeData> p) {
SortKeyPredicate after = SortKeyPredicate after =
(SortKeyPredicate) find(p, SortKeyPredicate.class, "sortkey_after"); (SortKeyPredicate) find(p, SortKeyPredicate.class, "sortkey_after");
return after != null && after.getMaxValue() > 0; return after != null && after.getMaxValue(schema) > 0;
} }
public static boolean hasSortKey(Predicate<ChangeData> p) { public static boolean hasSortKey(Predicate<ChangeData> p) {

View File

@@ -18,14 +18,25 @@ import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.index.ChangeField; import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.index.Schema;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider; import com.google.inject.Provider;
public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> { public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
@SuppressWarnings("deprecation")
private static long parseSortKey(Schema<ChangeData> schema, String value) {
FieldDef<ChangeData, ?> field = schema.getFields().get(ChangeField.SORTKEY.getName());
if (field == ChangeField.SORTKEY) {
return ChangeUtil.parseSortKey(value);
} else {
return ChangeField.legacyParseSortKey(value);
}
}
protected final Provider<ReviewDb> dbProvider; protected final Provider<ReviewDb> dbProvider;
@SuppressWarnings("deprecation")
SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) { SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
super(ChangeField.SORTKEY, name, value); super(ChangeField.SORTKEY, name, value);
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
@@ -36,8 +47,8 @@ public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
return 1; return 1;
} }
public abstract long getMinValue(); public abstract long getMinValue(Schema<ChangeData> schema);
public abstract long getMaxValue(); public abstract long getMaxValue(Schema<ChangeData> schema);
public abstract SortKeyPredicate copy(String newValue); public abstract SortKeyPredicate copy(String newValue);
public static class Before extends SortKeyPredicate { public static class Before extends SortKeyPredicate {
@@ -46,13 +57,13 @@ public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
} }
@Override @Override
public long getMinValue() { public long getMinValue(Schema<ChangeData> schema) {
return 0; return 0;
} }
@Override @Override
public long getMaxValue() { public long getMaxValue(Schema<ChangeData> schema) {
return ChangeUtil.parseSortKey(getValue()); return parseSortKey(schema, getValue());
} }
@Override @Override
@@ -73,12 +84,12 @@ public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
} }
@Override @Override
public long getMinValue() { public long getMinValue(Schema<ChangeData> schema) {
return ChangeUtil.parseSortKey(getValue()); return parseSortKey(schema, getValue());
} }
@Override @Override
public long getMaxValue() { public long getMaxValue(Schema<ChangeData> schema) {
return Long.MAX_VALUE; return Long.MAX_VALUE;
} }

View File

@@ -205,8 +205,8 @@ class SolrChangeIndex implements ChangeIndex, LifecycleListener {
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) { if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(closedIndex); indexes.add(closedIndex);
} }
return new QuerySource(indexes, QueryBuilder.toQuery(p), limit, return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
ChangeQueryBuilder.hasNonTrivialSortKeyAfter(p)); ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
} }
private void commit(SolrServer server) throws IOException { private void commit(SolrServer server) throws IOException {