Skip to content

Commit a86a7c1

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

File tree

6 files changed

+133
-6
lines changed

6 files changed

+133
-6
lines changed

src/docs/asciidoc/release_notes.adoc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ For known issues, consult <<known-issues>>.
3737

3838
The following was fixed or changed since Jaybird 6.0.6:
3939

40+
* Two system properties were added to disable forced close of a connection after fatal errors (https://github.com/FirebirdSQL/jaybird/issues/893[#893])
41+
+
42+
See also <<disable-force-close-on-fatal>>.
4043
* Fixed: Legacy authentication fails with non-ASCII characters in password (https://github.com/FirebirdSQL/jaybird/issues/858[#858])
4144
+
4245
The hash that is sent instead of the actual password -- for authentication plugin `Legacy_Auth` or on Firebird 2.5 or older -- was incorrectly generated based on the `char` (UTF-16) value.
@@ -1459,6 +1462,29 @@ select {\{\}fn EXP(2){\}\} from SOME_TABLE
14591462
select {fn EXP(2)} from SOME_TABLE
14601463
----
14611464

1465+
[#disable-force-close-on-fatal]
1466+
=== System properties to disable force close of connections
1467+
1468+
Since Jaybird 6.0.6 (and Jaybird 5.0.13), Jaybird supports two new Boolean system properties.
1469+
These properties control if Jaybird closes a connection on fatal errors.
1470+
1471+
`org.firebirdsql.jdbc.connection.forceCloseOnFatal`::
1472+
Configures force close behaviour of "`normal`" JDBC connections (i.e. created with `org.firebirdsql.jaybird.xca.FBStandAloneConnectionManager`);
1473+
defaults to `true`.
1474+
+
1475+
We use prefix `org.firebirdsql.jdbc.connection.` because XCA is internal API, and this affects most JDBC connections (except those from `FBXADataSource`, or those created with a custom `XcaConnectionManager`).
1476+
1477+
`org.firebirdsql.ds.xa.connection.forceCloseOnFatal`::
1478+
Configures force close behaviour of (XA) connections created by `org.firebirdsql.ds.FBXADataSource`;
1479+
defaults to `true`.
1480+
1481+
These system properties are checked _dynamically_, so they can be changed at run time.
1482+
1483+
These properties should only be used for debugging or workarounds.
1484+
Correct behaviour of a connection after a fatal error is ignored is not guaranteed.
1485+
1486+
See also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2026-04-disable-closing-of-broken-connections.adoc[jdp-2026-04: Disable closing of broken connections].
1487+
14621488
[#other-fixes-and-changes]
14631489
=== Other fixes and changes
14641490

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.firebirdsql.ds;
2020

21+
import org.firebirdsql.gds.JaybirdSystemProperties;
2122
import org.firebirdsql.gds.impl.GDSFactory;
2223
import org.firebirdsql.gds.impl.GDSType;
2324
import org.firebirdsql.gds.ng.LockCloseable;
@@ -33,6 +34,7 @@
3334
import javax.sql.XADataSource;
3435
import java.io.Serial;
3536
import java.io.Serializable;
37+
import java.lang.System.Logger.Level;
3638
import java.sql.SQLException;
3739

3840
/**
@@ -107,15 +109,20 @@ public void connectionClosed(XcaConnectionEvent ce) {
107109

108110
@Override
109111
public void connectionErrorOccurred(XcaConnectionEvent ce) {
110-
destroyConnection(ce);
112+
boolean forceCloseOnFatal = JaybirdSystemProperties.isXaConnectionForceCloseOnFatal();
113+
LOG.log(Level.TRACE, () -> "ConnectionErrorOccurred (forceCloseOnFatal=%b)".formatted(forceCloseOnFatal),
114+
ce.getException());
115+
if (forceCloseOnFatal) {
116+
destroyConnection(ce);
117+
}
111118
}
112119

113120
private void destroyConnection(XcaConnectionEvent ce) {
114121
FBManagedConnection mc = ce.getSource();
115122
try {
116123
mc.destroy(ce);
117124
} catch (SQLException e) {
118-
LOG.log(System.Logger.Level.WARNING, "Ignored exception closing unmanaged connection", e);
125+
LOG.log(Level.WARNING, "Ignored exception closing unmanaged connection", e);
119126
} finally {
120127
mc.removeConnectionEventListener(this);
121128
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public final class JaybirdSystemProperties {
5151
public static final String WIRE_DECRYPT_BUFFER_SIZE = WIRE_PREFIX + "decryptBufferSize";
5252
public static final String WIRE_INPUT_BUFFER_SIZE = WIRE_PREFIX + "inputBufferSize";
5353
public static final String WIRE_OUTPUT_BUFFER_SIZE = WIRE_PREFIX + "outputBufferSize";
54+
public static final String JDBC_CONNECTION_FORCE_CLOSE_FATAL = JDBC_PREFIX + "connection.forceCloseOnFatal";
55+
public static final String XA_CONNECTION_FORCE_CLOSE_FATAL = COMMON_PREFIX + "ds.xa.connection.forceCloseOnFatal";
5456

5557
private JaybirdSystemProperties() {
5658
// no instances
@@ -115,7 +117,7 @@ public static String getDefaultReportSQLWarnings() {
115117
public static Boolean getDefaultAsyncFetch() {
116118
String asyncFetch = getSystemPropertyPrivileged(DEFAULT_ASYNC_FETCH);
117119
if (asyncFetch == null) return null;
118-
// Special handling for empty string to be equal to true
120+
// Special handling for blank string -> true
119121
return asyncFetch.isBlank() || Boolean.parseBoolean(asyncFetch);
120122
}
121123

@@ -127,11 +129,26 @@ public static Integer getDefaultMaxBlobCacheSize() {
127129
return getIntegerSystemPropertyPrivileged(DEFAULT_MAX_BLOB_CACHE_SIZE);
128130
}
129131

132+
public static boolean isJdbcConnectionForceCloseOnFatal() {
133+
return getBooleanWithDefault(JDBC_CONNECTION_FORCE_CLOSE_FATAL, true);
134+
}
135+
136+
public static boolean isXaConnectionForceCloseOnFatal() {
137+
return getBooleanWithDefault(XA_CONNECTION_FORCE_CLOSE_FATAL, true);
138+
}
139+
130140
private static int getWithDefault(String propertyName, int defaultValue) {
131141
Integer value = getIntegerSystemPropertyPrivileged(propertyName);
132142
return value != null ? value : defaultValue;
133143
}
134144

145+
@SuppressWarnings("SameParameterValue")
146+
private static boolean getBooleanWithDefault(String propertyName, boolean defaultValue) {
147+
String stringValue = getSystemPropertyPrivileged(propertyName);
148+
// Special handling for null -> default, and blank string -> true
149+
return stringValue == null ? defaultValue : (stringValue.isBlank() || Boolean.parseBoolean(stringValue));
150+
}
151+
135152
private static String getSystemPropertyPrivileged(final String propertyName) {
136153
return AccessController.doPrivileged((PrivilegedAction<String>) () -> System.getProperty(propertyName));
137154
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
*/
1919
package org.firebirdsql.jaybird.xca;
2020

21+
import org.firebirdsql.gds.JaybirdSystemProperties;
2122
import org.firebirdsql.jdbc.FirebirdConnection;
2223

2324
import java.io.Serial;
2425
import java.io.Serializable;
26+
import java.lang.System.Logger.Level;
2527
import java.sql.SQLException;
2628

2729
/**
@@ -54,16 +56,20 @@ public void connectionClosed(XcaConnectionEvent ce) {
5456

5557
@Override
5658
public void connectionErrorOccurred(XcaConnectionEvent ce) {
57-
log.log(System.Logger.Level.TRACE, "ConnectionErrorOccurred", ce.getException());
58-
destroyConnection(ce);
59+
boolean forceCloseOnFatal = JaybirdSystemProperties.isJdbcConnectionForceCloseOnFatal();
60+
log.log(Level.TRACE, () -> "ConnectionErrorOccurred (forceCloseOnFatal=%b)".formatted(forceCloseOnFatal),
61+
ce.getException());
62+
if (forceCloseOnFatal) {
63+
destroyConnection(ce);
64+
}
5965
}
6066

6167
private void destroyConnection(XcaConnectionEvent ce) {
6268
FBManagedConnection mc = ce.getSource();
6369
try {
6470
mc.destroy(ce);
6571
} catch (SQLException e) {
66-
log.log(System.Logger.Level.WARNING, "Exception closing unmanaged connection", e);
72+
log.log(Level.WARNING, "Exception closing unmanaged connection", e);
6773
} finally {
6874
mc.removeConnectionEventListener(this);
6975
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,33 @@
2121
import org.firebirdsql.common.FBTestProperties;
2222
import org.firebirdsql.common.extension.UsesDatabaseExtension;
2323
import org.firebirdsql.gds.impl.GDSServerVersion;
24+
import org.firebirdsql.gds.ng.AbstractFbAttachment;
25+
import org.firebirdsql.gds.ng.listeners.ExceptionListenerDispatcher;
2426
import org.firebirdsql.jaybird.xca.XidImpl;
2527
import org.firebirdsql.jdbc.FirebirdConnection;
2628
import org.firebirdsql.jdbc.SQLStateConstants;
29+
import org.jspecify.annotations.Nullable;
2730
import org.junit.jupiter.api.AfterEach;
2831
import org.junit.jupiter.api.Test;
2932
import org.junit.jupiter.api.extension.RegisterExtension;
33+
import org.junit.jupiter.params.ParameterizedTest;
34+
import org.junit.jupiter.params.provider.CsvSource;
3035

3136
import javax.sql.XAConnection;
3237
import javax.transaction.xa.XAResource;
3338
import javax.transaction.xa.Xid;
39+
import java.lang.invoke.MethodHandles;
3440
import java.sql.*;
3541
import java.util.ArrayList;
3642
import java.util.List;
3743

3844
import static org.firebirdsql.common.FBTestProperties.*;
3945
import static org.firebirdsql.common.JdbcResourceHelper.closeQuietly;
46+
import static org.firebirdsql.common.SystemPropertyHelper.withTemporarySystemProperty;
4047
import static org.firebirdsql.common.matchers.GdsTypeMatchers.isPureJavaType;
4148
import static org.firebirdsql.common.matchers.MatcherAssume.assumeThat;
4249
import static org.firebirdsql.common.matchers.SQLExceptionMatchers.sqlStateEquals;
50+
import static org.firebirdsql.gds.JaybirdSystemProperties.XA_CONNECTION_FORCE_CLOSE_FATAL;
4351
import static org.hamcrest.MatcherAssert.assertThat;
4452
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4553
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -282,4 +290,34 @@ void enableWireCompression() throws Exception {
282290
}
283291
}
284292

293+
@ParameterizedTest
294+
@CsvSource(useHeadersInDisplayName = true, nullValues = { "NIL" }, textBlock = """
295+
propertyValue, expectForceClose
296+
NIL, true
297+
true, true
298+
'', true
299+
false, false
300+
garbage, false
301+
""")
302+
void systemPropertyXaConnectionForceCloseOnFatal(@Nullable String propertyValue, boolean expectForceClose)
303+
throws Exception {
304+
var lookup = MethodHandles.privateLookupIn(AbstractFbAttachment.class, MethodHandles.lookup())
305+
.findVarHandle(AbstractFbAttachment.class, "exceptionListenerDispatcher", ExceptionListenerDispatcher.class);
306+
XAConnection xaConnection = getXAConnection();
307+
try (var ignored = withTemporarySystemProperty(XA_CONNECTION_FORCE_CLOSE_FATAL, propertyValue);
308+
var connection = xaConnection.getConnection().unwrap(FirebirdConnection.class)) {
309+
AbstractFbAttachment<?> db = (AbstractFbAttachment<?>) connection.getFbDatabase();
310+
ExceptionListenerDispatcher eld = (ExceptionListenerDispatcher) lookup.get(db);
311+
312+
assertFalse(connection.isClosed(), "Expected open connection before sending fatal exception");
313+
314+
eld.errorOccurred(new SQLException("Test fatal exception", SQLStateConstants.SQL_STATE_CONNECTION_FAILURE));
315+
316+
assertEquals(expectForceClose, connection.isClosed(),
317+
"Unexpected Connection#isClosed() value after sending fatal exception");
318+
assertEquals(!expectForceClose, db.isAttached(),
319+
"Unexpected FbDatabase#isAttached() value after sending fatal exception");
320+
}
321+
}
322+
285323
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
import org.firebirdsql.gds.JaybirdErrorCodes;
2727
import org.firebirdsql.gds.TransactionParameterBuffer;
2828
import org.firebirdsql.gds.impl.GDSServerVersion;
29+
import org.firebirdsql.gds.ng.AbstractFbAttachment;
2930
import org.firebirdsql.gds.ng.FbDatabase;
3031
import org.firebirdsql.gds.ng.FbExceptionBuilder;
3132
import org.firebirdsql.gds.ng.IConnectionProperties;
3233
import org.firebirdsql.gds.ng.InfoProcessor;
3334
import org.firebirdsql.gds.ng.WireCrypt;
35+
import org.firebirdsql.gds.ng.listeners.ExceptionListenerDispatcher;
3436
import org.firebirdsql.gds.ng.wire.crypt.FBSQLEncryptException;
3537
import org.firebirdsql.jaybird.props.PropertyNames;
3638
import org.jspecify.annotations.NullMarked;
@@ -47,6 +49,7 @@
4749
import org.mockito.Mock;
4850
import org.mockito.junit.jupiter.MockitoExtension;
4951

52+
import java.lang.invoke.MethodHandles;
5053
import java.nio.charset.StandardCharsets;
5154
import java.sql.*;
5255
import java.util.List;
@@ -70,6 +73,7 @@
7073
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_invalidConnectionEncoding;
7174
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_invalidIdentifierLength;
7275
import static org.firebirdsql.gds.JaybirdSystemProperties.DEFAULT_CONNECTION_ENCODING_PROPERTY;
76+
import static org.firebirdsql.gds.JaybirdSystemProperties.JDBC_CONNECTION_FORCE_CLOSE_FATAL;
7377
import static org.firebirdsql.gds.JaybirdSystemProperties.PROCESS_ID_PROP;
7478
import static org.firebirdsql.gds.JaybirdSystemProperties.PROCESS_NAME_PROP;
7579
import static org.firebirdsql.gds.JaybirdSystemProperties.REQUIRE_CONNECTION_ENCODING_PROPERTY;
@@ -1172,6 +1176,35 @@ void isSimpleIdentifier(String identifier, boolean expectedIsSimple) throws Exce
11721176
}
11731177
}
11741178

1179+
@ParameterizedTest
1180+
@CsvSource(useHeadersInDisplayName = true, nullValues = { "NIL" }, textBlock = """
1181+
propertyValue, expectForceClose
1182+
NIL, true
1183+
true, true
1184+
'', true
1185+
false, false
1186+
garbage, false
1187+
""")
1188+
void systemPropertyJdbcConnectionForceCloseOnFatal(@Nullable String propertyValue, boolean expectForceClose)
1189+
throws Exception {
1190+
var lookup = MethodHandles.privateLookupIn(AbstractFbAttachment.class, MethodHandles.lookup())
1191+
.findVarHandle(AbstractFbAttachment.class, "exceptionListenerDispatcher", ExceptionListenerDispatcher.class);
1192+
try (var ignored = withTemporarySystemProperty(JDBC_CONNECTION_FORCE_CLOSE_FATAL, propertyValue);
1193+
var connection = getConnectionViaDriverManager()) {
1194+
AbstractFbAttachment<?> db = (AbstractFbAttachment<?>) connection.getFbDatabase();
1195+
ExceptionListenerDispatcher eld = (ExceptionListenerDispatcher) lookup.get(db);
1196+
1197+
assertFalse(connection.isClosed(), "Expected open connection before sending fatal exception");
1198+
1199+
eld.errorOccurred(new SQLException("Test fatal exception", SQLStateConstants.SQL_STATE_CONNECTION_FAILURE));
1200+
1201+
assertEquals(expectForceClose, connection.isClosed(),
1202+
"Unexpected Connection#isClosed() value after sending fatal exception");
1203+
assertEquals(!expectForceClose, db.isAttached(),
1204+
"Unexpected FbDatabase#isAttached() value after sending fatal exception");
1205+
}
1206+
}
1207+
11751208
/**
11761209
* Single-use executor, delays the command to be executed until signalled.
11771210
*/

0 commit comments

Comments
 (0)