Skip to content

Commit 26efcc1

Browse files
committed
#904 Fix Statement.cancel() causes lockup
(partial backport of #903 from Jaybird 7)
1 parent 255a5ef commit 26efcc1

File tree

12 files changed

+350
-146
lines changed

12 files changed

+350
-146
lines changed

src/docs/asciidoc/release_notes.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ The following has been changed or fixed since Jaybird 5.0.10:
3838
* Fixed: `FBServiceManager.getAuthPlugins()` reported the `dbCryptConfig` value (https://github.com/FirebirdSQL/jaybird/issues/902[#902])
3939
+
4040
This fix was backported from Jaybird 6.0.4.
41+
* Fixed: `Statement.cancel()` causes lockup (https://github.com/FirebirdSQL/jaybird/issues/904[#904])
42+
+
43+
This is a partial backport from Jaybird 7.
44+
See also <<breaking-changes-internal-api-5-0-11>>.
4145
4246
[#jaybird-5-0-10-changelog]
4347
=== Jaybird 5.0.10
@@ -1484,6 +1488,22 @@ This should only affect maintainers of Jaybird forks, or people using the intern
14841488
** Constructors now throw `SQLException` if the provided database is null or not attached, or the provided transaction is null or not active.
14851489
Previously, passing in a null transaction would result in a `NullPointerException`.
14861490
1491+
[#breaking-changes-internal-api-5-0-11]
1492+
==== Breaking changes internal API for 5.0.11
1493+
1494+
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.
1495+
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].
1496+
1497+
Implementations and forks *must* ensure they hold the transmit lock _within_ the connection lock while sending statement or transaction related messages.
1498+
The transmit lock *must* be released before receiving (reading) responses from the server.
1499+
1500+
We have avoided most of the breaking changes that we made in Jaybird 7, but the following is a minor incompatible change:
1501+
1502+
* V10Statement
1503+
** `sendExecute(int, RowValue)` should no longer be overridden, instead override `sendExecuteMsg(XdrOutputStream, int, RowValue)`.
1504+
+
1505+
If you do continue to override `sendExecute(int, RowValue)`, make sure the transmit lock covers the entire message.
1506+
14871507
[#breaking-changes-unlikely]
14881508
=== Unlikely breaking changes
14891509

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,16 @@ protected final XdrOutputStream getXdrOut() throws SQLException {
8282
return getXdrStreamAccess().getXdrOut();
8383
}
8484

85-
private XdrStreamAccess getXdrStreamAccess() throws SQLException {
86-
if (database != null) {
87-
return database.getXdrStreamAccess();
88-
} else {
89-
throw new SQLException("Connection closed or no connection available");
90-
}
85+
/**
86+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
87+
* @since 5.0.11
88+
*/
89+
protected final void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException {
90+
getXdrStreamAccess().withTransmitLock(transmitAction);
91+
}
92+
93+
private XdrStreamAccess getXdrStreamAccess() {
94+
return database.getXdrStreamAccess();
9195
}
9296

9397
@Override
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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+
29+
import java.io.IOException;
30+
import java.sql.SQLException;
31+
32+
/**
33+
* Action for transmitting messages to Firebird.
34+
* <p>
35+
* This action is primarily intended for transmitting data while the transmit lock is held.
36+
* </p>
37+
*
38+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
39+
* @since 5.0.11
40+
*/
41+
@FunctionalInterface
42+
public interface TransmitAction {
43+
44+
/**
45+
* Transmits a message (or messages) to Firebird.
46+
* <p>
47+
* Implementations <strong>should not</strong> obtain (additional) locks, and <strong>must not</strong> attempt to
48+
* read (receive) data from the server. Preferably, the only operations done are writes to {@code xdrOut}. In
49+
* general, an action should write the whole message. If that is not possible, make sure that the full message is
50+
* written while the transmit lock is held.
51+
* </p>
52+
*
53+
* @param xdrOut
54+
* XDR output stream to Firebird server
55+
* @throws IOException
56+
* for errors writing into {@code xdrOut}
57+
* @throws SQLException
58+
* for connection state errors
59+
* @see XdrStreamAccess#withTransmitLock(TransmitAction)
60+
*/
61+
void transmit(XdrOutputStream xdrOut) throws IOException, SQLException;
62+
63+
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import java.util.List;
5858
import java.util.Map;
5959
import java.util.concurrent.TimeUnit;
60+
import java.util.concurrent.locks.ReentrantLock;
6061

6162
import static org.firebirdsql.gds.impl.wire.WireProtocolConstants.*;
6263

@@ -91,6 +92,9 @@ public abstract class WireConnection<T extends IAttachProperties<T>, C extends F
9192
private XdrOutputStream xdrOut;
9293
private XdrInputStream xdrIn;
9394
private final XdrStreamAccess streamAccess = new XdrStreamAccess() {
95+
96+
private final ReentrantLock transmitLock = new ReentrantLock();
97+
9498
@Override
9599
public XdrInputStream getXdrIn() throws SQLException {
96100
if (isConnected() && xdrIn != null) {
@@ -108,6 +112,17 @@ public XdrOutputStream getXdrOut() throws SQLException {
108112
throw new SQLException("Connection closed or no connection available");
109113
}
110114
}
115+
116+
@Override
117+
public void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException {
118+
transmitLock.lock();
119+
try {
120+
transmitAction.transmit(getXdrOut());
121+
} finally {
122+
transmitLock.unlock();
123+
}
124+
}
125+
111126
};
112127

113128
/**
@@ -599,7 +614,18 @@ private static String getSystemPropertyPrivileged(final String propertyName) {
599614
* If there is no socket, the socket is closed, or for errors writing to the socket.
600615
*/
601616
public final void writeDirect(byte[] data) throws IOException {
602-
xdrOut.writeDirect(data);
617+
try {
618+
// NOTE: In Jaybird 5 and 6, this is not fully thread-safe, as the transmit lock only covers cancellation
619+
// and statement operations; a full solution is available in Jaybird 7 and higher
620+
getXdrStreamAccess().withTransmitLock(xdrOut -> {
621+
xdrOut.flush();
622+
xdrOut.writeDirect(data);
623+
});
624+
} catch (SQLException e) {
625+
// This can happen when withTransmitLock is called on a broken connection; we don't want to change
626+
// the signature of this method, so we convert to an IOException
627+
throw new IOException(e);
628+
}
603629
}
604630

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

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

Lines changed: 41 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
@@ -29,6 +27,7 @@
2927
import org.firebirdsql.gds.impl.wire.XdrInputStream;
3028
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
3129

30+
import java.io.IOException;
3231
import java.sql.SQLException;
3332

3433
/**
@@ -40,22 +39,54 @@
4039
public interface XdrStreamAccess {
4140

4241
/**
43-
* Gets the XdrInputStream.
42+
* Gets the XDR input stream.
4443
*
45-
* @return Instance of XdrInputStream
44+
* @return instance of {@link XdrInputStream}
4645
* @throws SQLException
47-
* If no connection is opened or when exceptions occur
48-
* retrieving the InputStream
46+
* if no connection is opened or when exceptions occur retrieving the InputStream
4947
*/
5048
XdrInputStream getXdrIn() throws SQLException;
5149

5250
/**
53-
* Gets the XdrOutputStream.
51+
* Gets the XDR output stream.
5452
*
55-
* @return Instance of XdrOutputStream
53+
* @return instance of {@link XdrOutputStream}
5654
* @throws SQLException
57-
* If no connection is opened or when exceptions occur
58-
* retrieving the OutputStream
55+
* if no connection is opened or when exceptions occur retrieving the OutputStream
56+
* @see #withTransmitLock(TransmitAction)
5957
*/
6058
XdrOutputStream getXdrOut() throws SQLException;
59+
60+
/**
61+
* Runs {@link TransmitAction#transmit(XdrOutputStream)} with {@link #getXdrOut()} on {@code transmitAction} under
62+
* the transmit lock.
63+
* <p>
64+
* For Jaybird 5 and 6, the transmit lock only needs to be used for statement operations and cancellation. See also
65+
* <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>
66+
* </p>
67+
* <p>
68+
* The transmit lock should only cover sending messages to the server. It should be held for the duration of the
69+
* entire message. It <strong>must</strong> be released <em>before</em> reading (receiving) messages from the
70+
* server. If possible, do not do anything other than writing to the XDR output stream while holding the lock.
71+
* </p>
72+
* <p>
73+
* Normal operations <strong>must</strong> obtain the lock while holding the connection lock (i.e. the various
74+
* {@code withLock()} methods). Out-of-band operations (e.g. cancellation) <strong>must not</strong> take out the
75+
* connection lock, otherwise they can't be out-of-band.
76+
* </p>
77+
* <p>
78+
* Note for implementations: the lock used must be reentrant.
79+
* </p>
80+
*
81+
* @param transmitAction
82+
* the transmit action to run under lock
83+
* @throws IOException
84+
* for errors writing to the XDR output stream
85+
* @throws SQLException
86+
* for other database access errors
87+
* @see TransmitAction
88+
* @since 5.0.11
89+
*/
90+
void withTransmitLock(TransmitAction transmitAction) throws IOException, SQLException;
91+
6192
}

0 commit comments

Comments
 (0)