Skip to content

Commit 311898e

Browse files
committed
#892 Fix Statement.cancel() causes lockup
(partial backport of #903 from Jaybird 7)
1 parent ce62cb1 commit 311898e

File tree

16 files changed

+399
-168
lines changed

16 files changed

+399
-168
lines changed

devdoc/publish.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Publishing
88
To publish to Maven use
99

1010
```
11-
gradlewclean dist assemble publish -PcredentialsPassphrase=<credentials password>
11+
gradlew clean dist assemble publish -PcredentialsPassphrase=<credentials password>
1212
```
1313
Where `<credentials password>` is the password used to add the credentials (see
1414
also below).

src/docs/asciidoc/release_notes.adoc

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

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

40+
* Fixed: `Statement.cancel()` causes lockup (https://github.com/FirebirdSQL/jaybird/issues/892[#892])
41+
+
42+
This is a partial backport from Jaybird 7.
43+
See also <<breaking-changes-internal-api-6-0-4>>.
4044
* Fixed: Negative buffer size on Firebird 2.5 if information response is 32KiB or greater (https://github.com/FirebirdSQL/jaybird/issues/895[#895])
4145
+
4246
The error occurs when preparing or executing statements with a lot of columns, especially select statements with columns that have long names and/or aliases for columns, tables and table owners.
@@ -2093,6 +2097,22 @@ This should only affect maintainers of Jaybird forks, or people using the intern
20932097
** Constructors now throw `SQLException` if the provided database is null or not attached, or the provided transaction is null or not active.
20942098
Previously, passing in a null transaction would result in a `NullPointerException`.
20952099

2100+
[#breaking-changes-internal-api-6-0-4]
2101+
==== Breaking changes internal API for 6.0.4
2102+
2103+
With the introduction of the (partial) transmit lock in the wire protocol to address https://github.com/FirebirdSQL/jaybird/issues/892[#892 Statement.cancel() causes lockup], statement and transaction operations must now hold the transmit lock while sending messages to the server.
2104+
See also https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2026-02-cancellation-thread-safety-backport.adoc[jdp-2026-02: Cancellation thread-safety backport].
2105+
2106+
Implementations and forks *must* ensure they hold the transmit lock _within_ the connection lock while sending statement or transaction related messages.
2107+
The transmit lock *must* be released before receiving (reading) responses from the server.
2108+
2109+
We have avoided most of the breaking changes that we made in Jaybird 7, but the following is a minor incompatible change:
2110+
2111+
* V10Statement
2112+
** `sendExecute(int, RowValue)` should no longer be overridden, instead override `sendExecuteMsg(XdrOutputStream, int, RowValue)`.
2113+
+
2114+
If you do continue to override `sendExecute(int, RowValue)`, make sure the transmit lock covers the entire message.
2115+
20962116
[#breaking-changes-unlikely]
20972117
=== Unlikely breaking changes
20982118

src/main/org/firebirdsql/gds/ng/wire/AbstractFbWireStatement.java

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.firebirdsql.gds.ng.AbstractFbStatement;
2626
import org.firebirdsql.gds.ng.DeferredResponse;
2727
import org.firebirdsql.gds.ng.FbDatabase;
28-
import org.firebirdsql.gds.ng.FbExceptionBuilder;
2928
import org.firebirdsql.gds.ng.FbStatement;
3029
import org.firebirdsql.gds.ng.FbTransaction;
3130
import org.firebirdsql.gds.ng.LockCloseable;
@@ -91,12 +90,16 @@ protected final XdrOutputStream getXdrOut() throws SQLException {
9190
return getXdrStreamAccess().getXdrOut();
9291
}
9392

94-
private XdrStreamAccess getXdrStreamAccess() throws SQLException {
95-
if (database != null) {
96-
return database.getXdrStreamAccess();
97-
} else {
98-
throw FbExceptionBuilder.connectionClosed();
99-
}
93+
/**
94+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
95+
* @since 6.0.4
96+
*/
97+
protected final void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException {
98+
getXdrStreamAccess().withTransmitLock(transmitAction);
99+
}
100+
101+
private XdrStreamAccess getXdrStreamAccess() {
102+
return database.getXdrStreamAccess();
100103
}
101104

102105
@Override
@@ -318,11 +321,12 @@ public void run() {
318321
if (database == null) return;
319322
try (LockCloseable ignored = database.withLock()) {
320323
if (!database.isAttached()) return;
321-
XdrOutputStream xdrOut = database.getXdrStreamAccess().getXdrOut();
322-
xdrOut.writeInt(WireProtocolConstants.op_free_statement); // p_operation
323-
xdrOut.writeInt(handle); // p_sqlfree_statement
324-
xdrOut.writeInt(ISCConstants.DSQL_drop); // p_sqlfree_option
325-
xdrOut.flush();
324+
database.getXdrStreamAccess().withTransmitLock(xdrOut -> {
325+
xdrOut.writeInt(WireProtocolConstants.op_free_statement); // p_operation
326+
xdrOut.writeInt(handle); // p_sqlfree_statement
327+
xdrOut.writeInt(ISCConstants.DSQL_drop); // p_sqlfree_option
328+
xdrOut.flush();
329+
});
326330
// TODO: This may process deferred actions on the cleaner thread, we may want to change that
327331
database.enqueueDeferredAction(CLEANUP_FREE_DEFERRED_ACTION);
328332
} catch (SQLException | IOException e) {

src/main/org/firebirdsql/gds/ng/wire/AbstractWireOperations.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ protected final XdrOutputStream getXdrOut() throws SQLException {
9292
return getXdrStreamAccess().getXdrOut();
9393
}
9494

95+
/**
96+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
97+
* @since 6.0.4
98+
*/
99+
protected final void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException {
100+
getXdrStreamAccess().withTransmitLock(transmitAction);
101+
}
102+
95103
@Override
96104
public final SQLException readStatusVector() throws SQLException {
97105
return readStatusVector(getXdrIn());
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Public Firebird Java API.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are met:
6+
* 1. Redistributions of source code must retain the above copyright notice,
7+
* this list of conditions and the following disclaimer.
8+
* 2. Redistributions in binary form must reproduce the above copyright
9+
* notice, this list of conditions and the following disclaimer in the
10+
* documentation and/or other materials provided with the distribution.
11+
* 3. The name of the author may not be used to endorse or promote products
12+
* derived from this software without specific prior written permission.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15+
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16+
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17+
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
18+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19+
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
20+
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
21+
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
22+
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
23+
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
*/
25+
package org.firebirdsql.gds.ng.wire;
26+
27+
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
28+
import org.jspecify.annotations.NullMarked;
29+
30+
import java.io.IOException;
31+
import java.sql.SQLException;
32+
33+
/**
34+
* Action for transmitting messages to Firebird.
35+
* <p>
36+
* This action is primarily intended for transmitting data while the transmit lock is held.
37+
* </p>
38+
*
39+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
40+
* @since 6.0.4
41+
*/
42+
@FunctionalInterface
43+
@NullMarked
44+
public interface TransmitAction {
45+
46+
/**
47+
* Transmits a message (or messages) to Firebird.
48+
* <p>
49+
* Implementations <strong>should not</strong> obtain (additional) locks, and <strong>must not</strong> attempt to
50+
* read (receive) data from the server. Preferably, the only operations done are writes to {@code xdrOut}. In
51+
* general, an action should write the whole message. If that is not possible, make sure that the full message is
52+
* written while the transmit lock is held.
53+
* </p>
54+
*
55+
* @param xdrOut
56+
* XDR output stream to Firebird server
57+
* @throws IOException
58+
* for errors writing into {@code xdrOut}
59+
* @throws SQLException
60+
* for connection state errors
61+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
62+
*/
63+
void transmit(XdrOutputStream xdrOut) throws IOException, SQLException;
64+
65+
}

src/main/org/firebirdsql/gds/ng/wire/WireConnection.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.firebirdsql.gds.ng.wire.crypt.KnownServerKey;
3939
import org.firebirdsql.jaybird.props.def.ConnectionProperty;
4040
import org.firebirdsql.jaybird.util.ByteArrayHelper;
41+
import org.jspecify.annotations.NonNull;
4142

4243
import javax.net.SocketFactory;
4344
import java.io.ByteArrayOutputStream;
@@ -59,6 +60,7 @@
5960
import java.util.Optional;
6061
import java.util.Properties;
6162
import java.util.concurrent.TimeUnit;
63+
import java.util.concurrent.locks.ReentrantLock;
6264

6365
import static java.lang.System.Logger.Level.DEBUG;
6466
import static org.firebirdsql.gds.impl.wire.WireProtocolConstants.*;
@@ -98,8 +100,11 @@ public abstract class WireConnection<T extends IAttachProperties<T>, C extends F
98100
private XdrOutputStream xdrOut;
99101
private XdrInputStream xdrIn;
100102
private final XdrStreamAccess streamAccess = new XdrStreamAccess() {
103+
104+
private final ReentrantLock transmitLock = new ReentrantLock();
105+
101106
@Override
102-
public XdrInputStream getXdrIn() throws SQLException {
107+
public @NonNull XdrInputStream getXdrIn() throws SQLException {
103108
if (isConnected() && xdrIn != null) {
104109
return xdrIn;
105110
} else {
@@ -108,13 +113,24 @@ public XdrInputStream getXdrIn() throws SQLException {
108113
}
109114

110115
@Override
111-
public XdrOutputStream getXdrOut() throws SQLException {
116+
public @NonNull XdrOutputStream getXdrOut() throws SQLException {
112117
if (isConnected() && xdrOut != null) {
113118
return xdrOut;
114119
} else {
115120
throw FbExceptionBuilder.connectionClosed();
116121
}
117122
}
123+
124+
@Override
125+
public void withTransmitLock(@NonNull TransmitAction transmitAction) throws IOException, SQLException {
126+
transmitLock.lock();
127+
try {
128+
transmitAction.transmit(getXdrOut());
129+
} finally {
130+
transmitLock.unlock();
131+
}
132+
}
133+
118134
};
119135

120136
/**
@@ -678,7 +694,18 @@ private static String getSystemPropertyPrivileged(final String propertyName) {
678694
* If there is no socket, the socket is closed, or for errors writing to the socket.
679695
*/
680696
public final void writeDirect(byte[] data) throws IOException {
681-
xdrOut.writeDirect(data);
697+
try {
698+
// NOTE: In Jaybird 5 and 6, this is not fully thread-safe, as the transmit lock only covers cancellation
699+
// and statement operations; a full solution is available in Jaybird 7 and higher
700+
getXdrStreamAccess().withTransmitLock(xdrOut -> {
701+
xdrOut.flush();
702+
xdrOut.writeDirect(data);
703+
});
704+
} catch (SQLException e) {
705+
// This can happen when withTransmitLock is called on a broken connection; we don't want to change
706+
// the signature of this method, so we convert to an IOException
707+
throw new IOException(e);
708+
}
682709
}
683710

684711
final List<KnownServerKey.PluginSpecificData> getPluginSpecificData() {

src/main/org/firebirdsql/gds/ng/wire/XdrStreamAccess.java

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
/*
2-
* $Id$
3-
*
42
* Public Firebird Java API.
53
*
64
* Redistribution and use in source and binary forms, with or without
@@ -28,7 +26,9 @@
2826

2927
import org.firebirdsql.gds.impl.wire.XdrInputStream;
3028
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
29+
import org.jspecify.annotations.NullMarked;
3130

31+
import java.io.IOException;
3232
import java.sql.SQLException;
3333

3434
/**
@@ -37,25 +37,58 @@
3737
* @author Mark Rotteveel
3838
* @since 3.0
3939
*/
40+
@NullMarked
4041
public interface XdrStreamAccess {
4142

4243
/**
43-
* Gets the XdrInputStream.
44+
* Gets the XDR input stream.
4445
*
45-
* @return Instance of XdrInputStream
46+
* @return instance of {@link XdrInputStream}
4647
* @throws SQLException
47-
* If no connection is opened or when exceptions occur
48-
* retrieving the InputStream
48+
* if no connection is opened or when exceptions occur retrieving the InputStream
4949
*/
5050
XdrInputStream getXdrIn() throws SQLException;
5151

5252
/**
53-
* Gets the XdrOutputStream.
53+
* Gets the XDR output stream.
5454
*
55-
* @return Instance of XdrOutputStream
55+
* @return instance of {@link XdrOutputStream}
5656
* @throws SQLException
57-
* If no connection is opened or when exceptions occur
58-
* retrieving the OutputStream
57+
* if no connection is opened or when exceptions occur retrieving the OutputStream
58+
* @see #withTransmitLock(TransmitAction)
5959
*/
6060
XdrOutputStream getXdrOut() throws SQLException;
61+
62+
/**
63+
* Runs {@link TransmitAction#transmit(XdrOutputStream)} with {@link #getXdrOut()} on {@code transmitAction} under
64+
* the transmit lock.
65+
* <p>
66+
* For Jaybird 5 and 6, the transmit lock only needs to be used for statement operations and cancellation. See also
67+
* <a href="https://github.com/FirebirdSQL/jaybird/blob/master/devdoc/jdp/jdp-2026-02-cancellation-thread-safety-backport.adoc">jdp-2026-02: Cancellation thread-safety backport</a>
68+
* </p>
69+
* <p>
70+
* The transmit lock should only cover sending messages to the server. It should be held for the duration of the
71+
* entire message. It <strong>must</strong> be released <em>before</em> reading (receiving) messages from the
72+
* server. If possible, do not do anything other than writing to the XDR output stream while holding the lock.
73+
* </p>
74+
* <p>
75+
* Normal operations <strong>must</strong> obtain the lock while holding the connection lock (i.e. the various
76+
* {@code withLock()} methods). Out-of-band operations (e.g. cancellation) <strong>must not</strong> take out the
77+
* connection lock, otherwise they can't be out-of-band.
78+
* </p>
79+
* <p>
80+
* Note for implementations: the lock used must be reentrant.
81+
* </p>
82+
*
83+
* @param transmitAction
84+
* the transmit action to run under lock
85+
* @throws IOException
86+
* for errors writing to the XDR output stream
87+
* @throws SQLException
88+
* for other database access errors
89+
* @see TransmitAction
90+
* @since 6.0.4
91+
*/
92+
void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException;
93+
6194
}

0 commit comments

Comments
 (0)