Skip to content

Commit 91fe8ff

Browse files
authored
Merge pull request #2963 from newrelic/jdbc-generic-nf-replacement
Jdbc generic new fields replacement
2 parents f64bc35 + 31312f8 commit 91fe8ff

8 files changed

Lines changed: 186 additions & 71 deletions

File tree

.fleetControl/schemaGeneration/reference-newrelic.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@ common: &default_settings
690690
# Default: 10
691691
gc_cpu_threshold: 10
692692

693+
# JDBC Statements are cached in a weak key map by default. Set enabled:false to use a vanilla ConcurrentHashMap instead.
694+
# Users who set enabled:false MUST call Statement.close() on every JDBC Statement.
695+
jdbc_statement_weak_key_caching:
696+
enabled: true
697+
693698
# Class transformer can be used to disable all agent instrumentation or specific instrumentation modules.
694699
# All instrumentation modules can be found here: https://github.com/newrelic/newrelic-java-agent/tree/main/instrumentation
695700
class_transformer:

.fleetControl/schemas/config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,17 @@
11971197
"additionalProperties": true,
11981198
"description": "Circuit breaker protection settings."
11991199
},
1200+
"jdbc_statement_weak_key_caching": {
1201+
"type": "object",
1202+
"properties": {
1203+
"enabled": {
1204+
"type": "boolean",
1205+
"default": true
1206+
}
1207+
},
1208+
"additionalProperties": true,
1209+
"description": "JDBC Statements are cached in a weak key map by default. Set enabled:false to use a vanilla ConcurrentHashMap instead. Users who set enabled:false MUST call Statement.close() on every JDBC Statement."
1210+
},
12001211
"class_transformer": {
12011212
"type": "object",
12021213
"properties": {

agent-bridge-datastore/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
implementation(project(":agent-bridge"))
4141

4242
testImplementation('org.awaitility:awaitility:4.2.0')
43+
testImplementation("org.mockito:mockito-inline:4.11.0")
4344
testImplementation(fileTree(dir: 'src/test/resources/com/newrelic/agent/bridge', include: '*.jar'))
4445
}
4546

agent-bridge-datastore/src/main/java/com/newrelic/agent/bridge/datastore/JdbcHelper.java

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
package com.newrelic.agent.bridge.datastore;
99

1010
import com.newrelic.agent.bridge.AgentBridge;
11-
import com.newrelic.agent.bridge.NoOpTransaction;
1211
import com.newrelic.api.agent.Logger;
1312
import com.newrelic.api.agent.NewRelic;
1413

@@ -41,10 +40,25 @@ protected Boolean initialValue() {
4140
}
4241
};
4342

43+
//statement cache settings
44+
/**
45+
* In extreme high-throughput scenarios, weak keyed eviction may throttle the CPU when it tries to perform maintenance on statement caches.
46+
* Setting -Dnewrelic.config.jdbc_statement_weak_key_caching.enabled=false will convert the caches to ordinary ConcurrentHashMaps
47+
* to alleviate this maintenance overhead.
48+
* <p>
49+
* Statements are only removed from the caches on Statement.close(), so this config MUST be enabled with caution. If the user does not properly close their
50+
* statements, setting -Dnewrelic.config.jdbc_statement_weak_key_caching.enabled=false will cause memory issues. The user MUST explicitly call
51+
* Statement.close() (not the implicit Statement.closeOnCompletion()) for cleanup to be guaranteed.
52+
*/
53+
private static final String JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED = "jdbc_statement_weak_key_caching.enabled";
54+
private static final boolean JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED_DEFAULT = Boolean.TRUE;
55+
private static final int INITIAL_STATEMENT_CACHE_CAPACITY = 1024;
56+
private static final Map<Statement, Object[]> statementToParams = initStatementCache("statementToParams");
57+
private static final Map<Statement, String> statementToSql = initStatementCache("statementToSql");
58+
4459
// This will contain every vendor type that we detected on the client system
4560
private static final Map<String, DatabaseVendor> typeToVendorLookup = new ConcurrentHashMap<>(10);
4661
private static final Map<Class<?>, DatabaseVendor> classToVendorLookup = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
47-
private static final Map<Statement, String> statementToSql = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
4862
private static final Map<Connection, String> connectionToIdentifier = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
4963
private static final Map<Connection, String> connectionToURL = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
5064
public static final String UNKNOWN = "unknown";
@@ -58,6 +72,31 @@ protected Boolean initialValue() {
5872
private static volatile Boolean isSqlMetadataCommentsEnabled = null;
5973
private static volatile String cachedServiceGuid = null;
6074

75+
public static Object[] getParams(Statement statement) {
76+
return statementToParams.get(statement);
77+
}
78+
79+
public static void putParams(Statement statement, Object[] params) {
80+
statementToParams.put(statement, params);
81+
}
82+
83+
public static String getSql(Statement statement) {
84+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting sql for statement: {0}", statement);
85+
return statementToSql.get(statement);
86+
}
87+
88+
public static void putSql(Statement statement, String sql) {
89+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing sql for statement: {0}", statement);
90+
statementToSql.put(statement, sql);
91+
}
92+
93+
public static void removeStatement(Statement statement) {
94+
if (statement != null) {
95+
statementToParams.remove(statement);
96+
statementToSql.remove(statement);
97+
}
98+
}
99+
61100
public static void putVendor(Class<?> driverOrDatastoreClass, DatabaseVendor databaseVendor) {
62101
classToVendorLookup.put(driverOrDatastoreClass, databaseVendor);
63102
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing class: {0}, vendor: {1}", driverOrDatastoreClass, databaseVendor);
@@ -68,7 +107,7 @@ public static void putVendor(Class<?> driverOrDatastoreClass, DatabaseVendor dat
68107

69108
public static DatabaseVendor getVendor(Class<?> driverOrDatastoreClass, String url) {
70109
DatabaseVendor vendor = classToVendorLookup.get(driverOrDatastoreClass);
71-
AgentBridge.getAgent().getLogger().log(Level.FINEST,"Getting class: {0}, url: {1}, vendor: {2}", driverOrDatastoreClass, url, vendor);
110+
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting class: {0}, url: {1}, vendor: {2}", driverOrDatastoreClass, url, vendor);
72111

73112
if (vendor != null) {
74113
return vendor;
@@ -150,16 +189,6 @@ public static String getCachedDatabaseName(Connection connection) {
150189
return null;
151190
}
152191

153-
public static void putSql(Statement statement, String sql) {
154-
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Storing sql for statement: {0}", statement);
155-
statementToSql.put(statement, sql);
156-
}
157-
158-
public static String getSql(Statement statement) {
159-
AgentBridge.getAgent().getLogger().log(Level.FINEST, "Getting sql for statement: {0}", statement);
160-
return statementToSql.get(statement);
161-
}
162-
163192
public static Object[] growParameterArray(Object[] params, int missingIndex) {
164193
int length = Math.max(10, (int) (missingIndex * 1.2));
165194
Object[] newParams = new Object[length];
@@ -224,7 +253,7 @@ public static String getConnectionURL(Connection connection) {
224253
*
225254
* @param connectionString URL connection string to parse.
226255
* @return identifier parsed from connection string if vendor is part of supported in-memory JDBC drivers,
227-
* {@link JdbcHelper#UNKNOWN} otherwise.
256+
* {@link JdbcHelper#UNKNOWN} otherwise.
228257
*/
229258
public static String parseInMemoryIdentifier(String connectionString) {
230259
if (connectionString == null) {
@@ -257,8 +286,6 @@ public static String parseInMemoryIdentifier(String connectionString) {
257286
return UNKNOWN;
258287
}
259288

260-
261-
262289
/**
263290
* Parse and cache identifier of in-memory database from Connection string url.
264291
*
@@ -325,7 +352,6 @@ public static void invalidateMetadataCommentConfig() {
325352
* the original SQL statement.
326353
*
327354
* @param sql the target SQL statement
328-
*
329355
* @return a SQL statement which might have the metadata comment prepended to it
330356
*/
331357
public static String addSqlMetadataCommentIfNeeded(String sql) {
@@ -412,6 +438,29 @@ private static Boolean isSqlMetadataCommentsEnabled() {
412438
return (isSqlMetadataCommentsEnabled != null) ? isSqlMetadataCommentsEnabled : Boolean.FALSE;
413439
}
414440

441+
/**
442+
* Checks the config to see whether weak key caching is enabled.
443+
* <p>
444+
* If weak key caching is enabled (default) a Caffeine-backed Weak Keyed cache will be returned.
445+
* If weak key caching is not enabled a vanilla Concurrent Hash Map will be returned.
446+
*/
447+
static <V> Map<Statement, V> initStatementCache(String cacheName) {
448+
boolean weakKeyCachingEnabled = NewRelic.getAgent()
449+
.getConfig()
450+
.getValue(JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED, JDBC_STATEMENT_WEAK_KEY_CACHING_ENABLED_DEFAULT);
451+
if (weakKeyCachingEnabled) {
452+
NewRelic.getAgent().getLogger().log(Level.INFO, "JDBC Statement weak key caching is enabled. Using default Weak Keyed Cache for {0}.", cacheName);
453+
return AgentBridge.collectionFactory.createConcurrentWeakKeyedMap();
454+
} else {
455+
NewRelic.getAgent()
456+
.getLogger()
457+
.log(Level.INFO,
458+
"JDBC Statement weak key caching is disabled. Using a ConcurrentHashMap for {0}. All JDBC Statements MUST be closed when this setting is enabled.",
459+
cacheName);
460+
return AgentBridge.collectionFactory.createVanillaJavaConcurrentHashMap(INITIAL_STATEMENT_CACHE_CAPACITY);
461+
}
462+
}
463+
415464
/**
416465
* Retrieves the cached entity GUID value and initialize the value on first access.
417466
* If the entity GUID is not yet set (empty string), this method will retry on subsequent

agent-bridge-datastore/src/test/java/com/newrelic/agent/bridge/datastore/JdbcHelperTest.java

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@
99

1010
import com.newrelic.agent.bridge.Agent;
1111
import com.newrelic.agent.bridge.AgentBridge;
12-
import org.junit.After;
1312
import org.junit.Assert;
14-
import org.junit.Before;
1513
import org.junit.Test;
14+
import com.newrelic.api.agent.NewRelic;
15+
import org.mockito.Answers;
16+
import org.mockito.MockedStatic;
1617
import org.mockito.Mockito;
1718

1819
import java.sql.Connection;
1920
import java.sql.DatabaseMetaData;
2021
import java.sql.Driver;
2122
import java.sql.SQLException;
23+
import java.sql.Statement;
24+
import java.util.Map;
2225

2326
import static org.junit.Assert.assertEquals;
2427
import static org.junit.Assert.assertFalse;
2528
import static org.junit.Assert.assertNull;
2629
import static org.junit.Assert.assertTrue;
30+
import static org.mockito.ArgumentMatchers.any;
31+
import static org.mockito.ArgumentMatchers.eq;
32+
import static org.mockito.Mockito.mockStatic;
2733

2834
public class JdbcHelperTest {
2935

@@ -313,4 +319,68 @@ public void testEntityGuidCaching_handlesNull() {
313319
JdbcHelper.resetEntityGuidCache();
314320
}
315321
}
322+
323+
@Test
324+
public void testInitStatementCachesReturnsDefaultCacheType() {
325+
Class<?> expectedDefaultCacheType = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap().getClass();
326+
Class<?> actualDefaultCacheType = JdbcHelper.initStatementCache("test_cache").getClass();
327+
assertEquals("Default statement cache type should be a weak keyed cache", expectedDefaultCacheType, actualDefaultCacheType);
328+
}
329+
330+
@Test
331+
public void testInitStatementCacheWeakKeysDisabledReturnsVanillaCHM() {
332+
try (MockedStatic<NewRelic> nRMockedStatic = mockStatic(NewRelic.class, Answers.RETURNS_DEEP_STUBS)) {
333+
addDefaultConfigValToAgentMock(nRMockedStatic);
334+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
335+
eq("jdbc_statement_weak_key_caching.enabled"), any(Boolean.class)))
336+
.thenReturn(false);
337+
338+
Map<Statement, String> cache = JdbcHelper.initStatementCache("test_cache_disabled");
339+
340+
Class<?> expectedCacheType = AgentBridge.collectionFactory.createVanillaJavaConcurrentHashMap(1024).getClass();
341+
assertEquals("When weak key caching is disabled, should return a ConcurrentHashMap",
342+
expectedCacheType, cache.getClass());
343+
}
344+
}
345+
346+
@Test
347+
public void testInitStatementCacheWeakKeysEnabledReturnsDefaultCacheType() {
348+
try (MockedStatic<NewRelic> nRMockedStatic = mockStatic(NewRelic.class, Answers.RETURNS_DEEP_STUBS)) {
349+
addDefaultConfigValToAgentMock(nRMockedStatic);
350+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
351+
eq("jdbc_statement_weak_key_caching.enabled"), any(Boolean.class)))
352+
.thenReturn(true);
353+
354+
Map<Statement, String> cache = JdbcHelper.initStatementCache("test_cache_enabled");
355+
356+
Class<?> expectedCacheType = AgentBridge.collectionFactory.createConcurrentWeakKeyedMap().getClass();
357+
assertEquals("When weak key caching is enabled, should return a weak keyed cache",
358+
expectedCacheType, cache.getClass());
359+
}
360+
}
361+
362+
@Test
363+
public void testRemoveStatementClearsBothCaches() throws SQLException {
364+
Statement st = Mockito.mock(Statement.class);
365+
String fakeSql = "SELECT * FROM foo";
366+
Object[] fakeParams = {"bar", 6};
367+
368+
//Add the st to both caches and make sure it's in there.
369+
JdbcHelper.putSql(st, fakeSql);
370+
JdbcHelper.putParams(st, fakeParams);
371+
assertEquals(fakeSql, JdbcHelper.getSql(st));
372+
assertEquals(fakeParams, JdbcHelper.getParams(st));
373+
374+
//Remove should remove from both caches.
375+
JdbcHelper.removeStatement(st);
376+
assertNull(JdbcHelper.getSql(st));
377+
assertNull(JdbcHelper.getParams(st));
378+
}
379+
380+
//This config setting is read by JdbcHelper when it is initialized, so it needs to be added to the static mock.
381+
private void addDefaultConfigValToAgentMock(MockedStatic<NewRelic> nRMockedStatic){
382+
nRMockedStatic.when(() -> NewRelic.getAgent().getConfig().getValue(
383+
eq("jdbc_helper_cache_expire_time"), any()))
384+
.thenReturn(7200);
385+
}
316386
}

instrumentation/jdbc-generic/src/main/java/java/sql/Connection_Weaved.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,66 +18,66 @@ public abstract class Connection_Weaved {
1818
public PreparedStatement_Weaved prepareStatement(String sql) throws SQLException {
1919
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
2020
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
21-
preparedStatement.preparedSql = sql;
21+
JdbcHelper.putSql((Statement) preparedStatement, sql);
2222
return preparedStatement;
2323
}
2424

2525
public CallableStatement prepareCall(String sql) throws SQLException {
2626
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
2727
CallableStatement callableStatement = Weaver.callOriginal();
28-
((PreparedStatement_Weaved) callableStatement).preparedSql = sql;
28+
JdbcHelper.putSql(callableStatement, sql);
2929
return callableStatement;
3030
}
3131

3232
public PreparedStatement_Weaved prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
3333
throws SQLException {
3434
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
3535
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
36-
preparedStatement.preparedSql = sql;
36+
JdbcHelper.putSql((Statement) preparedStatement, sql);
3737
return preparedStatement;
3838
}
3939

4040
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
4141
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
4242
CallableStatement callableStatement = Weaver.callOriginal();
43-
((PreparedStatement_Weaved) callableStatement).preparedSql = sql;
43+
JdbcHelper.putSql(callableStatement, sql);
4444
return callableStatement;
4545
}
4646

4747
public PreparedStatement_Weaved prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
4848
int resultSetHoldability) throws SQLException {
4949
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
5050
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
51-
preparedStatement.preparedSql = sql;
51+
JdbcHelper.putSql((Statement) preparedStatement, sql);
5252
return preparedStatement;
5353
}
5454

5555
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
5656
int resultSetHoldability) throws SQLException {
5757
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
5858
CallableStatement callableStatement = Weaver.callOriginal();
59-
((PreparedStatement_Weaved) callableStatement).preparedSql = sql;
59+
JdbcHelper.putSql(callableStatement, sql);
6060
return callableStatement;
6161
}
6262

6363
public PreparedStatement_Weaved prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
6464
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
6565
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
66-
preparedStatement.preparedSql = sql;
66+
JdbcHelper.putSql((Statement) preparedStatement, sql);
6767
return preparedStatement;
6868
}
6969

7070
public PreparedStatement_Weaved prepareStatement(String sql, int[] columnIndexes) throws SQLException {
7171
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
7272
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
73-
preparedStatement.preparedSql = sql;
73+
JdbcHelper.putSql((Statement) preparedStatement, sql);
7474
return preparedStatement;
7575
}
7676

7777
public PreparedStatement_Weaved prepareStatement(String sql, String[] columnNames) throws SQLException {
7878
sql = JdbcHelper.addSqlMetadataCommentIfNeeded(sql);
7979
PreparedStatement_Weaved preparedStatement = Weaver.callOriginal();
80-
preparedStatement.preparedSql = sql;
80+
JdbcHelper.putSql((Statement) preparedStatement, sql);
8181
return preparedStatement;
8282
}
8383
}

0 commit comments

Comments
 (0)