Skip to content

Commit 04572bb

Browse files
Replace IndexNameExpressionResolver.ExpressionList with imperative logic (#115487) (#115602)
The approach taken by `ExpressionList` becomes very expensive for large numbers of indices/datastreams. It implies that large lists of concrete names (as they are passed down from the transport layer via e.g. security) are copied at least twice during iteration. Removing the intermediary list and inlining the logic brings down the latency of searches targetting many shards/indices at once and allows for subsequent optimizations. The removed tests appear redundant as they tested an implementation detail of the IndexNameExpressionResolver which itself is well covered by its own tests.
1 parent e58fb83 commit 04572bb

File tree

2 files changed

+85
-410
lines changed

2 files changed

+85
-410
lines changed

server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java

Lines changed: 85 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
import java.util.Collections;
4949
import java.util.HashMap;
5050
import java.util.HashSet;
51-
import java.util.Iterator;
5251
import java.util.List;
5352
import java.util.Map;
5453
import java.util.Objects;
@@ -253,7 +252,7 @@ protected static Collection<String> resolveExpressions(Context context, String..
253252
} else {
254253
return ExplicitResourceNameFilter.filterUnavailable(
255254
context,
256-
DateMathExpressionResolver.resolve(context, List.of(expressions))
255+
DateMathExpressionResolver.resolve(context, Arrays.asList(expressions))
257256
);
258257
}
259258
} else {
@@ -264,7 +263,10 @@ protected static Collection<String> resolveExpressions(Context context, String..
264263
} else {
265264
return WildcardExpressionResolver.resolve(
266265
context,
267-
ExplicitResourceNameFilter.filterUnavailable(context, DateMathExpressionResolver.resolve(context, List.of(expressions)))
266+
ExplicitResourceNameFilter.filterUnavailable(
267+
context,
268+
DateMathExpressionResolver.resolve(context, Arrays.asList(expressions))
269+
)
268270
);
269271
}
270272
}
@@ -1294,34 +1296,51 @@ private static boolean shouldIncludeIfAlias(IndexAbstraction ia, IndexNameExpres
12941296
* </ol>
12951297
*/
12961298
public static Collection<String> resolve(Context context, List<String> expressions) {
1297-
ExpressionList expressionList = new ExpressionList(context, expressions);
12981299
// fast exit if there are no wildcards to evaluate
1299-
if (expressionList.hasWildcard() == false) {
1300+
if (context.getOptions().expandWildcardExpressions() == false) {
1301+
return expressions;
1302+
}
1303+
int firstWildcardIndex = 0;
1304+
for (; firstWildcardIndex < expressions.size(); firstWildcardIndex++) {
1305+
String expression = expressions.get(firstWildcardIndex);
1306+
if (isWildcard(expression)) {
1307+
break;
1308+
}
1309+
}
1310+
if (firstWildcardIndex == expressions.size()) {
13001311
return expressions;
13011312
}
13021313
Set<String> result = new HashSet<>();
1303-
for (ExpressionList.Expression expression : expressionList) {
1304-
if (expression.isWildcard()) {
1305-
Stream<IndexAbstraction> matchingResources = matchResourcesToWildcard(context, expression.get());
1314+
for (int i = 0; i < firstWildcardIndex; i++) {
1315+
result.add(expressions.get(i));
1316+
}
1317+
AtomicBoolean emptyWildcardExpansion = context.getOptions().allowNoIndices() ? null : new AtomicBoolean();
1318+
for (int i = firstWildcardIndex; i < expressions.size(); i++) {
1319+
String expression = expressions.get(i);
1320+
boolean isExclusion = i > firstWildcardIndex && expression.charAt(0) == '-';
1321+
if (i == firstWildcardIndex || isWildcard(expression)) {
1322+
Stream<IndexAbstraction> matchingResources = matchResourcesToWildcard(
1323+
context,
1324+
isExclusion ? expression.substring(1) : expression
1325+
);
13061326
Stream<String> matchingOpenClosedNames = expandToOpenClosed(context, matchingResources);
1307-
AtomicBoolean emptyWildcardExpansion = new AtomicBoolean(false);
1308-
if (context.getOptions().allowNoIndices() == false) {
1327+
if (emptyWildcardExpansion != null) {
13091328
emptyWildcardExpansion.set(true);
13101329
matchingOpenClosedNames = matchingOpenClosedNames.peek(x -> emptyWildcardExpansion.set(false));
13111330
}
1312-
if (expression.isExclusion()) {
1313-
matchingOpenClosedNames.forEachOrdered(result::remove);
1331+
if (isExclusion) {
1332+
matchingOpenClosedNames.forEach(result::remove);
13141333
} else {
1315-
matchingOpenClosedNames.forEachOrdered(result::add);
1334+
matchingOpenClosedNames.forEach(result::add);
13161335
}
1317-
if (emptyWildcardExpansion.get()) {
1318-
throw notFoundException(expression.get());
1336+
if (emptyWildcardExpansion != null && emptyWildcardExpansion.get()) {
1337+
throw notFoundException(expression);
13191338
}
13201339
} else {
1321-
if (expression.isExclusion()) {
1322-
result.remove(expression.get());
1340+
if (isExclusion) {
1341+
result.remove(expression.substring(1));
13231342
} else {
1324-
result.add(expression.get());
1343+
result.add(expression);
13251344
}
13261345
}
13271346
}
@@ -1507,27 +1526,35 @@ private DateMathExpressionResolver() {
15071526
// utility class
15081527
}
15091528

1529+
/**
1530+
* Resolves date math expressions. If this is a noop the given {@code expressions} list is returned without copying.
1531+
* As a result callers of this method should not mutate the returned list. Mutating it may come with unexpected side effects.
1532+
*/
15101533
public static List<String> resolve(Context context, List<String> expressions) {
1511-
List<String> result = new ArrayList<>(expressions.size());
1512-
for (ExpressionList.Expression expression : new ExpressionList(context, expressions)) {
1513-
result.add(resolveExpression(expression, context::getStartTime));
1534+
boolean wildcardSeen = false;
1535+
final boolean expandWildcards = context.getOptions().expandWildcardExpressions();
1536+
String[] result = null;
1537+
for (int i = 0, n = expressions.size(); i < n; i++) {
1538+
String expression = expressions.get(i);
1539+
// accepts date-math exclusions that are of the form "-<...{}>",f i.e. the "-" is outside the "<>" date-math template
1540+
boolean isExclusion = wildcardSeen && expression.startsWith("-");
1541+
wildcardSeen = wildcardSeen || (expandWildcards && isWildcard(expression));
1542+
String toResolve = isExclusion ? expression.substring(1) : expression;
1543+
String resolved = resolveExpression(toResolve, context::getStartTime);
1544+
if (toResolve != resolved) {
1545+
if (result == null) {
1546+
result = expressions.toArray(Strings.EMPTY_ARRAY);
1547+
}
1548+
result[i] = isExclusion ? "-" + resolved : resolved;
1549+
}
15141550
}
1515-
return result;
1551+
return result == null ? expressions : Arrays.asList(result);
15161552
}
15171553

15181554
static String resolveExpression(String expression) {
15191555
return resolveExpression(expression, System::currentTimeMillis);
15201556
}
15211557

1522-
static String resolveExpression(ExpressionList.Expression expression, LongSupplier getTime) {
1523-
if (expression.isExclusion()) {
1524-
// accepts date-math exclusions that are of the form "-<...{}>", i.e. the "-" is outside the "<>" date-math template
1525-
return "-" + resolveExpression(expression.get(), getTime);
1526-
} else {
1527-
return resolveExpression(expression.get(), getTime);
1528-
}
1529-
}
1530-
15311558
static String resolveExpression(String expression, LongSupplier getTime) {
15321559
if (expression.startsWith(EXPRESSION_LEFT_BOUND) == false || expression.endsWith(EXPRESSION_RIGHT_BOUND) == false) {
15331560
return expression;
@@ -1689,14 +1716,35 @@ private ExplicitResourceNameFilter() {
16891716
*/
16901717
public static List<String> filterUnavailable(Context context, List<String> expressions) {
16911718
ensureRemoteIndicesRequireIgnoreUnavailable(context.getOptions(), expressions);
1692-
List<String> result = new ArrayList<>(expressions.size());
1693-
for (ExpressionList.Expression expression : new ExpressionList(context, expressions)) {
1694-
validateAliasOrIndex(expression);
1695-
if (expression.isWildcard() || expression.isExclusion() || ensureAliasOrIndexExists(context, expression.get())) {
1696-
result.add(expression.expression());
1719+
final boolean expandWildcards = context.getOptions().expandWildcardExpressions();
1720+
boolean wildcardSeen = false;
1721+
List<String> result = null;
1722+
for (int i = 0; i < expressions.size(); i++) {
1723+
String expression = expressions.get(i);
1724+
if (Strings.isEmpty(expression)) {
1725+
throw notFoundException(expression);
1726+
}
1727+
// Expressions can not start with an underscore. This is reserved for APIs. If the check gets here, the API
1728+
// does not exist and the path is interpreted as an expression. If the expression begins with an underscore,
1729+
// throw a specific error that is different from the [[IndexNotFoundException]], which is typically thrown
1730+
// if the expression can't be found.
1731+
if (expression.charAt(0) == '_') {
1732+
throw new InvalidIndexNameException(expression, "must not start with '_'.");
1733+
}
1734+
final boolean isWildcard = expandWildcards && isWildcard(expression);
1735+
if (isWildcard || (wildcardSeen && expression.charAt(0) == '-') || ensureAliasOrIndexExists(context, expression)) {
1736+
if (result != null) {
1737+
result.add(expression);
1738+
}
1739+
} else {
1740+
if (result == null) {
1741+
result = new ArrayList<>(expressions.size() - 1);
1742+
result.addAll(expressions.subList(0, i));
1743+
}
16971744
}
1745+
wildcardSeen |= isWildcard;
16981746
}
1699-
return result;
1747+
return result == null ? expressions : result;
17001748
}
17011749

17021750
/**
@@ -1736,19 +1784,6 @@ private static boolean ensureAliasOrIndexExists(Context context, String name) {
17361784
return true;
17371785
}
17381786

1739-
private static void validateAliasOrIndex(ExpressionList.Expression expression) {
1740-
if (Strings.isEmpty(expression.expression())) {
1741-
throw notFoundException(expression.expression());
1742-
}
1743-
// Expressions can not start with an underscore. This is reserved for APIs. If the check gets here, the API
1744-
// does not exist and the path is interpreted as an expression. If the expression begins with an underscore,
1745-
// throw a specific error that is different from the [[IndexNotFoundException]], which is typically thrown
1746-
// if the expression can't be found.
1747-
if (expression.expression().charAt(0) == '_') {
1748-
throw new InvalidIndexNameException(expression.expression(), "must not start with '_'.");
1749-
}
1750-
}
1751-
17521787
private static void ensureRemoteIndicesRequireIgnoreUnavailable(IndicesOptions options, List<String> indexExpressions) {
17531788
if (options.ignoreUnavailable()) {
17541789
return;
@@ -1773,57 +1808,6 @@ private static void failOnRemoteIndicesNotIgnoringUnavailable(List<String> index
17731808
}
17741809
}
17751810

1776-
/**
1777-
* Used to iterate expression lists and work out which expression item is a wildcard or an exclusion.
1778-
*/
1779-
public static final class ExpressionList implements Iterable<ExpressionList.Expression> {
1780-
private final List<Expression> expressionsList;
1781-
private final boolean hasWildcard;
1782-
1783-
public record Expression(String expression, boolean isWildcard, boolean isExclusion) {
1784-
public String get() {
1785-
if (isExclusion()) {
1786-
// drop the leading "-" if exclusion because it is easier for callers to handle it like this
1787-
return expression().substring(1);
1788-
} else {
1789-
return expression();
1790-
}
1791-
}
1792-
}
1793-
1794-
/**
1795-
* Creates the expression iterable that can be used to easily check which expression item is a wildcard or an exclusion (or both).
1796-
* The {@param context} is used to check if wildcards ought to be considered or not.
1797-
*/
1798-
public ExpressionList(Context context, List<String> expressionStrings) {
1799-
List<Expression> expressionsList = new ArrayList<>(expressionStrings.size());
1800-
boolean wildcardSeen = false;
1801-
for (String expressionString : expressionStrings) {
1802-
boolean isExclusion = expressionString.startsWith("-") && wildcardSeen;
1803-
if (context.getOptions().expandWildcardExpressions() && isWildcard(expressionString)) {
1804-
wildcardSeen = true;
1805-
expressionsList.add(new Expression(expressionString, true, isExclusion));
1806-
} else {
1807-
expressionsList.add(new Expression(expressionString, false, isExclusion));
1808-
}
1809-
}
1810-
this.expressionsList = expressionsList;
1811-
this.hasWildcard = wildcardSeen;
1812-
}
1813-
1814-
/**
1815-
* Returns {@code true} if the expression contains any wildcard and the options allow wildcard expansion
1816-
*/
1817-
public boolean hasWildcard() {
1818-
return this.hasWildcard;
1819-
}
1820-
1821-
@Override
1822-
public Iterator<ExpressionList.Expression> iterator() {
1823-
return expressionsList.iterator();
1824-
}
1825-
}
1826-
18271811
/**
18281812
* This is a context for the DateMathExpressionResolver which does not require {@code IndicesOptions} or {@code ClusterState}
18291813
* since it uses only the start time to resolve expressions.

0 commit comments

Comments
 (0)