Skip to content

Commit 19a3483

Browse files
committed
refactored to enforceCleanClose
1 parent 152fe0a commit 19a3483

File tree

6 files changed

+217
-125
lines changed

6 files changed

+217
-125
lines changed

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -751,25 +751,24 @@ default DataSourceBuilder initDatabaseForPlatform(String platform) {
751751

752752

753753
/**
754-
* Sets the behaviour what to do, when a connection is closed (=returned to pool), while there is uncommitted work.
754+
* When enabled, the datasource enforces a clean close. This means, if you close a possible dirty
755+
* connection, that was not committed or rolled back, an exception is thrown.
755756
* <p>
756-
* Possible values
757-
* <ul>
758-
* <li><code>nothing</code> nothing happens</li>
759-
* <li><code>rollback</code> a rollback is performed (default)</li>
760-
* <li><code>commit</code> a commit is performed (dangerous!)</li>
761-
* <li><code>remove</code> the connection is removed from the pool (not recommended for production, connection might leak)</li>
762-
* <li><code>fail</code> an exception is thrown by close itself (for debugging, not recommended for production)</li>
763-
* </ul>
757+
* When disabled, the situation is logged as warning.
758+
* <p>
759+
* This option has no effect on readonly or autocommit connections.
760+
* <p>
761+
* Note: It is recommended to enable this option in tests/test systems to find possible
762+
* programming errors. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.
764763
*/
765-
DataSourceBuilder closeWithinTxn(String closeWithinTxn);
764+
DataSourceBuilder enforceCleanClose(boolean enforceCleanClose);
766765

767766
/**
768-
* Returns the behaviour, what to do, when a connection is (=returned to pool), while there is uncommitted work.
767+
* When <code>true</code>, an exception is thrown when a dirty connection is closed.
769768
* <p>
770-
* See {@link #closeWithinTxn(String)}.
769+
* See {@link #enforceCleanClose(boolean)}.
771770
*/
772-
String closeWithinTxn();
771+
boolean enforceCleanClose();
773772

774773
/**
775774
* The settings of the DataSourceBuilder. Provides getters/accessors

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public class DataSourceConfig implements DataSourceBuilder.Settings {
8383
private String applicationName;
8484
private boolean shutdownOnJvmExit;
8585
private boolean validateOnHeartbeat = !System.getenv().containsKey("LAMBDA_TASK_ROOT");
86-
private String closeWithinTxn = "rollback";
86+
private boolean enforceCleanClose;
8787

8888
@Override
8989
public Settings settings() {
@@ -145,7 +145,7 @@ public DataSourceConfig copy() {
145145
copy.initSql = initSql;
146146
copy.alert = alert;
147147
copy.listener = listener;
148-
copy.closeWithinTxn = closeWithinTxn;
148+
copy.enforceCleanClose = enforceCleanClose;
149149
return copy;
150150
}
151151

@@ -800,7 +800,7 @@ private void loadSettings(ConfigPropertiesHelper properties) {
800800
offline = properties.getBoolean("offline", offline);
801801
shutdownOnJvmExit = properties.getBoolean("shutdownOnJvmExit", shutdownOnJvmExit);
802802
validateOnHeartbeat = properties.getBoolean("validateOnHeartbeat", validateOnHeartbeat);
803-
closeWithinTxn = properties.get("closeWithinTxn", closeWithinTxn);
803+
enforceCleanClose = properties.getBoolean("enforceCleanClose", enforceCleanClose);
804804

805805

806806
String isoLevel = properties.get("isolationLevel", _isolationLevel(isolationLevel));
@@ -851,14 +851,14 @@ Map<String, String> parseCustom(String customProperties) {
851851
}
852852

853853
@Override
854-
public DataSourceBuilder closeWithinTxn(String closeWithinTxn) {
855-
this.closeWithinTxn = closeWithinTxn;
854+
public DataSourceBuilder enforceCleanClose(boolean enforceCleanClose) {
855+
this.enforceCleanClose = enforceCleanClose;
856856
return this;
857857
}
858858

859859
@Override
860-
public String closeWithinTxn() {
861-
return closeWithinTxn;
860+
public boolean enforceCleanClose() {
861+
return enforceCleanClose;
862862
}
863863

864864
/**

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

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525
*/
2626
final class ConnectionPool implements DataSourcePool {
2727

28-
enum CloseWithinTxn {
29-
NOTHING, ROLLBACK, COMMIT, FAIL, REMOVE;
30-
}
31-
3228
private static final String APPLICATION_NAME = "ApplicationName";
3329
private final ReentrantLock heartbeatLock = new ReentrantLock(false);
3430
private final ReentrantLock notifyLock = new ReentrantLock(false);
@@ -59,7 +55,7 @@ enum CloseWithinTxn {
5955
private final boolean failOnStart;
6056
private final int maxInactiveMillis;
6157
private final long validateStaleMillis;
62-
private final CloseWithinTxn closeWithinTxn;
58+
private final boolean enforceCleanClose;
6359
/**
6460
* Max age a connection is allowed in millis.
6561
* A value of 0 means no limit (no trimming based on max age).
@@ -134,7 +130,7 @@ enum CloseWithinTxn {
134130
this.user = params.getUsername();
135131
this.shutdownOnJvmExit = params.isShutdownOnJvmExit();
136132
this.source = DriverDataSource.of(name, params);
137-
this.closeWithinTxn = Enum.valueOf(CloseWithinTxn.class, params.closeWithinTxn().toUpperCase(Locale.ROOT));
133+
this.enforceCleanClose = params.enforceCleanClose();
138134
if (!params.isOffline()) {
139135
init();
140136
}
@@ -527,32 +523,8 @@ boolean invalidConnection(PooledConnection conn) {
527523
/**
528524
* Fail hard in close() when there is uncommitted work. This is for debugging to find wrong code.
529525
*/
530-
boolean failIfWithinTransaction() {
531-
return closeWithinTxn == CloseWithinTxn.FAIL;
532-
}
533-
534-
/**
535-
* will be called, before the connection is returned to the pool. This happens, when there may
536-
* be uncommitted changes and before all settings like autocommit/schema/catalog are reset to default.
537-
* <p>
538-
* If this method fails, the connection is silently removed from pool
539-
*/
540-
void closeWithinTxn(PooledConnection pooledConnection) throws SQLException {
541-
switch (closeWithinTxn) {
542-
case NOTHING:
543-
Log.trace("Closing active connection.");
544-
break;
545-
case ROLLBACK:
546-
pooledConnection.rollback();
547-
Log.trace("Closing active connection. Rollback performed.");
548-
break;
549-
case COMMIT:
550-
pooledConnection.commit();
551-
Log.trace("Closing active connection. Commit performed.");
552-
break;
553-
case REMOVE:
554-
throw new SQLException("Closing active connection. Removing from pool.");
555-
}
526+
boolean enforceCleanClose() {
527+
return enforceCleanClose;
556528
}
557529

558530
/**

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

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ String description() {
219219
}
220220

221221
String fullDescription() {
222-
return "name[" + name + "] startTime[" + startUseTime() + "] busySeconds[" + busySeconds() + "] stackTrace[" + stackTraceAsString() + "] stmt[" + lastStatement() + "]";
222+
return "name[" + name + "] startTime[" + startUseTime() + "] busySeconds[" + busySeconds() + "] stackTrace[" + stackTraceAsString(stackTrace) + "] stmt[" + lastStatement() + "]";
223223
}
224224

225225
/**
@@ -449,9 +449,9 @@ public void close() throws SQLException {
449449
throw new SQLException(IDLE_CONNECTION_ACCESSED_ERROR + "close()");
450450
}
451451
boolean mayHaveUncommittedChanges = !autoCommit && !readOnly && status == STATUS_ACTIVE;
452-
if (mayHaveUncommittedChanges && pool.failIfWithinTransaction()) {
452+
if (mayHaveUncommittedChanges && pool.enforceCleanClose()) {
453453
pool.returnConnectionForceClose(this);
454-
throw new SQLException("Tried to close active connection within transaction");
454+
throw new AssertionError("Tried to close a dirty connection. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.");
455455
}
456456
if (hadErrors) {
457457
if (failoverToReadOnly) {
@@ -469,7 +469,9 @@ public void close() throws SQLException {
469469
return;
470470
}
471471
if (mayHaveUncommittedChanges) {
472-
pool.closeWithinTxn(this);
472+
Log.warn("Tried to close a dirty connection at {0}. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details.",
473+
stackTraceAsString(Thread.currentThread().getStackTrace()));
474+
connection.rollback();
473475
}
474476
// reset the autoCommit back if client code changed it
475477
if (autoCommit != pool.isAutoCommit()) {
@@ -949,35 +951,23 @@ void setStackTrace(StackTraceElement[] stackTrace) {
949951
/**
950952
* Return the stackTrace as a String for logging purposes.
951953
*/
952-
private String stackTraceAsString() {
953-
StackTraceElement[] stackTrace = stackTrace();
954+
private String stackTraceAsString(StackTraceElement[] stackTrace) {
954955
if (stackTrace == null) {
955956
return "";
956957
}
957-
return Arrays.toString(stackTrace);
958-
}
959-
960-
/**
961-
* Return the full stack trace that got the connection from the pool. You
962-
* could use this if getCreatedByMethod() doesn't work for you.
963-
*/
964-
private StackTraceElement[] stackTrace() {
965-
if (stackTrace == null) {
966-
return null;
967-
}
968958

969959
// filter off the top of the stack that we are not interested in
970960
ArrayList<StackTraceElement> filteredList = new ArrayList<>();
971961
boolean include = false;
972962
for (StackTraceElement stackTraceElement : stackTrace) {
973-
if (!include && includeMethodLine(stackTraceElement.toString())) {
963+
if (!include && includeMethodLine(stackTraceElement.getClassName())) {
974964
include = true;
975965
}
976966
if (include && filteredList.size() < maxStackTrace) {
977967
filteredList.add(stackTraceElement);
978968
}
979969
}
980-
return filteredList.toArray(new StackTraceElement[0]);
970+
return filteredList.toString();
981971
}
982972

983973
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package io.ebean.datasource.pool;
2+
3+
import ch.qos.logback.classic.Level;
4+
import ch.qos.logback.classic.spi.ILoggingEvent;
5+
import ch.qos.logback.core.read.ListAppender;
6+
import io.ebean.datasource.DataSourceBuilder;
7+
import io.ebean.datasource.DataSourcePool;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.sql.Connection;
15+
import java.sql.SQLException;
16+
import java.sql.SQLSyntaxErrorException;
17+
import java.util.List;
18+
import java.util.stream.Collectors;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
22+
23+
/**
24+
* Tests, if connections are clean when closing them.
25+
* <p>
26+
* Test must run with "do not use module path" option in IntelliJ.
27+
* <p>
28+
* See also https://github.com/ebean-orm/ebean-datasource/issues/116 for more details
29+
*/
30+
class ConnectionPoolCloseTest {
31+
32+
private static ListAppender<ILoggingEvent> logCapture = new ListAppender<>();
33+
34+
private static DataSourcePool pool = createPool(false, false);
35+
private static DataSourcePool poolRo = createPool(false, true);
36+
private static DataSourcePool poolEnforce = createPool(true, false);
37+
private static DataSourcePool poolEnforceRo = createPool(true, true);
38+
39+
40+
@BeforeAll
41+
static void before() {
42+
// attach logback appender to capture log messages
43+
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger("io.ebean.datasource")).addAppender(logCapture);
44+
logCapture.start();
45+
}
46+
47+
@BeforeEach
48+
void beforeEach() {
49+
logCapture.list.clear();
50+
}
51+
52+
@AfterAll
53+
static void after() {
54+
logCapture.stop();
55+
pool.shutdown();
56+
poolRo.shutdown();
57+
poolEnforce.shutdown();
58+
poolEnforceRo.shutdown();
59+
}
60+
61+
@Test
62+
void testNoCommitOrRollback() throws SQLException {
63+
64+
doNoCommitOrRollback(pool);
65+
assertThat(getAndResetLogWarinings())
66+
.hasSize(1)
67+
.first().asString().startsWith("[WARN] Tried to close a dirty connection at");
68+
69+
assertThatThrownBy(() -> doNoCommitOrRollback(poolEnforce)).isInstanceOf(AssertionError.class);
70+
71+
doNoCommitOrRollback(poolRo);
72+
assertThat(getAndResetLogWarinings()).isEmpty();
73+
74+
doNoCommitOrRollback(poolEnforceRo);
75+
assertThat(getAndResetLogWarinings()).isEmpty();
76+
77+
}
78+
79+
@Test
80+
void testCommit() throws SQLException {
81+
doCommit(pool);
82+
doCommit(poolRo);
83+
doCommit(poolEnforce);
84+
doCommit(poolEnforceRo);
85+
assertThat(getAndResetLogWarinings()).isEmpty();
86+
}
87+
88+
@Test
89+
void testRollback() throws SQLException {
90+
doRollback(pool);
91+
doRollback(poolRo);
92+
doRollback(poolEnforce);
93+
doRollback(poolEnforceRo);
94+
assertThat(getAndResetLogWarinings()).isEmpty();
95+
}
96+
97+
@Test
98+
void testExecuteValidSql() throws SQLException {
99+
doExecute(pool, "SELECT 1");
100+
doExecute(poolRo, "SELECT 1");
101+
doExecute(poolEnforce, "SELECT 1");
102+
doExecute(poolEnforceRo, "SELECT 1");
103+
assertThat(getAndResetLogWarinings()).isEmpty();
104+
}
105+
106+
@Test
107+
void testExecuteInvalidSql() throws SQLException {
108+
assertThatThrownBy(() -> doExecute(pool, "invalid query"))
109+
.isInstanceOf(SQLSyntaxErrorException.class)
110+
.hasNoSuppressedExceptions();
111+
// (un)fortunately, we will log a missing rollback, when exception in try-with-resourches is thrown.
112+
assertThat(getAndResetLogWarinings())
113+
.hasSize(1)
114+
.first().asString().startsWith("[WARN] Tried to close a dirty connection at");
115+
116+
assertThatThrownBy(() -> doExecute(poolEnforce, "invalid query"))
117+
.isInstanceOf(SQLSyntaxErrorException.class)
118+
.hasSuppressedException(new AssertionError("Tried to close a dirty connection. See https://github.com/ebean-orm/ebean-datasource/issues/116 for details."));
119+
120+
assertThatThrownBy(() -> doExecute(poolRo, "invalid query"))
121+
.isInstanceOf(SQLSyntaxErrorException.class)
122+
.hasNoSuppressedExceptions();
123+
124+
assertThatThrownBy(() -> doExecute(poolEnforceRo, "invalid query"))
125+
.isInstanceOf(SQLSyntaxErrorException.class)
126+
.hasNoSuppressedExceptions();
127+
assertThat(getAndResetLogWarinings()).isEmpty();
128+
}
129+
130+
private static void doNoCommitOrRollback(DataSourcePool pool) throws SQLException {
131+
try (Connection connection = pool.getConnection()) {
132+
// we do nothing here.
133+
}
134+
}
135+
136+
private static void doCommit(DataSourcePool pool) throws SQLException {
137+
try (Connection connection = pool.getConnection()) {
138+
connection.commit();
139+
}
140+
}
141+
142+
private static void doRollback(DataSourcePool pool) throws SQLException {
143+
try (Connection connection = pool.getConnection()) {
144+
connection.rollback();
145+
}
146+
}
147+
148+
private static void doExecute(DataSourcePool pool, String query) throws SQLException {
149+
try (Connection connection = pool.getConnection()) {
150+
connection.createStatement().execute(query);
151+
connection.commit();
152+
}
153+
}
154+
155+
private static List<ILoggingEvent> getAndResetLogWarinings() {
156+
List<ILoggingEvent> warnings = logCapture.list.stream().filter(e -> e.getLevel().levelInt >= Level.WARN_INT).collect(Collectors.toList());
157+
logCapture.list.clear();
158+
return warnings;
159+
}
160+
161+
private static DataSourcePool createPool(boolean enforceCleanClose, boolean readOnly) {
162+
DataSourcePool ret = DataSourceBuilder.create()
163+
.url("jdbc:h2:mem:tests")
164+
.username("sa")
165+
.password("")
166+
.minConnections(2)
167+
.maxConnections(4)
168+
.enforceCleanClose(enforceCleanClose)
169+
.readOnly(readOnly)
170+
.build();
171+
// clear capturer after create
172+
logCapture.list.clear();
173+
return ret;
174+
}
175+
}

0 commit comments

Comments
 (0)