Skip to content

Commit f755b27

Browse files
committed
Merge branch 'master' into db2-tests
2 parents 11be910 + d672910 commit f755b27

File tree

8 files changed

+227
-14
lines changed

8 files changed

+227
-14
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.ebean.datasource;
2+
3+
import java.sql.SQLException;
4+
5+
/**
6+
* This exception is thrown, if the connection pool has reached maxSize.
7+
* @author Roland Praml, Foconis Analytics GmbH
8+
*/
9+
public class ConnectionPoolExhaustedException extends SQLException {
10+
public ConnectionPoolExhaustedException(String reason) {
11+
super(reason);
12+
}
13+
}

ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceBuilder.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,15 @@ default DataSourceBuilder heartbeatTimeoutSeconds(int heartbeatTimeoutSeconds) {
409409
@Deprecated(forRemoval = true)
410410
DataSourceBuilder setHeartbeatTimeoutSeconds(int heartbeatTimeoutSeconds);
411411

412+
/**
413+
* Sets the coun how often the heartbeat has to detect pool exhaustion in succession.
414+
* in succession before an error is raised and the pool will be reset.
415+
* <p>
416+
* By default, this value must be multiplied with the sum of heartbeatfreq + waitTimeoutMillis to
417+
* estimate the time, when the pool will be restarted, because all connections were leaked.
418+
*/
419+
DataSourceBuilder heartbeatMaxPoolExhaustedCount(int count);
420+
412421
/**
413422
* Set to true if a stack trace should be captured when obtaining a connection from the pool.
414423
* <p>
@@ -706,6 +715,7 @@ default DataSourceBuilder initDatabaseForPlatform(String platform) {
706715
* <p>
707716
* This is enabled by default. Generally we only want to turn this
708717
* off when using the pool with a Lambda function.
718+
*
709719
* @param validateOnHeartbeat Use false to disable heartbeat validation.
710720
*/
711721
DataSourceBuilder validateOnHeartbeat(boolean validateOnHeartbeat);
@@ -922,6 +932,11 @@ default String driverClassName() {
922932
*/
923933
int getHeartbeatTimeoutSeconds();
924934

935+
/**
936+
* Return the number, how often the heartbeat has to detect pool exhaustion in succession.
937+
*/
938+
int getHeartbeatMaxPoolExhaustedCount();
939+
925940
/**
926941
* Return true if a stack trace should be captured when obtaining a connection from the pool.
927942
* <p>

ebean-datasource-api/src/main/java/io/ebean/datasource/DataSourceConfig.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class DataSourceConfig implements DataSourceBuilder.Settings {
6262
private String heartbeatSql;
6363
private int heartbeatFreqSecs = 30;
6464
private int heartbeatTimeoutSeconds = 30;
65+
private int heartbeatMaxPoolExhaustedCount = 10;
6566
private boolean captureStackTrace;
6667
private int maxStackTraceSize = 5;
6768
private int leakTimeMinutes = 30;
@@ -181,10 +182,10 @@ public DataSourceConfig setDefaults(DataSourceBuilder builder) {
181182
if (minConnections == 2 && other.getMinConnections() < 2) {
182183
minConnections = other.getMinConnections();
183184
}
184-
if (!shutdownOnJvmExit && other.isShutdownOnJvmExit()){
185+
if (!shutdownOnJvmExit && other.isShutdownOnJvmExit()) {
185186
shutdownOnJvmExit = true;
186187
}
187-
if (validateOnHeartbeat && !other.isValidateOnHeartbeat()){
188+
if (validateOnHeartbeat && !other.isValidateOnHeartbeat()) {
188189
validateOnHeartbeat = false;
189190
}
190191
if (customProperties == null) {
@@ -468,6 +469,17 @@ public DataSourceConfig setHeartbeatTimeoutSeconds(int heartbeatTimeoutSeconds)
468469
return this;
469470
}
470471

472+
@Override
473+
public int getHeartbeatMaxPoolExhaustedCount() {
474+
return this.heartbeatMaxPoolExhaustedCount;
475+
}
476+
477+
@Override
478+
public DataSourceBuilder heartbeatMaxPoolExhaustedCount(int count) {
479+
this.heartbeatMaxPoolExhaustedCount = count;
480+
return this;
481+
}
482+
471483
@Override
472484
public boolean isCaptureStackTrace() {
473485
return captureStackTrace;

ebean-datasource/src/main/java/io/ebean/datasource/pool/ConnectionPool.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ enum CloseWithinTxn {
5050
private final String heartbeatSql;
5151
private final int heartbeatFreqSecs;
5252
private final int heartbeatTimeoutSeconds;
53+
private final int heartbeatMaxPoolExhaustedCount;
54+
5355
private final long trimPoolFreqMillis;
5456
private final int transactionIsolation;
5557
private final boolean autoCommit;
@@ -82,6 +84,7 @@ enum CloseWithinTxn {
8284
private final int pstmtCacheSize;
8385
private final PooledConnectionQueue queue;
8486
private Timer heartBeatTimer;
87+
private int heartbeatPoolExhaustedCount;
8588
/**
8689
* Used to find and close() leaked connections. Leaked connections are
8790
* thought to be busy but have not been used for some time. Each time a
@@ -118,6 +121,7 @@ enum CloseWithinTxn {
118121
this.waitTimeoutMillis = params.getWaitTimeoutMillis();
119122
this.heartbeatFreqSecs = params.getHeartbeatFreqSecs();
120123
this.heartbeatTimeoutSeconds = params.getHeartbeatTimeoutSeconds();
124+
this.heartbeatMaxPoolExhaustedCount = params.getHeartbeatMaxPoolExhaustedCount();
121125
this.heartbeatSql = params.getHeartbeatSql();
122126
this.validateOnHeartbeat = params.isValidateOnHeartbeat();
123127
this.trimPoolFreqMillis = 1000L * params.getTrimPoolFreqSecs();
@@ -375,11 +379,19 @@ private void testConnection() {
375379
try {
376380
// Get a connection from the pool and test it
377381
conn = getConnection();
382+
heartbeatPoolExhaustedCount = 0;
378383
if (testConnection(conn)) {
379384
notifyDataSourceIsUp();
380385
} else {
381386
notifyDataSourceIsDown(null);
382387
}
388+
} catch (ConnectionPoolExhaustedException be) {
389+
heartbeatPoolExhaustedCount++;
390+
if (heartbeatPoolExhaustedCount > heartbeatMaxPoolExhaustedCount) {
391+
notifyDataSourceIsDown(be);
392+
} else {
393+
Log.warn("Heartbeat: " + be.getMessage());
394+
}
383395
} catch (SQLException ex) {
384396
notifyDataSourceIsDown(ex);
385397
} finally {
@@ -615,6 +627,7 @@ PooledConnection createConnectionForQueue(int connId) throws SQLException {
615627
* </ul>
616628
*/
617629
private void reset() {
630+
heartbeatPoolExhaustedCount = 0;
618631
queue.reset(leakTimeMinutes);
619632
}
620633

ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnection.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ void closeConnectionFully(boolean logErrors) {
250250
Log.error("Error checking if connection [" + name + "] is closed", ex);
251251
}
252252
}
253+
lock.lock();
253254
try {
254255
for (ExtendedPreparedStatement ps : pstmtCache.values()) {
255256
ps.closeDestroy();
@@ -258,6 +259,19 @@ void closeConnectionFully(boolean logErrors) {
258259
if (logErrors) {
259260
Log.warn("Error when closing connection Statements", ex);
260261
}
262+
} finally {
263+
lock.unlock();
264+
}
265+
try {
266+
// DB2 (and some other DBMS) may have uncommitted changes and do not allow close
267+
// so try to do a rollback.
268+
if (!connection.getAutoCommit()) {
269+
connection.rollback();
270+
}
271+
} catch (SQLException ex) {
272+
if (logErrors) {
273+
Log.warn("Could not perform rollback", ex);
274+
}
261275
}
262276
try {
263277
// DB2 (and some other DBMS) may have uncommitted changes and do not allow close

ebean-datasource/src/main/java/io/ebean/datasource/pool/PooledConnectionQueue.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.ebean.datasource.pool;
22

3+
import io.ebean.datasource.ConnectionPoolExhaustedException;
34
import io.ebean.datasource.PoolStatus;
45
import io.ebean.datasource.pool.ConnectionPool.Status;
56

@@ -226,18 +227,13 @@ private PooledConnection _obtainConnection() throws InterruptedException, SQLExc
226227
hitCount++;
227228
// are other threads already waiting? (they get priority)
228229
if (waitingThreads == 0) {
229-
PooledConnection freeConnection = extractFromFreeList();
230-
if (freeConnection != null) {
231-
return freeConnection;
230+
PooledConnection connection = extractFromFreeList();
231+
if (connection != null) {
232+
return connection;
232233
}
233-
if (busyList.size() < maxSize) {
234-
// grow the connection pool
235-
PooledConnection c = pool.createConnectionForQueue(connectionId++);
236-
int busySize = registerBusyConnection(c);
237-
if (Log.isLoggable(DEBUG)) {
238-
Log.debug("DataSource [{0}] grow; id[{1}] busy[{2}] max[{3}]", name, c.name(), busySize, maxSize);
239-
}
240-
return c;
234+
connection = createConnection();
235+
if (connection != null) {
236+
return connection;
241237
}
242238
}
243239
try {
@@ -257,21 +253,40 @@ private PooledConnection _obtainConnection() throws InterruptedException, SQLExc
257253
}
258254
}
259255

256+
private PooledConnection createConnection() throws SQLException {
257+
if (busyList.size() < maxSize) {
258+
// grow the connection pool
259+
PooledConnection c = pool.createConnectionForQueue(connectionId++);
260+
int busySize = registerBusyConnection(c);
261+
if (Log.isLoggable(DEBUG)) {
262+
Log.debug("DataSource [{0}] grow; id[{1}] busy[{2}] max[{3}]", name, c.name(), busySize, maxSize);
263+
}
264+
return c;
265+
} else {
266+
return null;
267+
}
268+
}
269+
260270
/**
261271
* Got into a loop waiting for connections to be returned to the pool.
262272
*/
263273
private PooledConnection _obtainConnectionWaitLoop() throws SQLException, InterruptedException {
264274
long nanos = MILLIS_TIME_UNIT.toNanos(waitTimeoutMillis);
265275
for (; ; ) {
266276
if (nanos <= 0) {
277+
// We waited long enough, that a connection was returned, so we try to create a new connection.
278+
PooledConnection conn = createConnection();
279+
if (conn != null) {
280+
return conn;
281+
}
267282
String msg = "Unsuccessfully waited [" + waitTimeoutMillis + "] millis for a connection to be returned."
268283
+ " No connections are free. You need to Increase the max connections of [" + maxSize + "]"
269284
+ " or look for a connection pool leak using datasource.xxx.capturestacktrace=true";
270285
if (pool.captureStackTrace()) {
271286
dumpBusyConnectionInformation();
272287
}
273288

274-
throw new SQLException(msg);
289+
throw new ConnectionPoolExhaustedException(msg);
275290
}
276291

277292
try {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.ebean.datasource.pool;
2+
3+
import io.ebean.datasource.DataSourceAlert;
4+
import io.ebean.datasource.DataSourceBuilder;
5+
import io.ebean.datasource.DataSourcePool;
6+
import org.junit.jupiter.api.Test;
7+
8+
import javax.sql.DataSource;
9+
import java.sql.Connection;
10+
import java.sql.SQLException;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
public class ConnectionPoolFullTest implements DataSourceAlert {
15+
16+
private int up;
17+
private int down;
18+
19+
@Test
20+
void testPoolFullWithHeartbeat() throws Exception {
21+
22+
DataSourcePool pool = DataSourceBuilder.create()
23+
.url("jdbc:h2:mem:testConnectionPoolFull")
24+
.username("sa")
25+
.password("sa")
26+
.heartbeatFreqSecs(1)
27+
.minConnections(1)
28+
.maxConnections(1)
29+
.trimPoolFreqSecs(1)
30+
.heartbeatMaxPoolExhaustedCount(1)
31+
.alert(this)
32+
.failOnStart(false)
33+
.build();
34+
35+
assertThat(up).isEqualTo(1);
36+
assertThat(down).isEqualTo(0);
37+
38+
try {
39+
// block the thread for 2 secs. The heartbeat must not shutdown the pool
40+
try (Connection connection = pool.getConnection()) {
41+
System.out.println("waiting 2s");
42+
Thread.sleep(2000);
43+
connection.rollback();
44+
}
45+
assertThat(up).isEqualTo(1);
46+
assertThat(down).isEqualTo(0);
47+
48+
// now block the thread longer, so that exhausted count will be reached
49+
try (Connection connection = pool.getConnection()) {
50+
System.out.println("waiting 4s");
51+
Thread.sleep(4000);
52+
connection.rollback();
53+
}
54+
// we expect, that the pool goes down.
55+
assertThat(up).isEqualTo(1);
56+
assertThat(down).isEqualTo(1);
57+
58+
System.out.println("waiting 2s for recovery");
59+
Thread.sleep(2000);
60+
assertThat(up).isEqualTo(2);
61+
assertThat(down).isEqualTo(1);
62+
63+
// pool should be OK again
64+
try (Connection connection = pool.getConnection()) {
65+
connection.rollback();
66+
}
67+
68+
assertThat(up).isEqualTo(2);
69+
assertThat(down).isEqualTo(1);
70+
71+
72+
} finally {
73+
pool.shutdown();
74+
}
75+
76+
}
77+
78+
79+
@Override
80+
public void dataSourceUp(DataSource dataSource) {
81+
up++;
82+
}
83+
84+
@Override
85+
public void dataSourceDown(DataSource dataSource, SQLException reason) {
86+
down++;
87+
}
88+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.ebean.datasource.pool;
2+
3+
import io.ebean.datasource.DataSourceBuilder;
4+
import io.ebean.datasource.DataSourcePool;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.sql.Connection;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
import java.util.concurrent.Future;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
public class ConnectionPoolRecoverTest {
17+
18+
@Test
19+
void testHeavyLoadPool() throws Exception {
20+
DataSourcePool pool = DataSourceBuilder.create()
21+
.url("jdbc:h2:mem:testConnectionPoolFull")
22+
.username("sa")
23+
.password("sa")
24+
.heartbeatFreqSecs(1)
25+
.minConnections(1)
26+
.maxConnections(1)
27+
.trimPoolFreqSecs(1)
28+
// .heartbeatMaxPoolExhaustedCount(1)
29+
.failOnStart(false)
30+
.build();
31+
try {
32+
for (int i = 0; i < 5; i++) {
33+
try (Connection conn = pool.getConnection()) {
34+
Thread.sleep(2000);
35+
conn.rollback();
36+
}
37+
}
38+
} finally {
39+
pool.shutdown();
40+
}
41+
}
42+
43+
}

0 commit comments

Comments
 (0)