@@ -6,6 +6,11 @@ import datadog.trace.api.Config
6
6
import datadog.trace.api.DDSpanTypes
7
7
import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags
8
8
import datadog.trace.bootstrap.instrumentation.api.Tags
9
+ import org.apache.commons.dbcp2.BasicDataSource
10
+ import org.apache.commons.pool2.PooledObject
11
+ import org.apache.commons.pool2.PooledObjectFactory
12
+ import org.apache.commons.pool2.impl.DefaultPooledObject
13
+ import org.apache.commons.pool2.impl.GenericObjectPool
9
14
import org.apache.derby.jdbc.EmbeddedDataSource
10
15
import org.h2.jdbcx.JdbcDataSource
11
16
import spock.lang.Shared
@@ -18,11 +23,16 @@ import java.sql.Connection
18
23
import java.sql.Driver
19
24
import java.sql.PreparedStatement
20
25
import java.sql.ResultSet
26
+ import java.sql.SQLException
27
+ import java.sql.SQLTimeoutException
28
+ import java.sql.SQLTransientConnectionException
21
29
import java.sql.Statement
30
+ import java.time.Duration
22
31
23
32
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
24
33
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
25
34
import static datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE
35
+ import static datadog.trace.api.config.TraceInstrumentationConfig.JDBC_POOL_WAITING_ENABLED
26
36
27
37
abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
28
38
@@ -97,6 +107,22 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
97
107
return ds
98
108
}
99
109
110
+ def createDbcp2DS (String dbType , String jdbcUrl ) {
111
+ BasicDataSource ds = new BasicDataSource ()
112
+ def jdbcUrlToSet = dbType == " derby" ? jdbcUrl + " ;create=true" : jdbcUrl
113
+ ds. setUrl(jdbcUrlToSet)
114
+ ds. setDriverClassName(jdbcDriverClassNames. get(dbType))
115
+ String username = jdbcUserNames. get(dbType)
116
+ if (username != null ) {
117
+ ds. setUsername(username)
118
+ }
119
+ ds. setPassword(jdbcPasswords. get(dbType))
120
+ ds. setMaxTotal(1 ) // to test proper caching, having > 1 max active connection will be hard to
121
+ // determine whether the connection is properly cached
122
+ ds. setMaxWait(Duration . ofMillis(1000 ))
123
+ return ds
124
+ }
125
+
100
126
def createHikariDS (String dbType , String jdbcUrl ) {
101
127
HikariConfig config = new HikariConfig ()
102
128
def jdbcUrlToSet = dbType == " derby" ? jdbcUrl + " ;create=true" : jdbcUrl
@@ -110,6 +136,7 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
110
136
config. addDataSourceProperty(" prepStmtCacheSize" , " 250" )
111
137
config. addDataSourceProperty(" prepStmtCacheSqlLimit" , " 2048" )
112
138
config. setMaximumPoolSize(1 )
139
+ config. setConnectionTimeout(1000 )
113
140
114
141
return new HikariDataSource (config)
115
142
}
@@ -133,6 +160,9 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
133
160
if (connectionPoolName == " tomcat" ) {
134
161
ds = createTomcatDS(dbType, jdbcUrl)
135
162
}
163
+ if (connectionPoolName == " dbcp2" ) {
164
+ ds = createDbcp2DS(dbType, jdbcUrl)
165
+ }
136
166
if (connectionPoolName == " hikari" ) {
137
167
ds = createHikariDS(dbType, jdbcUrl)
138
168
}
@@ -148,6 +178,7 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
148
178
149
179
injectSysConfig(" dd.trace.jdbc.prepared.statement.class.name" , " test.TestPreparedStatement" )
150
180
injectSysConfig(" dd.integration.jdbc-datasource.enabled" , " true" )
181
+ injectSysConfig(JDBC_POOL_WAITING_ENABLED , " true" )
151
182
}
152
183
153
184
def setupSpec () {
@@ -814,6 +845,151 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
814
845
" c3p0" | _
815
846
}
816
847
848
+ def " #connectionPoolName should have pool. waiting span when pool exhausted for #exhaustPoolForMillis with exception thrown #expectException" () {
849
+ setup:
850
+ String dbType = " hsqldb"
851
+ DataSource ds = createDS(connectionPoolName, dbType, jdbcUrls.get(dbType))
852
+
853
+ if (exhaustPoolForMillis != null) {
854
+ def saturatedConnection = ds.getConnection()
855
+ new Thread(() -> {
856
+ Thread.sleep(exhaustPoolForMillis)
857
+ saturatedConnection.close()
858
+ }, " saturated connection closer" ).start()
859
+ }
860
+
861
+ when:
862
+ Throwable timedOutException = null
863
+ runUnderTrace(" parent" ) {
864
+ try {
865
+ ds.getConnection().close()
866
+ } catch (SQLTransientConnectionException e) {
867
+ if (e.getMessage().contains(" request timed out after" )) {
868
+ // Hikari, newer
869
+ timedOutException = e
870
+ } else {
871
+ throw e
872
+ }
873
+ } catch (SQLTimeoutException e) {
874
+ // Hikari, older
875
+ timedOutException = e
876
+ } catch (SQLException e) {
877
+ if (e.getMessage().contains(" pool error Timeout waiting for idle object" )) {
878
+ // dbcp2
879
+ timedOutException = e
880
+ } else {
881
+ throw e
882
+ }
883
+ }
884
+ }
885
+
886
+ then:
887
+ assertTraces(1) {
888
+ trace(connectionPoolName == " dbcp2" ? 4 : 3) {
889
+ basicSpan(it, " parent" )
890
+
891
+ span {
892
+ operationName " database. connection"
893
+ resourceName " ${ds. class. simpleName}. getConnection"
894
+ childOf span(0)
895
+ errored timedOutException != null
896
+ tags {
897
+ " $Tags . COMPONENT " " java- jdbc- connection"
898
+ defaultTagsNoPeerService()
899
+ if (timedOutException) {
900
+ errorTags(timedOutException)
901
+ }
902
+ }
903
+ }
904
+
905
+ // dbcp2 will have two database.connection spans
906
+ if (connectionPoolName == " dbcp2" ) {
907
+ span {
908
+ operationName " database. connection"
909
+ resourceName " PoolingDataSource . getConnection"
910
+ childOf span(1)
911
+ errored timedOutException != null
912
+ tags {
913
+ " $Tags . COMPONENT " " java- jdbc- connection"
914
+ defaultTagsNoPeerService()
915
+ if (timedOutException) {
916
+ errorTags(timedOutException)
917
+ }
918
+ }
919
+ }
920
+ }
921
+
922
+ span {
923
+ operationName " pool. waiting"
924
+ resourceName " ${connectionPoolName}. waiting"
925
+ childOf span(connectionPoolName == " dbcp2" ? 2 : 1)
926
+ tags {
927
+ " $Tags . COMPONENT " " java- jdbc- pool- waiting"
928
+ if (connectionPoolName == " hikari" ) {
929
+ " $Tags . DB_POOL_NAME " String
930
+ }
931
+ defaultTagsNoPeerService()
932
+ }
933
+ }
934
+ }
935
+ }
936
+ assert expectException == (timedOutException != null)
937
+
938
+ cleanup:
939
+ if (ds instanceof Closeable) {
940
+ ds.close()
941
+ }
942
+
943
+ where:
944
+ connectionPoolName | exhaustPoolForMillis | expectException
945
+ " hikari" | 500 | false
946
+ " dbcp2" | 500 | false
947
+ " hikari" | 1500 | true
948
+ " dbcp2" | 1500 | true
949
+ }
950
+
951
+ def " Ensure LinkedBlockingDeque . pollFirst called outside of DBCP2 does not create spans" () {
952
+ setup:
953
+ def pool = new GenericObjectPool<>(new PooledObjectFactory() {
954
+
955
+ @Override
956
+ void activateObject(PooledObject p) throws Exception {
957
+ }
958
+
959
+ @Override
960
+ void destroyObject(PooledObject p) throws Exception {
961
+ }
962
+
963
+ @Override
964
+ PooledObject makeObject() throws Exception {
965
+ return new DefaultPooledObject(new Object())
966
+ }
967
+
968
+ @Override
969
+ void passivateObject(PooledObject p) throws Exception {
970
+ }
971
+
972
+ @Override
973
+ boolean validateObject(PooledObject p) {
974
+ return false
975
+ }
976
+ })
977
+ pool.setMaxTotal(1)
978
+
979
+ when:
980
+ def exhaustPoolForMillis = 500
981
+ def saturatedConnection = pool.borrowObject()
982
+ new Thread(() -> {
983
+ Thread.sleep(exhaustPoolForMillis)
984
+ pool.returnObject(saturatedConnection)
985
+ }, " saturated connection closer" ).start()
986
+
987
+ pool.borrowObject(1000)
988
+
989
+ then:
990
+ TEST_WRITER.size() == 0
991
+ }
992
+
817
993
Driver driverFor(String db) {
818
994
return newDriver(jdbcDriverClassNames.get(db))
819
995
}
0 commit comments