Skip to content

Commit d1bb068

Browse files
committed
#893 Add system property to disable closing broken connections
1 parent d4c6ea3 commit d1bb068

File tree

6 files changed

+109
-8
lines changed

6 files changed

+109
-8
lines changed

devdoc/jdp/jdp-2026-04-disable-closing-of-broken-connections.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
== Status
77

8-
* Draft
9-
* Proposed for: Jaybird 5.0.13, Jaybird 6.0.6, Jaybird 7
8+
* Published: 2026-04-07
9+
* Implemented in: Jaybird 5.0.13, Jaybird 6.0.6, Jaybird 7
1010

1111
== Type
1212

src/main/org/firebirdsql/ds/FBXADataSource.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: LGPL-2.1-or-later
33
package org.firebirdsql.ds;
44

5+
import org.firebirdsql.gds.JaybirdSystemProperties;
56
import org.firebirdsql.gds.impl.GDSFactory;
67
import org.firebirdsql.gds.impl.GDSType;
78
import org.firebirdsql.jaybird.xca.*;
@@ -18,6 +19,7 @@
1819
import javax.sql.XADataSource;
1920
import java.io.Serial;
2021
import java.io.Serializable;
22+
import java.lang.System.Logger.Level;
2123
import java.sql.SQLException;
2224

2325
/**
@@ -95,15 +97,20 @@ public void connectionClosed(XcaConnectionEvent ce) {
9597

9698
@Override
9799
public void connectionErrorOccurred(XcaConnectionEvent ce) {
98-
destroyConnection(ce);
100+
boolean forceCloseOnFatal = JaybirdSystemProperties.isXaConnectionForceCloseOnFatal();
101+
LOG.log(Level.TRACE, () -> "ConnectionErrorOccurred (forceCloseOnFatal=%b)".formatted(forceCloseOnFatal),
102+
ce.getException());
103+
if (forceCloseOnFatal) {
104+
destroyConnection(ce);
105+
}
99106
}
100107

101108
private void destroyConnection(XcaConnectionEvent ce) {
102109
FBManagedConnection mc = ce.getSource();
103110
try {
104111
mc.destroy(ce);
105112
} catch (SQLException e) {
106-
LOG.log(System.Logger.Level.WARNING, "Ignored exception closing unmanaged connection", e);
113+
LOG.log(Level.WARNING, "Ignored exception closing unmanaged connection", e);
107114
} finally {
108115
mc.removeConnectionEventListener(this);
109116
}

src/main/org/firebirdsql/gds/JaybirdSystemProperties.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public final class JaybirdSystemProperties {
3737
public static final String WIRE_DECRYPT_BUFFER_SIZE = WIRE_PREFIX + "decryptBufferSize";
3838
public static final String WIRE_INPUT_BUFFER_SIZE = WIRE_PREFIX + "inputBufferSize";
3939
public static final String WIRE_OUTPUT_BUFFER_SIZE = WIRE_PREFIX + "outputBufferSize";
40+
public static final String JDBC_CONNECTION_FORCE_CLOSE_FATAL = JDBC_PREFIX + "connection.forceCloseOnFatal";
41+
public static final String XA_CONNECTION_FORCE_CLOSE_FATAL = COMMON_PREFIX + "ds.xa.connection.forceCloseOnFatal";
4042

4143
private JaybirdSystemProperties() {
4244
// no instances
@@ -101,7 +103,7 @@ public static int getWireOutputBufferSize(int defaultValue) {
101103
public static @Nullable Boolean getDefaultAsyncFetch() {
102104
String asyncFetch = getSystemPropertyPrivileged(DEFAULT_ASYNC_FETCH);
103105
if (asyncFetch == null) return null;
104-
// Special handling for empty string to be equal to true
106+
// Special handling for blank string -> true
105107
return asyncFetch.isBlank() || Boolean.parseBoolean(asyncFetch);
106108
}
107109

@@ -113,11 +115,26 @@ public static int getWireOutputBufferSize(int defaultValue) {
113115
return getIntegerSystemPropertyPrivileged(DEFAULT_MAX_BLOB_CACHE_SIZE);
114116
}
115117

118+
public static boolean isJdbcConnectionForceCloseOnFatal() {
119+
return getBooleanWithDefault(JDBC_CONNECTION_FORCE_CLOSE_FATAL, true);
120+
}
121+
122+
public static boolean isXaConnectionForceCloseOnFatal() {
123+
return getBooleanWithDefault(XA_CONNECTION_FORCE_CLOSE_FATAL, true);
124+
}
125+
116126
private static int getWithDefault(String propertyName, int defaultValue) {
117127
Integer value = getIntegerSystemPropertyPrivileged(propertyName);
118128
return value != null ? value : defaultValue;
119129
}
120130

131+
@SuppressWarnings("SameParameterValue")
132+
private static boolean getBooleanWithDefault(String propertyName, boolean defaultValue) {
133+
String stringValue = getSystemPropertyPrivileged(propertyName);
134+
// Special handling for null -> default, and blank string -> true
135+
return stringValue == null ? defaultValue : (stringValue.isBlank() || Boolean.parseBoolean(stringValue));
136+
}
137+
121138
private static @Nullable String getSystemPropertyPrivileged(final String propertyName) {
122139
return AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty(propertyName));
123140
}

src/main/org/firebirdsql/jaybird/xca/FBStandAloneConnectionManager.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
*/
88
package org.firebirdsql.jaybird.xca;
99

10+
import org.firebirdsql.gds.JaybirdSystemProperties;
1011
import org.firebirdsql.jdbc.FirebirdConnection;
1112

1213
import java.io.Serial;
1314
import java.io.Serializable;
15+
import java.lang.System.Logger.Level;
1416
import java.sql.SQLException;
1517

1618
/**
@@ -43,16 +45,20 @@ public void connectionClosed(XcaConnectionEvent ce) {
4345

4446
@Override
4547
public void connectionErrorOccurred(XcaConnectionEvent ce) {
46-
log.log(System.Logger.Level.TRACE, "ConnectionErrorOccurred", ce.getException());
47-
destroyConnection(ce);
48+
boolean forceCloseOnFatal = JaybirdSystemProperties.isJdbcConnectionForceCloseOnFatal();
49+
log.log(Level.TRACE, () -> "ConnectionErrorOccurred (forceCloseOnFatal=%b)".formatted(forceCloseOnFatal),
50+
ce.getException());
51+
if (forceCloseOnFatal) {
52+
destroyConnection(ce);
53+
}
4854
}
4955

5056
private void destroyConnection(XcaConnectionEvent ce) {
5157
FBManagedConnection mc = ce.getSource();
5258
try {
5359
mc.destroy(ce);
5460
} catch (SQLException e) {
55-
log.log(System.Logger.Level.WARNING, "Exception closing unmanaged connection", e);
61+
log.log(Level.WARNING, "Exception closing unmanaged connection", e);
5662
} finally {
5763
mc.removeConnectionEventListener(this);
5864
}

src/test/org/firebirdsql/ds/FBXADataSourceTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,33 @@
55
import org.firebirdsql.common.FBTestProperties;
66
import org.firebirdsql.common.extension.UsesDatabaseExtension;
77
import org.firebirdsql.gds.impl.GDSServerVersion;
8+
import org.firebirdsql.gds.ng.AbstractFbAttachment;
9+
import org.firebirdsql.gds.ng.listeners.ExceptionListenerDispatcher;
810
import org.firebirdsql.jaybird.xca.XidImpl;
911
import org.firebirdsql.jdbc.FirebirdConnection;
1012
import org.firebirdsql.jdbc.SQLStateConstants;
13+
import org.jspecify.annotations.Nullable;
1114
import org.junit.jupiter.api.AfterEach;
1215
import org.junit.jupiter.api.Test;
1316
import org.junit.jupiter.api.extension.RegisterExtension;
17+
import org.junit.jupiter.params.ParameterizedTest;
18+
import org.junit.jupiter.params.provider.CsvSource;
1419

1520
import javax.sql.XAConnection;
1621
import javax.transaction.xa.XAResource;
1722
import javax.transaction.xa.Xid;
23+
import java.lang.invoke.MethodHandles;
1824
import java.sql.*;
1925
import java.util.ArrayList;
2026
import java.util.List;
2127

2228
import static org.firebirdsql.common.FBTestProperties.*;
2329
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
30+
import static org.firebirdsql.common.SystemPropertyHelper.withTemporarySystemProperty;
2431
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isPureJavaType;
2532
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
2633
import static org.firebirdsql.common.matchers.SQLExceptionMatchers.sqlStateEquals;
34+
import static org.firebirdsql.gds.JaybirdSystemProperties.XA_CONNECTION_FORCE_CLOSE_FATAL;
2735
import static org.hamcrest.MatcherAssert.assertThat;
2836
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2937
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -266,4 +274,34 @@ void enableWireCompression() throws Exception {
266274
}
267275
}
268276

277+
@ParameterizedTest
278+
@CsvSource(useHeadersInDisplayName = true, nullValues = { "NIL" }, textBlock = """
279+
propertyValue, expectForceClose
280+
NIL, true
281+
true, true
282+
'', true
283+
false, false
284+
garbage, false
285+
""")
286+
void systemPropertyXaConnectionForceCloseOnFatal(@Nullable String propertyValue, boolean expectForceClose)
287+
throws Exception {
288+
var lookup = MethodHandles.privateLookupIn(AbstractFbAttachment.class, MethodHandles.lookup())
289+
.findVarHandle(AbstractFbAttachment.class, "exceptionListenerDispatcher", ExceptionListenerDispatcher.class);
290+
XAConnection xaConnection = getXAConnection();
291+
try (var ignored = withTemporarySystemProperty(XA_CONNECTION_FORCE_CLOSE_FATAL, propertyValue);
292+
var connection = xaConnection.getConnection().unwrap(FirebirdConnection.class)) {
293+
AbstractFbAttachment<?> db = (AbstractFbAttachment<?>) connection.getFbDatabase();
294+
ExceptionListenerDispatcher eld = (ExceptionListenerDispatcher) lookup.get(db);
295+
296+
assertFalse(connection.isClosed(), "Expected open connection before sending fatal exception");
297+
298+
eld.errorOccurred(new SQLException("Test fatal exception", SQLStateConstants.SQL_STATE_CONNECTION_FAILURE));
299+
300+
assertEquals(expectForceClose, connection.isClosed(),
301+
"Unexpected Connection#isClosed() value after sending fatal exception");
302+
assertEquals(!expectForceClose, db.isAttached(),
303+
"Unexpected FbDatabase#isAttached() value after sending fatal exception");
304+
}
305+
}
306+
269307
}

src/test/org/firebirdsql/jdbc/FBConnectionTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import org.firebirdsql.gds.JaybirdErrorCodes;
1212
import org.firebirdsql.gds.TransactionParameterBuffer;
1313
import org.firebirdsql.gds.impl.GDSServerVersion;
14+
import org.firebirdsql.gds.ng.AbstractFbAttachment;
1415
import org.firebirdsql.gds.ng.FbDatabase;
1516
import org.firebirdsql.gds.ng.FbExceptionBuilder;
1617
import org.firebirdsql.gds.ng.IConnectionProperties;
1718
import org.firebirdsql.gds.ng.InfoProcessor;
1819
import org.firebirdsql.gds.ng.WireCrypt;
20+
import org.firebirdsql.gds.ng.listeners.ExceptionListenerDispatcher;
1921
import org.firebirdsql.gds.ng.wire.crypt.FBSQLEncryptException;
2022
import org.firebirdsql.jaybird.props.PropertyNames;
2123
import org.jspecify.annotations.NullMarked;
@@ -32,6 +34,7 @@
3234
import org.mockito.Mock;
3335
import org.mockito.junit.jupiter.MockitoExtension;
3436

37+
import java.lang.invoke.MethodHandles;
3538
import java.nio.charset.StandardCharsets;
3639
import java.sql.*;
3740
import java.util.List;
@@ -55,6 +58,7 @@
5558
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_invalidConnectionEncoding;
5659
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_invalidIdentifierLength;
5760
import static org.firebirdsql.gds.JaybirdSystemProperties.DEFAULT_CONNECTION_ENCODING_PROPERTY;
61+
import static org.firebirdsql.gds.JaybirdSystemProperties.JDBC_CONNECTION_FORCE_CLOSE_FATAL;
5862
import static org.firebirdsql.gds.JaybirdSystemProperties.PROCESS_ID_PROP;
5963
import static org.firebirdsql.gds.JaybirdSystemProperties.PROCESS_NAME_PROP;
6064
import static org.firebirdsql.gds.JaybirdSystemProperties.REQUIRE_CONNECTION_ENCODING_PROPERTY;
@@ -1199,6 +1203,35 @@ void escapeProcessing_disabled() throws Exception {
11991203
}
12001204
}
12011205

1206+
@ParameterizedTest
1207+
@CsvSource(useHeadersInDisplayName = true, nullValues = { "NIL" }, textBlock = """
1208+
propertyValue, expectForceClose
1209+
NIL, true
1210+
true, true
1211+
'', true
1212+
false, false
1213+
garbage, false
1214+
""")
1215+
void systemPropertyJdbcConnectionForceCloseOnFatal(@Nullable String propertyValue, boolean expectForceClose)
1216+
throws Exception {
1217+
var lookup = MethodHandles.privateLookupIn(AbstractFbAttachment.class, MethodHandles.lookup())
1218+
.findVarHandle(AbstractFbAttachment.class, "exceptionListenerDispatcher", ExceptionListenerDispatcher.class);
1219+
try (var ignored = withTemporarySystemProperty(JDBC_CONNECTION_FORCE_CLOSE_FATAL, propertyValue);
1220+
var connection = getConnectionViaDriverManager()) {
1221+
AbstractFbAttachment<?> db = (AbstractFbAttachment<?>) connection.getFbDatabase();
1222+
ExceptionListenerDispatcher eld = (ExceptionListenerDispatcher) lookup.get(db);
1223+
1224+
assertFalse(connection.isClosed(), "Expected open connection before sending fatal exception");
1225+
1226+
eld.errorOccurred(new SQLException("Test fatal exception", SQLStateConstants.SQL_STATE_CONNECTION_FAILURE));
1227+
1228+
assertEquals(expectForceClose, connection.isClosed(),
1229+
"Unexpected Connection#isClosed() value after sending fatal exception");
1230+
assertEquals(!expectForceClose, db.isAttached(),
1231+
"Unexpected FbDatabase#isAttached() value after sending fatal exception");
1232+
}
1233+
}
1234+
12021235
/**
12031236
* Single-use executor, delays the command to be executed until signalled.
12041237
*/

0 commit comments

Comments
 (0)