Skip to content

Commit ff67d02

Browse files
committed
Introduce CircuitBreaker in the Parser
1 parent 782a71d commit ff67d02

File tree

3 files changed

+32
-20
lines changed

3 files changed

+32
-20
lines changed

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/SqlParser.java

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public LogicalPlan createStatement(String sql, List<SqlTypedParamValue> params)
6363
if (log.isDebugEnabled()) {
6464
log.debug("Parsing as statement: {}", sql);
6565
}
66-
return invokeParser("statement", sql, params, SqlBaseParser::singleStatement, AstBuilder::plan);
66+
return invokeParser(sql, params, SqlBaseParser::singleStatement, AstBuilder::plan);
6767
}
6868

6969
/**
@@ -81,11 +81,10 @@ public Expression createExpression(String expression, List<SqlTypedParamValue> p
8181
log.debug("Parsing as expression: {}", expression);
8282
}
8383

84-
return invokeParser("expression", expression, params, SqlBaseParser::singleExpression, AstBuilder::expression);
84+
return invokeParser(expression, params, SqlBaseParser::singleExpression, AstBuilder::expression);
8585
}
8686

87-
private <T> T invokeParser(String name,
88-
String sql,
87+
private <T> T invokeParser(String sql,
8988
List<SqlTypedParamValue> params, Function<SqlBaseParser,
9089
ParserRuleContext> parseFunction,
9190
BiFunction<AstBuilder, ParserRuleContext, T> visitor) {
@@ -100,6 +99,7 @@ private <T> T invokeParser(String name,
10099
CommonTokenStream tokenStream = new CommonTokenStream(tokenSource);
101100
SqlBaseParser parser = new SqlBaseParser(tokenStream);
102101

102+
parser.addParseListener(new CircuitBreakerProcessor());
103103
parser.addParseListener(new PostProcessor(Arrays.asList(parser.getRuleNames())));
104104

105105
parser.removeErrorListeners();
@@ -126,11 +126,7 @@ private <T> T invokeParser(String name,
126126
log.info("Parse tree {} " + tree.toStringTree());
127127
}
128128

129-
try {
130-
return visitor.apply(new AstBuilder(paramTokens), tree);
131-
} catch (StackOverflowError e) {
132-
throw new ParsingException("{} is too large to parse (causes stack overflow)", name);
133-
}
129+
return visitor.apply(new AstBuilder(paramTokens), tree);
134130
}
135131

136132
private static void debug(SqlBaseParser parser) {
@@ -162,7 +158,7 @@ private class PostProcessor extends SqlBaseBaseListener {
162158
public void exitBackQuotedIdentifier(SqlBaseParser.BackQuotedIdentifierContext context) {
163159
Token token = context.BACKQUOTED_IDENTIFIER().getSymbol();
164160
throw new ParsingException(
165-
"backquoted indetifiers not supported; please use double quotes instead",
161+
"backquoted identifiers not supported; please use double quotes instead",
166162
null,
167163
token.getLine(),
168164
token.getCharPositionInLine());
@@ -213,6 +209,24 @@ public void exitNonReserved(SqlBaseParser.NonReservedContext context) {
213209
}
214210
}
215211

212+
/**
213+
* Used to catch large expressions that can lead to stack overflows
214+
*/
215+
private class CircuitBreakerProcessor extends SqlBaseBaseListener {
216+
217+
private static final short MAX_BOOLEAN_ELEMENTS = 1000;
218+
private short countElementsInBooleanExpressions = 0;
219+
220+
@Override
221+
public void enterLogicalBinary(SqlBaseParser.LogicalBinaryContext ctx) {
222+
if (++countElementsInBooleanExpressions == MAX_BOOLEAN_ELEMENTS) {
223+
throw new ParsingException("boolean expression is too large to parse, (exceeds {} elements)",
224+
MAX_BOOLEAN_ELEMENTS);
225+
}
226+
super.enterLogicalBinary(ctx);
227+
}
228+
}
229+
216230
private static final BaseErrorListener ERROR_LISTENER = new BaseErrorListener() {
217231
@Override
218232
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/QuotingTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void testBackQuotedAttribute() {
7070
String name = "@timestamp";
7171
ParsingException ex = expectThrows(ParsingException.class, () ->
7272
new SqlParser().createExpression(quote + name + quote));
73-
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted indetifiers not supported; please use double quotes instead"));
73+
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted identifiers not supported; please use double quotes instead"));
7474
}
7575

7676
public void testQuotedAttributeAndQualifier() {
@@ -92,7 +92,7 @@ public void testBackQuotedAttributeAndQualifier() {
9292
String name = "@timestamp";
9393
ParsingException ex = expectThrows(ParsingException.class, () ->
9494
new SqlParser().createExpression(quote + qualifier + quote + "." + quote + name + quote));
95-
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted indetifiers not supported; please use double quotes instead"));
95+
assertThat(ex.getMessage(), equalTo("line 1:1: backquoted identifiers not supported; please use double quotes instead"));
9696
}
9797

9898
public void testGreedyQuoting() {

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,14 @@ public void testMultiMatchQuery() {
138138
assertThat(mmqp.optionMap(), hasEntry("fuzzy_rewrite", "scoring_boolean"));
139139
}
140140

141-
public void testStackOverflowStatement() {
142-
ParsingException e = expectThrows(ParsingException.class, () ->
143-
parseStatement("SELECT " + Joiner.on(" OR ").join(nCopies(2000, "a = b"))));
144-
assertEquals("statement is too large to parse (causes stack overflow)", e.getErrorMessage());
145-
}
141+
public void testLimitToPreventStackOverflowFromLargeBooleanExpression() {
142+
// 1000 elements is ok
143+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(1000, "a = b")));
146144

147-
public void testStackOverflowExpression() {
145+
// 1001 elements parser's "circuit breaker" is triggered
148146
ParsingException e = expectThrows(ParsingException.class, () ->
149-
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(2000, "a = b"))));
150-
assertEquals("expression is too large to parse (causes stack overflow)", e.getErrorMessage());
147+
new SqlParser().createExpression(Joiner.on(" OR ").join(nCopies(1001, "a = b"))));
148+
assertEquals("boolean expression is too large to parse, (exceeds 1000 elements)", e.getErrorMessage());
151149
}
152150

153151
private LogicalPlan parseStatement(String sql) {

0 commit comments

Comments
 (0)