Skip to content

Commit b5a744f

Browse files
authored
Fixes erroneous closing behavior in JtaConnection.java (helidon-io#6321)
* Fixes erroneous closing behavior in JtaConnection.java Signed-off-by: Laird Nelson <laird.nelson@oracle.com>
1 parent 0706517 commit b5a744f

3 files changed

Lines changed: 148 additions & 56 deletions

File tree

integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/ConditionallyCloseableConnection.java

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2022 Oracle and/or its affiliates.
2+
* Copyright (c) 2021, 2023 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -94,28 +94,42 @@ public class ConditionallyCloseableConnection extends DelegatingConnection {
9494
private SQLBooleanSupplier isClosedFunction;
9595

9696
/**
97-
* Whether or not the {@link #close()} method will actually close this {@link DelegatingConnection}.
97+
* The internal state of this {@link ConditionallyCloseableConnection}.
9898
*
99-
* <p>This field is set based on the value of the {@code strictClosedChecking} argument supplied to the {@link
100-
* #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor. It may end up deliberately doing
101-
* nothing.</p>
99+
* <p>This field is never {@code null}.</p>
100+
*
101+
* <!--
102+
* digraph ConditionallyCloseableConnection {
103+
*
104+
* CLOSEABLE -> CLOSED [label="close()"];
105+
* CLOSEABLE -> NOT_CLOSEABLE [label="setCloseable(false)"];
106+
* CLOSEABLE -> CLOSEABLE;
107+
*
108+
* NOT_CLOSEABLE -> CLOSE_PENDING [label="close()"];
109+
* NOT_CLOSEABLE -> NOT_CLOSEABLE [label="setCloseable(false), isCloseable(), isClosed()"];
110+
* NOT_CLOSEABLE -> CLOSEABLE [label="setCloseable(true)"];
111+
*
112+
* CLOSE_PENDING -> CLOSE_PENDING [label="close(), setCloseable(false), isCloseable(), isClosed()"];
113+
* CLOSE_PENDING -> CLOSEABLE [label="setCloseable(true)"];
114+
*
115+
* CLOSED -> CLOSED [label="close(), isCloseable(), isClosed()"];
116+
*
117+
* }
118+
* -->
119+
*
120+
* @see #isClosed()
102121
*
103122
* @see #isCloseable()
104123
*
105124
* @see #setCloseable(boolean)
106125
*
107-
* @see #ConditionallyCloseableConnection(Connection, boolean, boolean)
108-
*/
109-
private volatile boolean closeable;
110-
111-
/**
112-
* Whether or not a {@link #close()} request has been issued from another thread.
126+
* @see #close()
113127
*
114128
* @see #isClosePending()
115129
*
116-
* @see #isClosed()
130+
* @see #ConditionallyCloseableConnection(Connection, boolean, boolean)
117131
*/
118-
private volatile boolean closePending;
132+
private volatile State state;
119133

120134

121135
/*
@@ -205,12 +219,12 @@ public ConditionallyCloseableConnection(Connection delegate,
205219
super(delegate);
206220
if (strictClosedChecking) {
207221
this.closedChecker = this::failWhenClosed;
208-
this.isClosedFunction = () -> this.isClosePending() || super.isClosed();
222+
this.isClosedFunction = this::strictIsClosed;
209223
} else {
210224
this.closedChecker = ConditionallyCloseableConnection::doNothing;
211225
this.isClosedFunction = super::isClosed;
212226
}
213-
this.closeable = closeable;
227+
this.state = closeable ? State.CLOSEABLE : State.NOT_CLOSEABLE;
214228
}
215229

216230

@@ -252,15 +266,29 @@ public ConditionallyCloseableConnection(Connection delegate,
252266
@Override // DelegatingConnection
253267
public void close() throws SQLException {
254268
// this.checkOpen(); // Deliberately omitted per spec.
255-
if (this.isCloseable()) {
269+
switch (this.state) {
270+
case CLOSEABLE:
256271
try {
257272
super.close();
258273
} finally {
259-
this.closePending = false;
274+
this.state = State.CLOSED;
260275
this.onClose();
261276
}
262-
} else {
263-
this.closePending = true;
277+
break;
278+
case NOT_CLOSEABLE:
279+
this.state = State.CLOSE_PENDING;
280+
break;
281+
case CLOSE_PENDING:
282+
break;
283+
case CLOSED:
284+
try {
285+
super.close();
286+
} finally {
287+
this.onClose();
288+
}
289+
break;
290+
default:
291+
throw new AssertionError();
264292
}
265293
}
266294

@@ -329,7 +357,21 @@ protected void onClose() throws SQLException {
329357
*/
330358
public boolean isCloseable() throws SQLException {
331359
// this.checkOpen(); // Deliberately omitted.
332-
return this.closeable && !this.isClosed();
360+
switch (this.state) {
361+
case CLOSEABLE:
362+
return !this.isClosed(); // reduces to !super.isClosed() which reduces to !this.delegate().isClosed()
363+
case NOT_CLOSEABLE:
364+
assert !this.isClosed(); // reduces to !super.isClosed() which reduces to !this.delegate().isClosed()
365+
return false;
366+
case CLOSE_PENDING:
367+
// (Can't assert about isClosed() because its behavior depends on strictClosedChecking constructor parameter.)
368+
return false;
369+
case CLOSED:
370+
assert this.isClosed(); // reduces to super.isClosed() which reduces to this.delegate().isClosed()
371+
return false;
372+
default:
373+
throw new AssertionError();
374+
}
333375
}
334376

335377
/**
@@ -364,9 +406,22 @@ public boolean isCloseable() throws SQLException {
364406
*/
365407
public void setCloseable(boolean closeable) {
366408
// this.checkOpen(); // Deliberately omitted.
367-
this.closeable = closeable;
368-
if (closeable) {
369-
this.closePending = false;
409+
switch (this.state) {
410+
case CLOSEABLE:
411+
if (!closeable) {
412+
this.state = State.NOT_CLOSEABLE;
413+
}
414+
break;
415+
case NOT_CLOSEABLE:
416+
case CLOSE_PENDING:
417+
if (closeable) {
418+
this.state = State.CLOSEABLE;
419+
}
420+
break;
421+
case CLOSED:
422+
break;
423+
default:
424+
throw new AssertionError();
370425
}
371426
}
372427

@@ -393,7 +448,7 @@ public void setCloseable(boolean closeable) {
393448
*/
394449
public boolean isClosePending() {
395450
// this.checkOpen(); // Deliberately omitted.
396-
return this.closePending;
451+
return this.state == State.CLOSE_PENDING;
397452
}
398453

399454
@Override // DelegatingConnection
@@ -481,6 +536,11 @@ public boolean isClosed() throws SQLException {
481536
return this.isClosedFunction.getAsBoolean();
482537
}
483538

539+
// (Invoked by method reference only.)
540+
private boolean strictIsClosed() throws SQLException {
541+
return this.isClosePending() || super.isClosed();
542+
}
543+
484544
@Override // DelegatingConnection
485545
public DatabaseMetaData getMetaData() throws SQLException {
486546
this.checkOpen();
@@ -866,4 +926,50 @@ private static void doNothing() {
866926

867927
}
868928

929+
930+
/*
931+
* Inner and nested classes.
932+
*/
933+
934+
935+
/**
936+
* A state that a {@link ConditionallyCloseableConnection} can have.
937+
*/
938+
private enum State {
939+
940+
/**
941+
* A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link
942+
* ConditionallyCloseableConnection#close() close()} method will close {@linkplain
943+
* ConditionallyCloseableConnection#delegate() its underlying delegate}.
944+
*/
945+
CLOSEABLE,
946+
947+
/**
948+
* A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link
949+
* ConditionallyCloseableConnection#close() close()} method will place it into the {@link #CLOSE_PENDING} state.
950+
*
951+
* @see ConditionallyCloseableConnection#setCloseable(boolean)
952+
*/
953+
NOT_CLOSEABLE,
954+
955+
/**
956+
* A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link
957+
* ConditionallyCloseableConnection#close() close()} method has placed it into this state, and actual closing of
958+
* {@linkplain ConditionallyCloseableConnection#delegate() its underlying delegate} will need to be arranged.
959+
*
960+
* @see ConditionallyCloseableConnection#setCloseable(boolean)
961+
*/
962+
CLOSE_PENDING,
963+
964+
/**
965+
* A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link
966+
* ConditionallyCloseableConnection#close() close()} method has placed it into this state, and that {@linkplain
967+
* ConditionallyCloseableConnection#delegate() its underlying delegate} has also been closed.
968+
*
969+
* <p>This is a terminal state.</p>
970+
*/
971+
CLOSED;
972+
973+
}
974+
869975
}

integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaConnection.java

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ private void enlist() throws SQLException {
10491049
throw new SQLTransientException("xaResourceSupplier.get() == null");
10501050
}
10511051

1052-
Enlistment enlistment = new Enlistment(t, xar);
1052+
Enlistment enlistment = new Enlistment(Thread.currentThread().getId(), t, xar);
10531053
if (!ENLISTMENT.compareAndSet(this, null, enlistment)) { // atomic volatile write
10541054
// Setting this.enlistment could conceivably fail if another thread already enlisted this JtaConnection.
10551055
// That would be bad.
@@ -1124,27 +1124,18 @@ private Connection connectionFunction(Xid xid) {
11241124
private void transactionCompleted(int commitedOrRolledBack) {
11251125
this.enlistment = null; // volatile write
11261126
try {
1127-
if (!super.isClosed()) {
1128-
// Did someone call close() while we were enlisted? That's permitted by section 4.2 of the JTA
1129-
// specification, but it shouldn't actually close the connection because the prepare/commit cycle
1130-
// wouldn't have started.
1131-
boolean closeWasPending = this.isClosePending(); // volatile read
1132-
1133-
// If they did, then we must be non-closeable. If they didn't, we could be either closeable or not
1134-
// closeable.
1135-
assert !closeWasPending || !this.isCloseable();
1136-
1137-
// Now the global transaction is over, so blindly set our closeable status to true. (It may already be
1138-
// true.) This resets the closePending status, per spec.
1139-
1140-
this.setCloseable(true);
1141-
assert this.isCloseable();
1127+
boolean closeWasPending = this.isClosePending();
1128+
this.setCloseable(true);
1129+
assert this.isCloseable();
1130+
assert !this.isClosePending();
1131+
if (closeWasPending) {
1132+
assert !this.isClosed();
1133+
assert !this.delegate().isClosed();
1134+
this.close();
1135+
assert this.isClosed();
1136+
assert this.delegate().isClosed();
1137+
assert !this.isCloseable();
11421138
assert !this.isClosePending();
1143-
1144-
// If there WAS a close() attempt, now it CAN work, so let it work.
1145-
if (closeWasPending) {
1146-
this.close();
1147-
}
11481139
}
11491140
} catch (SQLException e) {
11501141
// (Synchronization implementations can throw only unchecked exceptions.)
@@ -1200,10 +1191,6 @@ default void beforeCompletion() {
12001191

12011192
private static final record Enlistment(long threadId, Transaction transaction, XAResource xaResource) {
12021193

1203-
private Enlistment(Transaction transaction, XAResource xaResource) {
1204-
this(Thread.currentThread().getId(), transaction, xaResource);
1205-
}
1206-
12071194
private Enlistment {
12081195
Objects.requireNonNull(transaction, "transaction");
12091196
Objects.requireNonNull(xaResource, "xaResource");

integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestJtaConnection.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,6 @@ final void testBeginSuspendBeginCommitResumeCommit()
332332
// The TransactionManager will report that there is no transaction.
333333
assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION));
334334

335-
// assertThat(logicalConnection.enlisted(), is(true)); // we're enlisted in a suspended transaction
336335
assertThat(logicalConnection.isCloseable(), is(false)); // we're still enlisted in a suspended transaction
337336

338337
logicalConnection.close(); // doesn't really close, but the caller thinks it did, which is what we want
@@ -378,7 +377,7 @@ final void testBeginSuspendBeginCommitResumeCommit()
378377
assertThat(physicalConnection2.isClosed(), is(true));
379378

380379
tm.resume(s);
381-
380+
382381
t = tm.getTransaction();
383382
assertThat(t, sameInstance(s));
384383
assertThat(t.getStatus(), is(Status.STATUS_ACTIVE));
@@ -392,12 +391,12 @@ final void testBeginSuspendBeginCommitResumeCommit()
392391

393392
tm.commit();
394393

395-
assertThat(logicalConnection.isClosePending(), is(true));
396-
397-
assertThat(logicalConnection.isClosed(), is(true)); // returns true only because close is pending
398-
assertThat(logicalConnection.delegate().isClosed(), is(false));
394+
// Now it should be closed for real.
395+
assertThat(logicalConnection.isClosePending(), is(false));
396+
assertThat(logicalConnection.isClosed(), is(true));
397+
assertThat(logicalConnection.delegate().isClosed(), is(true));
398+
assertThat(physicalConnection.isClosed(), is(true));
399399

400-
assertThat(physicalConnection.isClosed(), is(false)); // logicalConnection was never *really* closed
401400
assertThat(logicalConnection2.isClosed(), is(true));
402401
assertThat(physicalConnection2.isClosed(), is(true));
403402

0 commit comments

Comments
 (0)