@@ -6,6 +6,11 @@ import datadog.trace.api.Config
66import  datadog.trace.api.DDSpanTypes 
77import  datadog.trace.bootstrap.instrumentation.api.InstrumentationTags 
88import  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 
914import  org.apache.derby.jdbc.EmbeddedDataSource 
1015import  org.h2.jdbcx.JdbcDataSource 
1116import  spock.lang.Shared 
@@ -18,11 +23,16 @@ import java.sql.Connection
1823import  java.sql.Driver 
1924import  java.sql.PreparedStatement 
2025import  java.sql.ResultSet 
26+ import  java.sql.SQLException 
27+ import  java.sql.SQLTimeoutException 
28+ import  java.sql.SQLTransientConnectionException 
2129import  java.sql.Statement 
30+ import  java.time.Duration 
2231
2332import static  datadog.trace.agent.test.utils.TraceUtils.basicSpan 
2433import static  datadog.trace.agent.test.utils.TraceUtils.runUnderTrace 
2534import static  datadog.trace.api.config.TraceInstrumentationConfig.DB_CLIENT_HOST_SPLIT_BY_INSTANCE 
35+ import static  datadog.trace.api.config.TraceInstrumentationConfig.JDBC_POOL_WAITING_ENABLED 
2636
2737abstract  class  JDBCInstrumentationTest  extends  VersionedNamingTestBase  {
2838
@@ -97,6 +107,22 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
97107    return  ds
98108  }
99109
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+ 
100126  def  createHikariDS (String  dbType , String  jdbcUrl ) {
101127    HikariConfig  config =  new  HikariConfig ()
102128    def  jdbcUrlToSet =  dbType ==  " derby" ?  jdbcUrl +  " ;create=true" :  jdbcUrl
@@ -110,6 +136,7 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
110136    config. addDataSourceProperty(" prepStmtCacheSize" " 250" 
111137    config. addDataSourceProperty(" prepStmtCacheSqlLimit" " 2048" 
112138    config. setMaximumPoolSize(1 )
139+     config. setConnectionTimeout(1000 )
113140
114141    return  new  HikariDataSource (config)
115142  }
@@ -133,6 +160,9 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
133160    if  (connectionPoolName ==  " tomcat" 
134161      ds =  createTomcatDS(dbType, jdbcUrl)
135162    }
163+     if  (connectionPoolName ==  " dbcp2" 
164+       ds =  createDbcp2DS(dbType, jdbcUrl)
165+     }
136166    if  (connectionPoolName ==  " hikari" 
137167      ds =  createHikariDS(dbType, jdbcUrl)
138168    }
@@ -148,6 +178,7 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
148178
149179    injectSysConfig(" dd.trace.jdbc.prepared.statement.class.name" " test.TestPreparedStatement" 
150180    injectSysConfig(" dd.integration.jdbc-datasource.enabled" " true" 
181+     injectSysConfig(JDBC_POOL_WAITING_ENABLED , " true" 
151182  }
152183
153184  def  setupSpec () {
@@ -814,6 +845,151 @@ abstract class JDBCInstrumentationTest extends VersionedNamingTestBase {
814845    "  c3p0"              | _
815846  } 
816847
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 "  " - 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 "  " - 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 "  " - 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+ 
817993  Driver driverFor(String db) { 
818994    return newDriver(jdbcDriverClassNames.get(db)) 
819995  } 
0 commit comments