Skip to content

Commit a7464a5

Browse files
committed
#879 Statement close of a leaked statement did not detect fatal connection errors
Also fixes an NPE when the connection was terminated due to a fatal connection error.
1 parent 800dbba commit a7464a5

File tree

7 files changed

+190
-34
lines changed

7 files changed

+190
-34
lines changed

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.firebirdsql.gds.impl.wire.XdrInputStream;
77
import org.firebirdsql.gds.impl.wire.XdrOutputStream;
88
import org.firebirdsql.gds.ng.*;
9+
import org.jspecify.annotations.NullMarked;
910

1011
import java.io.IOException;
1112
import java.sql.SQLException;
@@ -210,9 +211,52 @@ public final Response readResponse(WarningMessageCallback warningCallback) throw
210211
return wireOperations.readResponse(warningCallback);
211212
}
212213

214+
/**
215+
* {@inheritDoc}
216+
* <p>
217+
* The implementation decorates {@code deferredAction} to ensure this database instance is notified of exceptions.
218+
* This can result in double notifications if the caller also notifies exceptions and this database instance is
219+
* registered as an exception listener for those notifications (this is not harmful).
220+
* </p>
221+
*/
213222
@Override
214223
public final void enqueueDeferredAction(DeferredAction deferredAction) throws SQLException {
215-
wireOperations.enqueueDeferredAction(deferredAction);
224+
wireOperations.enqueueDeferredAction(decorateWithExceptionNotification(deferredAction));
225+
}
226+
227+
@NullMarked
228+
private DeferredAction decorateWithExceptionNotification(DeferredAction deferredAction) {
229+
return deferredAction instanceof ExceptionNotifyingDeferredAction
230+
// Don't decorate again
231+
? deferredAction
232+
: new ExceptionNotifyingDeferredAction(deferredAction);
233+
}
234+
235+
@NullMarked
236+
private final class ExceptionNotifyingDeferredAction extends DeferredAction.DelegatingDeferredAction {
237+
238+
ExceptionNotifyingDeferredAction(DeferredAction delegate) {
239+
super(delegate);
240+
}
241+
242+
@Override
243+
public void onException(Exception exception) {
244+
try {
245+
super.onException(exception);
246+
} finally {
247+
exceptionListenerDispatcher.errorOccurred(toReadSQLException(exception));
248+
}
249+
}
250+
251+
private static SQLException toReadSQLException(Exception exception) {
252+
if (exception instanceof SQLException sqle) {
253+
return sqle;
254+
} else if (exception instanceof IOException ioe) {
255+
return FbExceptionBuilder.ioReadError(ioe);
256+
}
257+
return new SQLException(exception);
258+
}
259+
216260
}
217261

218262
@Override

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,18 +229,6 @@ private static final class CleanupAction implements Runnable, StatementListener,
229229
private static final AtomicReferenceFieldUpdater<CleanupAction, FbWireDatabase> databaseUpdater =
230230
AtomicReferenceFieldUpdater.newUpdater(CleanupAction.class, FbWireDatabase.class, "database");
231231

232-
private static final DeferredAction CLEANUP_FREE_DEFERRED_ACTION = new DeferredAction() {
233-
@Override
234-
public void processResponse(Response response) {
235-
// nothing to do
236-
}
237-
238-
@Override
239-
public boolean requiresSync() {
240-
return true;
241-
}
242-
};
243-
244232
private final int handle;
245233
private volatile FbWireDatabase database;
246234

@@ -296,7 +284,7 @@ public void run() {
296284
xdrOut.flush();
297285
});
298286
// TODO: This may process deferred actions on the cleaner thread, we may want to change that
299-
database.enqueueDeferredAction(CLEANUP_FREE_DEFERRED_ACTION);
287+
database.enqueueDeferredAction(DeferredAction.NO_OP_INSTANCE);
300288
} catch (SQLException | IOException e) {
301289
System.getLogger(getClass().getName()).log(System.Logger.Level.TRACE,
302290
"Ignored exception during statement clean up", e);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public final Response readOperationResponse(int operationCode, WarningMessageCal
130130
}
131131

132132
@Override
133-
public final int readNextOperation() throws IOException {
133+
public final int readNextOperation() throws SQLException, IOException {
134134
try (LockCloseable ignored = withLock()) {
135135
processDeferredActions();
136136
return connection.readNextOperation();

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

Lines changed: 130 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
// SPDX-FileCopyrightText: Copyright 2014-2023 Mark Rotteveel
1+
// SPDX-FileCopyrightText: Copyright 2014-2026 Mark Rotteveel
22
// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
33
package org.firebirdsql.gds.ng.wire;
44

55
import org.firebirdsql.gds.ng.DeferredResponse;
66
import org.firebirdsql.gds.ng.WarningMessageCallback;
7+
import org.jspecify.annotations.NullMarked;
8+
import org.jspecify.annotations.Nullable;
79

810
import java.util.function.Consumer;
911
import java.util.function.Function;
1012

1113
import static java.lang.System.Logger.Level.DEBUG;
14+
import static java.util.Objects.requireNonNull;
1215

1316
/**
1417
* Interface for processing deferred responses from the server.
1518
* <p>
16-
* This interfaces is used in protocol 11 or higher.
19+
* This interface is used in protocol 11 or higher.
1720
* </p>
1821
*
1922
* @author Mark Rotteveel
2023
* @since 3.0
2124
*/
25+
@NullMarked
2226
public interface DeferredAction {
2327

2428
/**
@@ -62,7 +66,7 @@ default void onException(Exception exception) {
6266
*
6367
* @return warning callback to use when executing this deferred action, {@code null} signals to use the default
6468
*/
65-
default WarningMessageCallback getWarningMessageCallback() {
69+
default @Nullable WarningMessageCallback getWarningMessageCallback() {
6670
return null;
6771
}
6872

@@ -97,25 +101,99 @@ default boolean requiresSync() {
97101
* @return deferred action
98102
*/
99103
static <T> DeferredAction wrapDeferredResponse(DeferredResponse<T> deferredResponse,
100-
Function<Response, T> responseMapper, WarningMessageCallback warningMessageCallback,
104+
Function<Response, T> responseMapper, @Nullable WarningMessageCallback warningMessageCallback,
101105
Consumer<Exception> exceptionConsumer, boolean requiresSync) {
102-
return new DeferredAction() {
106+
return builder()
107+
.withProcessResponse(response -> deferredResponse.onResponse(responseMapper.apply(response)))
108+
.withOnException(exception -> {
109+
try {
110+
deferredResponse.onException(exception);
111+
} finally {
112+
exceptionConsumer.accept(exception);
113+
}
114+
})
115+
.withWarningMessageCallback(warningMessageCallback)
116+
.withRequiresSync(requiresSync)
117+
.build();
118+
}
119+
120+
/**
121+
* @return builder for a deferred action instance.
122+
* @since 7
123+
*/
124+
static Builder builder() {
125+
return new Builder();
126+
}
127+
128+
/**
129+
* Builder for lambda-based deferred actions.
130+
*
131+
* @since 7
132+
*/
133+
final class Builder {
134+
135+
private static final Consumer<Response> RESPONSE_NO_OP = r -> {};
136+
private static final Consumer<Exception> EXCEPTION_NO_OP = e ->
137+
System.getLogger(DeferredAction.class.getName()).log(DEBUG, "Exception in processDeferredActions", e);
138+
139+
private Consumer<Response> processResponse = RESPONSE_NO_OP;
140+
private Consumer<Exception> onException = EXCEPTION_NO_OP;
141+
private @Nullable WarningMessageCallback warningMessageCallback;
142+
private boolean requiresSync;
143+
144+
private Builder() {
145+
}
146+
147+
public Builder withProcessResponse(Consumer<Response> processResponse) {
148+
this.processResponse = processResponse;
149+
return this;
150+
}
151+
152+
public Builder withOnException(Consumer<Exception> onException) {
153+
this.onException = onException;
154+
return this;
155+
}
156+
157+
public Builder withWarningMessageCallback(@Nullable WarningMessageCallback warningMessageCallback) {
158+
this.warningMessageCallback = warningMessageCallback;
159+
return this;
160+
}
161+
162+
public Builder withRequiresSync(boolean requiresSync) {
163+
this.requiresSync = requiresSync;
164+
return this;
165+
}
166+
167+
public DeferredAction build() {
168+
if (processResponse == RESPONSE_NO_OP && onException == EXCEPTION_NO_OP && warningMessageCallback == null
169+
&& !requiresSync) {
170+
return NO_OP_INSTANCE;
171+
}
172+
173+
return new DeferredActionImpl(processResponse, onException, warningMessageCallback, requiresSync);
174+
}
175+
176+
/**
177+
* Lambda-based deferred action implementation.
178+
*
179+
* @since 7
180+
*/
181+
private record DeferredActionImpl(Consumer<Response> processResponse, Consumer<Exception> onException,
182+
@Nullable WarningMessageCallback warningMessageCallback, boolean requiresSync)
183+
implements DeferredAction {
184+
103185
@Override
104186
public void processResponse(Response response) {
105-
deferredResponse.onResponse(responseMapper.apply(response));
187+
processResponse.accept(response);
106188
}
107189

108190
@Override
109191
public void onException(Exception exception) {
110-
try {
111-
deferredResponse.onException(exception);
112-
} finally {
113-
exceptionConsumer.accept(exception);
114-
}
192+
onException.accept(exception);
115193
}
116194

117195
@Override
118-
public WarningMessageCallback getWarningMessageCallback() {
196+
public @Nullable WarningMessageCallback getWarningMessageCallback() {
119197
return warningMessageCallback;
120198
}
121199

@@ -124,7 +202,46 @@ public boolean requiresSync() {
124202
return requiresSync;
125203
}
126204

127-
};
205+
}
206+
207+
}
208+
209+
/**
210+
* Deferred action implementation that delegates to another deferred action.
211+
* <p>
212+
* This class is intended as a base class for implementations that want to decorate method calls. To decorate it,
213+
* subclass this class, and override the method, and ensure you call {@code super.<overridden-method>} in such a
214+
* way that it is always called, even if the decoration fails.
215+
* </p>
216+
* @since 7
217+
*/
218+
abstract class DelegatingDeferredAction implements DeferredAction {
219+
220+
private final DeferredAction delegate;
221+
222+
DelegatingDeferredAction(DeferredAction delegate) {
223+
this.delegate = requireNonNull(delegate, "delegate");
224+
}
225+
226+
@Override
227+
public void processResponse(Response response) {
228+
delegate.processResponse(response);
229+
}
230+
231+
@Override
232+
public void onException(Exception exception) {
233+
delegate.onException(exception);
234+
}
235+
236+
@Override
237+
public @Nullable WarningMessageCallback getWarningMessageCallback() {
238+
return delegate.getWarningMessageCallback();
239+
}
240+
241+
@Override
242+
public boolean requiresSync() {
243+
return delegate.requiresSync();
244+
}
128245
}
129246

130247
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ public interface FbWireOperations {
5959
* </p>
6060
*
6161
* @return next operation
62-
* @throws java.io.IOException
62+
* @throws SQLException
63+
* when the connection is closed
64+
* @throws IOException
6365
* for errors reading the operation from the connection
6466
* @since 6
6567
*/
66-
int readNextOperation() throws IOException;
68+
int readNextOperation() throws SQLException, IOException;
6769

6870
/**
6971
* Reads the response from the server when the operation code has already been read.

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -635,11 +635,14 @@ private FbWireOperations getCryptKeyCallbackWireOperations() {
635635
* Reads the next operation code. Skips all {@link org.firebirdsql.gds.impl.wire.WireProtocolConstants#op_dummy}
636636
* codes received.
637637
*
638-
* @return Operation code
638+
* @return operation code
639+
* @throws SQLException
640+
* when the connection is closed
639641
* @throws IOException
640642
* if an error occurs while reading from the underlying InputStream
641643
*/
642-
public final int readNextOperation() throws IOException {
644+
public final int readNextOperation() throws SQLException, IOException {
645+
final XdrInputStream xdrIn = getXdrStreamAccess().getXdrIn();
643646
int op;
644647
do {
645648
op = xdrIn.readInt();

src/test/org/firebirdsql/management/FBMaintenanceManagerTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.firebirdsql.common.extension.RunEnvironmentExtension.EnvironmentRequirement;
1111
import org.firebirdsql.common.extension.UsesDatabaseExtension;
1212
import org.firebirdsql.gds.ISCConstants;
13+
import org.firebirdsql.gds.JaybirdErrorCodes;
1314
import org.firebirdsql.gds.TransactionParameterBuffer;
1415
import org.firebirdsql.gds.ng.FbDatabase;
1516
import org.firebirdsql.gds.ng.FbTransaction;
@@ -179,7 +180,7 @@ void testSetBadDialect() {
179180
@ValueSource(booleans = { true, false })
180181
void testForcedShutdown(boolean autoCommit) throws Exception {
181182
assumeThat("Test doesn't work correctly under embedded", GDS_TYPE, not(isEmbeddedType()));
182-
try (Connection conn = getConnectionViaDriverManager()) {
183+
try (var conn = getConnectionViaDriverManager()) {
183184
createTestTable();
184185

185186
conn.setAutoCommit(autoCommit);
@@ -191,7 +192,8 @@ void testForcedShutdown(boolean autoCommit) throws Exception {
191192

192193
SQLException exception = assertThrows(SQLException.class, () -> stmt.executeQuery(sql));
193194
assertThat(exception, errorCode(oneOf(
194-
ISCConstants.isc_shutdown, ISCConstants.isc_att_shutdown, ISCConstants.isc_net_read_err)));
195+
ISCConstants.isc_shutdown, ISCConstants.isc_att_shutdown, ISCConstants.isc_net_read_err,
196+
JaybirdErrorCodes.jb_connectionClosed)));
195197
} finally {
196198
closeQuietly(stmt);
197199
}

0 commit comments

Comments
 (0)