QueryProcessor: Short-circuit return empty results when disabled
When visibility is enforced, it is possible for the query processor to
be disabled due to the user having a non-positive query limit
capability. Previously, calling query in this case may have unexpected
behavior, such as throwing an unchecked exception due to dividing by
zero or constructing a QueryOptions with a negative limit. Avoid
exceptions by short-circuiting and returning no results.
This new implementation means that callers need to call isDisabled to
distinguish between an empty result set and a disabled processor. Most
callers do not care about detecting this specific case and returning an
error message to the user; as a result, it would be onerous to require
them to call isDisabled in all cases. Only the callers that care
specifically need to do so, such as QueryChanges and OutputStreamQuery.
Change-Id: Ie6db9d8d67e39eb5d4676417a21fa8cd9cf830da
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index 92439db..65f9721 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -14,10 +14,15 @@
package com.google.gerrit.server.query;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexCollection;
@@ -46,6 +51,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
public abstract class QueryProcessor<T> {
@Singleton
@@ -105,6 +111,20 @@
return this;
}
+ /**
+ * Specify whether to enforce visibility by filtering out results that are not visible to the
+ * user.
+ *
+ * <p>Enforcing visibility may have performance consequences, as the index system may need to
+ * post-filter a large number of results to fill even a modest limit.
+ *
+ * <p>If visibility is enforced, the user's {@code queryLimit} global capability is also used to
+ * bound the total number of results. If this capability is non-positive, this results in the
+ * entire query processor being {@link #isDisabled() disabled}.
+ *
+ * @param enforce whether to enforce visibility.
+ * @return this.
+ */
public QueryProcessor<T> enforceVisibility(boolean enforce) {
enforceVisibility = enforce;
return this;
@@ -134,6 +154,10 @@
/**
* Perform multiple queries in parallel.
*
+ * <p>If querying is disabled, short-circuits the index and returns empty results. Callers that
+ * wish to distinguish this case from a query returning no results from the index may call {@link
+ * #isDisabled()} themselves.
+ *
* @param queries list of queries.
* @return results of the queries, one QueryResult per input query, in the same order as the
* input.
@@ -152,11 +176,22 @@
}
}
- private List<QueryResult<T>> query(List<String> queryStrings, List<Predicate<T>> queries)
+ private List<QueryResult<T>> query(
+ @Nullable List<String> queryStrings, List<Predicate<T>> queries)
throws OrmException, QueryParseException {
long startNanos = System.nanoTime();
-
int cnt = queries.size();
+ if (queryStrings != null) {
+ int qs = queryStrings.size();
+ checkArgument(qs == cnt, "got %s query strings but %s predicates", qs, cnt);
+ }
+ if (cnt == 0) {
+ return ImmutableList.of();
+ }
+ if (isDisabled()) {
+ return disabledResults(queryStrings, queries);
+ }
+
// Parse and rewrite all queries.
List<Integer> limits = new ArrayList<>(cnt);
List<Predicate<T>> predicates = new ArrayList<>(cnt);
@@ -206,12 +241,25 @@
matches.get(i).toList()));
}
- // only measure successful queries
+ // Only measure successful queries that actually touched the index.
metrics.executionTime.record(
schemaDef.getName(), System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
return out;
}
+ private static <T> ImmutableList<QueryResult<T>> disabledResults(
+ List<String> queryStrings, List<Predicate<T>> queries) {
+ return IntStream.range(0, queries.size())
+ .mapToObj(
+ i ->
+ QueryResult.create(
+ queryStrings != null ? queryStrings.get(i) : null,
+ queries.get(i),
+ 0,
+ ImmutableList.of()))
+ .collect(toImmutableList());
+ }
+
protected QueryOptions createOptions(
IndexConfig indexConfig, int start, int limit, Set<String> requestedFields) {
return QueryOptions.create(indexConfig, start, limit, requestedFields);
@@ -234,6 +282,19 @@
return index != null ? index.getSchema().getStoredFields().keySet() : ImmutableSet.<String>of();
}
+ /**
+ * Check whether querying should be disabled.
+ *
+ * <p>Currently, the only condition that can disable the whole query processor is if both {@link
+ * #enforceVisibility(boolean) visibility is enforced} and the user has a non-positive maximum
+ * value for the {@code queryLimit} capability.
+ *
+ * <p>If querying is disabled, all calls to {@link #query(Predicate)} and {@link #query(List)}
+ * will return empty results. This method can be used if callers wish to distinguish this case
+ * from a query returning no results from the index.
+ *
+ * @return true if querying should be disabled.
+ */
public boolean isDisabled() {
return getPermittedLimit() <= 0;
}
@@ -265,6 +326,9 @@
possibleLimits.add(limitFromPredicate);
}
}
- return Ordering.natural().min(possibleLimits);
+ int result = Ordering.natural().min(possibleLimits);
+ // Should have short-circuited from #query or thrown some other exception before getting here.
+ checkState(result > 0, "effective limit should be positive");
+ return result;
}
}