Skip to content

Commit 340ba13

Browse files
chore: use a cache for the statement parser (#2790)
* chore: use a cache for the statement parser Add a cache for the statement parser that is used in the Connection API. This will reduce the number of times that a SQL string needs to be parsed by the JDBC driver and PGAdapter. * docs: fix/update comments * chore: add 'mb' to cache size property name * chore: use variable * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 655000a commit 340ba13

File tree

3 files changed

+177
-8
lines changed

3 files changed

+177
-8
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
2626
import com.google.common.annotations.VisibleForTesting;
2727
import com.google.common.base.Preconditions;
28+
import com.google.common.cache.Cache;
29+
import com.google.common.cache.CacheBuilder;
30+
import com.google.common.cache.CacheStats;
31+
import com.google.common.cache.Weigher;
2832
import com.google.common.collect.ImmutableMap;
2933
import com.google.common.collect.ImmutableSet;
3034
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
@@ -59,6 +63,13 @@ public abstract class AbstractStatementParser {
5963
Dialect.POSTGRESQL,
6064
PostgreSQLStatementParser.class);
6165

66+
@VisibleForTesting
67+
static void resetParsers() {
68+
synchronized (lock) {
69+
INSTANCES.clear();
70+
}
71+
}
72+
6273
/**
6374
* Get an instance of {@link AbstractStatementParser} for the specified dialect.
6475
*
@@ -171,7 +182,7 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
171182
private static ParsedStatement query(
172183
Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) {
173184
return new ParsedStatement(
174-
StatementType.QUERY, statement, sqlWithoutComments, defaultQueryOptions, false);
185+
StatementType.QUERY, null, statement, sqlWithoutComments, defaultQueryOptions, false);
175186
}
176187

177188
private static ParsedStatement update(
@@ -193,7 +204,7 @@ private ParsedStatement(
193204
this.type = StatementType.CLIENT_SIDE;
194205
this.clientSideStatement = clientSideStatement;
195206
this.statement = statement;
196-
this.sqlWithoutComments = sqlWithoutComments;
207+
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
197208
this.returningClause = false;
198209
}
199210

@@ -202,28 +213,48 @@ private ParsedStatement(
202213
Statement statement,
203214
String sqlWithoutComments,
204215
boolean returningClause) {
205-
this(type, statement, sqlWithoutComments, null, returningClause);
216+
this(type, null, statement, sqlWithoutComments, null, returningClause);
206217
}
207218

208219
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
209-
this(type, statement, sqlWithoutComments, null, false);
220+
this(type, null, statement, sqlWithoutComments, null, false);
210221
}
211222

212223
private ParsedStatement(
213224
StatementType type,
225+
ClientSideStatementImpl clientSideStatement,
214226
Statement statement,
215227
String sqlWithoutComments,
216228
QueryOptions defaultQueryOptions,
217229
boolean returningClause) {
218230
Preconditions.checkNotNull(type);
219-
Preconditions.checkNotNull(statement);
220231
this.type = type;
221-
this.clientSideStatement = null;
222-
this.statement = mergeQueryOptions(statement, defaultQueryOptions);
223-
this.sqlWithoutComments = sqlWithoutComments;
232+
this.clientSideStatement = clientSideStatement;
233+
this.statement = statement == null ? null : mergeQueryOptions(statement, defaultQueryOptions);
234+
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
224235
this.returningClause = returningClause;
225236
}
226237

238+
private ParsedStatement copy(Statement statement, QueryOptions defaultQueryOptions) {
239+
return new ParsedStatement(
240+
this.type,
241+
this.clientSideStatement,
242+
statement,
243+
this.sqlWithoutComments,
244+
defaultQueryOptions,
245+
this.returningClause);
246+
}
247+
248+
private ParsedStatement forCache() {
249+
return new ParsedStatement(
250+
this.type,
251+
this.clientSideStatement,
252+
null,
253+
this.sqlWithoutComments,
254+
null,
255+
this.returningClause);
256+
}
257+
227258
@Override
228259
public int hashCode() {
229260
return Objects.hash(
@@ -361,8 +392,58 @@ ClientSideStatement getClientSideStatement() {
361392
static final Set<String> dmlStatements = ImmutableSet.of("INSERT", "UPDATE", "DELETE");
362393
private final Set<ClientSideStatementImpl> statements;
363394

395+
/** The default maximum size of the statement cache in Mb. */
396+
public static final int DEFAULT_MAX_STATEMENT_CACHE_SIZE_MB = 5;
397+
398+
private static int getMaxStatementCacheSize() {
399+
String stringValue = System.getProperty("spanner.statement_cache_size_mb");
400+
if (stringValue == null) {
401+
return DEFAULT_MAX_STATEMENT_CACHE_SIZE_MB;
402+
}
403+
int value = 0;
404+
try {
405+
value = Integer.parseInt(stringValue);
406+
} catch (NumberFormatException ignore) {
407+
}
408+
return Math.max(value, 0);
409+
}
410+
411+
private static boolean isRecordStatementCacheStats() {
412+
return "true"
413+
.equalsIgnoreCase(System.getProperty("spanner.record_statement_cache_stats", "false"));
414+
}
415+
416+
/**
417+
* Cache for parsed statements. This prevents statements that are executed multiple times by the
418+
* application to be parsed over and over again. The default maximum size is 5Mb.
419+
*/
420+
private final Cache<String, ParsedStatement> statementCache;
421+
364422
AbstractStatementParser(Set<ClientSideStatementImpl> statements) {
365423
this.statements = Collections.unmodifiableSet(statements);
424+
int maxCacheSize = getMaxStatementCacheSize();
425+
if (maxCacheSize > 0) {
426+
CacheBuilder<String, ParsedStatement> cacheBuilder =
427+
CacheBuilder.newBuilder()
428+
// Set the max size to (approx) 5MB (by default).
429+
.maximumWeight(maxCacheSize * 1024L * 1024L)
430+
// We do length*2 because Java uses 2 bytes for each char.
431+
.weigher(
432+
(Weigher<String, ParsedStatement>)
433+
(key, value) -> 2 * key.length() + 2 * value.sqlWithoutComments.length())
434+
.concurrencyLevel(Runtime.getRuntime().availableProcessors());
435+
if (isRecordStatementCacheStats()) {
436+
cacheBuilder.recordStats();
437+
}
438+
this.statementCache = cacheBuilder.build();
439+
} else {
440+
this.statementCache = null;
441+
}
442+
}
443+
444+
@VisibleForTesting
445+
CacheStats getStatementCacheStats() {
446+
return statementCache == null ? null : statementCache.stats();
366447
}
367448

368449
@VisibleForTesting
@@ -383,6 +464,20 @@ public ParsedStatement parse(Statement statement) {
383464
}
384465

385466
ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
467+
if (statementCache == null) {
468+
return internalParse(statement, defaultQueryOptions);
469+
}
470+
471+
ParsedStatement parsedStatement = statementCache.getIfPresent(statement.getSql());
472+
if (parsedStatement == null) {
473+
parsedStatement = internalParse(statement, null);
474+
statementCache.put(statement.getSql(), parsedStatement.forCache());
475+
return parsedStatement;
476+
}
477+
return parsedStatement.copy(statement, defaultQueryOptions);
478+
}
479+
480+
private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
386481
String sql = removeCommentsAndTrim(statement.getSql());
387482
ClientSideStatementImpl client = parseClientSideStatement(sql);
388483
if (client != null) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ public void stressTest() throws Exception {
294294
() -> {
295295
while (!stopMaintenance.get()) {
296296
runMaintenanceLoop(clock, pool, 1);
297+
Uninterruptibles.sleepUninterruptibly(1L, TimeUnit.MILLISECONDS);
297298
}
298299
})
299300
.start();

google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.hamcrest.MatcherAssert.assertThat;
2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertFalse;
25+
import static org.junit.Assert.assertNotSame;
2526
import static org.junit.Assert.assertTrue;
2627
import static org.junit.Assert.fail;
2728
import static org.junit.Assume.assumeTrue;
@@ -34,6 +35,7 @@
3435
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
3536
import com.google.cloud.spanner.connection.ClientSideStatementImpl.CompileException;
3637
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
38+
import com.google.common.cache.CacheStats;
3739
import com.google.common.collect.ImmutableMap;
3840
import com.google.common.collect.ImmutableSet;
3941
import com.google.common.truth.Truth;
@@ -43,9 +45,12 @@
4345
import java.util.List;
4446
import java.util.Scanner;
4547
import java.util.Set;
48+
import java.util.UUID;
4649
import java.util.regex.Matcher;
4750
import java.util.regex.Pattern;
51+
import org.junit.AfterClass;
4852
import org.junit.Before;
53+
import org.junit.BeforeClass;
4954
import org.junit.Test;
5055
import org.junit.runner.RunWith;
5156
import org.junit.runners.Parameterized;
@@ -81,6 +86,17 @@ public static Object[] data() {
8186

8287
private AbstractStatementParser parser;
8388

89+
@BeforeClass
90+
public static void enableStatementCacheStats() {
91+
AbstractStatementParser.resetParsers();
92+
System.setProperty("spanner.record_statement_cache_stats", "true");
93+
}
94+
95+
@AfterClass
96+
public static void disableStatementCacheStats() {
97+
System.clearProperty("spanner.record_statement_cache_stats");
98+
}
99+
84100
@Before
85101
public void setupParser() {
86102
parser = AbstractStatementParser.getInstance(dialect);
@@ -1641,6 +1657,63 @@ public void testSkipMultiLineComment() {
16411657
skipMultiLineComment("/* foo /* inner comment */ not in inner comment */ bar", 0));
16421658
}
16431659

1660+
@Test
1661+
public void testStatementCache_NonParameterizedStatement() {
1662+
CacheStats statsBefore = parser.getStatementCacheStats();
1663+
1664+
String sql = "select foo from bar where id=" + UUID.randomUUID();
1665+
ParsedStatement parsedStatement1 = parser.parse(Statement.of(sql));
1666+
assertEquals(StatementType.QUERY, parsedStatement1.getType());
1667+
1668+
ParsedStatement parsedStatement2 = parser.parse(Statement.of(sql));
1669+
assertEquals(StatementType.QUERY, parsedStatement2.getType());
1670+
1671+
// Even though the parsed statements are cached, the returned instances are not the same.
1672+
// This makes sure that statements with the same SQL string and different parameter values
1673+
// can use the cache.
1674+
assertNotSame(parsedStatement1, parsedStatement2);
1675+
1676+
CacheStats statsAfter = parser.getStatementCacheStats();
1677+
CacheStats stats = statsAfter.minus(statsBefore);
1678+
1679+
// The first query had a cache miss. The second a cache hit.
1680+
assertEquals(1, stats.missCount());
1681+
assertEquals(1, stats.hitCount());
1682+
}
1683+
1684+
@Test
1685+
public void testStatementCache_ParameterizedStatement() {
1686+
CacheStats statsBefore = parser.getStatementCacheStats();
1687+
1688+
String sql =
1689+
"select "
1690+
+ UUID.randomUUID()
1691+
+ " from bar where id="
1692+
+ (dialect == Dialect.POSTGRESQL ? "$1" : "@p1");
1693+
Statement statement1 = Statement.newBuilder(sql).bind("p1").to(1L).build();
1694+
Statement statement2 = Statement.newBuilder(sql).bind("p1").to(2L).build();
1695+
1696+
ParsedStatement parsedStatement1 = parser.parse(statement1);
1697+
assertEquals(StatementType.QUERY, parsedStatement1.getType());
1698+
assertEquals(parsedStatement1.getStatement(), statement1);
1699+
1700+
ParsedStatement parsedStatement2 = parser.parse(statement2);
1701+
assertEquals(StatementType.QUERY, parsedStatement2.getType());
1702+
assertEquals(parsedStatement2.getStatement(), statement2);
1703+
1704+
// Even though the parsed statements are cached, the returned instances are not the same.
1705+
// This makes sure that statements with the same SQL string and different parameter values
1706+
// can use the cache.
1707+
assertNotSame(parsedStatement1, parsedStatement2);
1708+
1709+
CacheStats statsAfter = parser.getStatementCacheStats();
1710+
CacheStats stats = statsAfter.minus(statsBefore);
1711+
1712+
// The first query had a cache miss. The second a cache hit.
1713+
assertEquals(1, stats.missCount());
1714+
assertEquals(1, stats.hitCount());
1715+
}
1716+
16441717
private void assertUnclosedLiteral(String sql) {
16451718
try {
16461719
parser.convertPositionalParametersToNamedParameters('?', sql);

0 commit comments

Comments
 (0)