Skip to content

Commit 35105a0

Browse files
authored
Share parser execution and preserve cancellation signals (#2626)
1 parent a2baa18 commit 35105a0

3 files changed

Lines changed: 174 additions & 30 deletions

File tree

src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.io.Reader;
1515
import java.util.Stack;
1616
import java.util.concurrent.Callable;
17+
import java.util.concurrent.CancellationException;
18+
import java.util.concurrent.ExecutionException;
1719
import java.util.concurrent.ExecutorService;
1820
import java.util.concurrent.Executors;
1921
import java.util.concurrent.Future;
@@ -129,6 +131,9 @@ public static Statement parse(String sql, ExecutorService executorService,
129131
LOGGER.info("Trying SIMPLE parsing " + (allowComplex ? "first" : "only"));
130132
statement = parseStatement(parser.withAllowComplexParsing(false), executorService);
131133
} catch (JSQLParserException ex) {
134+
if (wasExecutionStopped(ex)) {
135+
throw ex;
136+
}
132137
LOGGER.info("Nesting Depth" + getNestingDepth(sql));
133138
if (allowComplex
134139
&& (allowedNestingDepth < 0 || getNestingDepth(sql) <= allowedNestingDepth)) {
@@ -407,24 +412,7 @@ public static Expression parseCondExpression(String conditionalExpressionStr,
407412

408413
public static Statement parseStatement(CCJSqlParser parser, ExecutorService executorService)
409414
throws JSQLParserException {
410-
Statement statement;
411-
Future<Statement> future = executorService.submit(new Callable<Statement>() {
412-
@Override
413-
public Statement call() throws ParseException {
414-
return parser.Statement();
415-
}
416-
});
417-
try {
418-
statement = future.get(parser.getAsLong(Feature.timeOut),
419-
TimeUnit.MILLISECONDS);
420-
} catch (TimeoutException ex) {
421-
parser.interrupted = true;
422-
future.cancel(true);
423-
throw new JSQLParserException("Time out occurred.", ex);
424-
} catch (Exception ex) {
425-
throw new JSQLParserException(ex);
426-
}
427-
return statement;
415+
return executeParser(parser, executorService, parser::Statement);
428416
}
429417

430418
/**
@@ -478,6 +466,9 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe
478466
try {
479467
return parseStatements(parser.withAllowComplexParsing(false), executorService);
480468
} catch (JSQLParserException ex) {
469+
if (wasExecutionStopped(ex)) {
470+
throw ex;
471+
}
481472
// when fast simple parsing fails, try complex parsing but only if it has a chance to
482473
// succeed
483474
if (allowComplex
@@ -502,24 +493,35 @@ public static Statements parseStatements(String sqls, ExecutorService executorSe
502493
*/
503494
public static Statements parseStatements(CCJSqlParser parser, ExecutorService executorService)
504495
throws JSQLParserException {
505-
Statements statements = null;
506-
Future<Statements> future = executorService.submit(new Callable<Statements>() {
507-
@Override
508-
public Statements call() throws ParseException {
509-
return parser.Statements();
510-
}
511-
});
496+
return executeParser(parser, executorService, parser::Statements);
497+
}
498+
499+
private static <T> T executeParser(CCJSqlParser parser, ExecutorService executorService,
500+
Callable<T> operation) throws JSQLParserException {
501+
Future<T> future = executorService.submit(operation);
512502
try {
513-
statements = future.get(parser.getAsLong(Feature.timeOut),
514-
TimeUnit.MILLISECONDS);
503+
return future.get(parser.getAsLong(Feature.timeOut), TimeUnit.MILLISECONDS);
504+
} catch (InterruptedException ex) {
505+
parser.interrupted = true;
506+
future.cancel(true);
507+
Thread.currentThread().interrupt();
508+
throw new JSQLParserException(ex);
515509
} catch (TimeoutException ex) {
516510
parser.interrupted = true;
517511
future.cancel(true);
518512
throw new JSQLParserException("Time out occurred.", ex);
519-
} catch (Exception ex) {
513+
} catch (CancellationException ex) {
514+
parser.interrupted = true;
515+
throw new JSQLParserException(ex);
516+
} catch (ExecutionException ex) {
520517
throw new JSQLParserException(ex);
521518
}
522-
return statements;
519+
}
520+
521+
private static boolean wasExecutionStopped(JSQLParserException exception) {
522+
Throwable cause = exception.getCause();
523+
return cause instanceof InterruptedException || cause instanceof TimeoutException
524+
|| cause instanceof CancellationException;
523525
}
524526

525527
public static void streamStatements(StatementListener listener, InputStream is, String encoding)

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
100100
public final static Logger LOGGER = Logger.getLogger(CCJSqlParser.class.getName());
101101
public int bracketsCounter = 0;
102102
public int caseCounter = 0;
103-
public boolean interrupted = false;
103+
public volatile boolean interrupted = false;
104104

105105
public CCJSqlParser withConfiguration(FeatureConfiguration configuration) {
106106
token_source.configuration = configuration;
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.util.List;
14+
import java.util.concurrent.AbstractExecutorService;
15+
import java.util.concurrent.Future;
16+
import java.util.concurrent.TimeUnit;
17+
import java.util.concurrent.TimeoutException;
18+
import java.util.concurrent.Executors;
19+
import net.sf.jsqlparser.JSQLParserException;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
24+
class ParserExecutionTest {
25+
private static class QueuedExecutor extends AbstractExecutorService {
26+
Future<?> task;
27+
int submissions;
28+
boolean shutdown;
29+
30+
@Override
31+
public void execute(Runnable command) {
32+
task = (Future<?>) command;
33+
submissions++;
34+
}
35+
36+
@Override
37+
public void shutdown() {
38+
shutdown = true;
39+
}
40+
41+
@Override
42+
public List<Runnable> shutdownNow() {
43+
shutdown();
44+
return List.of();
45+
}
46+
47+
@Override
48+
public boolean isShutdown() {
49+
return shutdown;
50+
}
51+
52+
@Override
53+
public boolean isTerminated() {
54+
return shutdown;
55+
}
56+
57+
@Override
58+
public boolean awaitTermination(long timeout, TimeUnit unit) {
59+
return shutdown;
60+
}
61+
}
62+
63+
@ParameterizedTest
64+
@ValueSource(booleans = {false, true})
65+
void cancelsTheSubmittedTaskAndPreservesCallerInterruption(boolean multiple) {
66+
QueuedExecutor executor = new QueuedExecutor();
67+
CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1");
68+
Thread.currentThread().interrupt();
69+
try {
70+
JSQLParserException error = assertThrows(JSQLParserException.class, () -> {
71+
if (multiple) {
72+
CCJSqlParserUtil.parseStatements(parser, executor);
73+
} else {
74+
CCJSqlParserUtil.parseStatement(parser, executor);
75+
}
76+
});
77+
assertInstanceOf(InterruptedException.class, error.getCause());
78+
assertTrue(Thread.currentThread().isInterrupted());
79+
assertTrue(parser.interrupted);
80+
assertTrue(executor.task.isCancelled());
81+
assertFalse(executor.isShutdown());
82+
} finally {
83+
Thread.interrupted();
84+
}
85+
}
86+
87+
@ParameterizedTest
88+
@ValueSource(booleans = {false, true})
89+
void doesNotRetryInterruptedConvenienceCalls(boolean multiple) {
90+
QueuedExecutor executor = new QueuedExecutor();
91+
Thread.currentThread().interrupt();
92+
try {
93+
JSQLParserException error = assertThrows(JSQLParserException.class, () -> {
94+
if (multiple) {
95+
CCJSqlParserUtil.parseStatements("SELECT 1", executor, null);
96+
} else {
97+
CCJSqlParserUtil.parse("SELECT 1", executor, null);
98+
}
99+
});
100+
assertInstanceOf(InterruptedException.class, error.getCause());
101+
assertEquals(1, executor.submissions);
102+
assertTrue(executor.task.isCancelled());
103+
assertTrue(Thread.currentThread().isInterrupted());
104+
} finally {
105+
Thread.interrupted();
106+
}
107+
}
108+
109+
@ParameterizedTest
110+
@ValueSource(booleans = {false, true})
111+
void timeoutCancelsOneTaskWithoutRetryingOrInterruptingTheCaller(boolean multiple) {
112+
QueuedExecutor executor = new QueuedExecutor();
113+
JSQLParserException error = assertThrows(JSQLParserException.class, () -> {
114+
if (multiple) {
115+
CCJSqlParserUtil.parseStatements("SELECT 1", executor,
116+
parser -> parser.withTimeOut(0));
117+
} else {
118+
CCJSqlParserUtil.parse("SELECT 1", executor, parser -> parser.withTimeOut(0));
119+
}
120+
});
121+
assertInstanceOf(TimeoutException.class, error.getCause());
122+
assertEquals(1, executor.submissions);
123+
assertTrue(executor.task.isCancelled());
124+
assertFalse(Thread.currentThread().isInterrupted());
125+
assertFalse(executor.isShutdown());
126+
}
127+
128+
@Test
129+
void successfulAndInvalidParsingKeepTheCallerExecutorUsable() throws Exception {
130+
var executor = Executors.newSingleThreadExecutor();
131+
try {
132+
assertEquals("SELECT 1", CCJSqlParserUtil.parse("SELECT 1", executor, null).toString());
133+
assertThrows(JSQLParserException.class,
134+
() -> CCJSqlParserUtil.parse("SELECT FROM", executor, null));
135+
assertEquals("SELECT 2;\n",
136+
CCJSqlParserUtil.parseStatements("SELECT 2", executor, null).toString());
137+
assertFalse(executor.isShutdown());
138+
} finally {
139+
executor.shutdownNow();
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)