AG-304 - use var handle in wrappers#188
Conversation
|
Is the linked JIRA reference correct? |
gastaldi
left a comment
There was a problem hiding this comment.
ConnectionWrapper#close() has a TOCTOU (time-of-check-time-of-use) race:
if ( wrappedConnection() != ClosedConnection.INSTANCE ) { // getAcquire (read)
WRAPPED.setRelease( this, ClosedConnection.INSTANCE ); // setRelease (write)The read and write are two separate operations. If two threads (e.g., the application thread and the Narayana reaper) both call close() concurrently, both can see the non-closed value from getAcquire, both enter the if block, and both execute the cleanup code — pruneClosed(), closeAllAutocloseableElements(), onConnectionWrapperClose().
Consider using an atomic swap instead:
if ( WRAPPED.getAndSet( this, ClosedConnection.INSTANCE ) != ClosedConnection.INSTANCE ) {
pruneClosed();
if ( trackedStatements != null ) {
addLeakedStatements( trackedStatements.closeAllAutocloseableElements() );
}
handler.onConnectionWrapperClose( this );
}getAndSet atomically reads the old value and writes the sentinel in one shot. Only the thread that swaps away the real connection enters the block — the other sees ClosedConnection.INSTANCE and skips it.
it addresses the (potential) interleaving issue. I can feel a new issue specifically for this if you want, |
|
No need for a new JIRA issue, it's fine to reuse the same if it's related |
gastaldi
left a comment
There was a problem hiding this comment.
Audit of all close() methods — TOCTOU and double-read issues
Since the goal of AG-304 is to fix interleaving with the Narayana reaper thread, all close() methods that guard on the sentinel should use atomic swaps to prevent concurrent threads from executing cleanup code twice.
XAConnectionWrapper#close() — same TOCTOU as ConnectionWrapper#close()
if ( wrappedXAConnection() != ClosedXAConnection.INSTANCE ) { // getAcquire
WRAPPED.setRelease( this, ClosedXAConnection.INSTANCE ); // setRelease
// cleanup: closeAllAutocloseableElements, handler.onConnectionWrapperClose
}Same race as ConnectionWrapper#close() — two threads can both pass the guard and run cleanup twice. Same fix applies:
if ( WRAPPED.getAndSet( this, ClosedXAConnection.INSTANCE ) != ClosedXAConnection.INSTANCE ) {
...
}StatementWrapper#close() — double-read
if ( wrappedStatement() != ClosedStatement.INSTANCE ) { // 1st getAcquire
...
wrappedStatement().close(); // 2nd getAcquire
}Between the two getAcquire calls, another thread could swap the field to the sentinel. The guard passes on the real statement, but .close() is then called on ClosedStatement.INSTANCE. Likely benign (the sentinel's close() is probably a no-op), but it's a logical race. Should cache the read in a local:
Statement stmt = wrappedStatement();
if ( stmt != ClosedStatement.INSTANCE ) {
...
stmt.close();
}XAResourceWrapper#close() — TOCTOU but harmless
if ( wrappedXAResource() != ClosedXAResource.INSTANCE ) {
WRAPPED.setRelease( this, ClosedXAResource.INSTANCE );
}Two threads could both enter, but the body is just the idempotent setRelease — no side-effectful cleanup. Not a bug, but getAndSet would be consistent with the other wrappers.
The rest are fine
CallableStatementWrapper#close()/PreparedStatementWrapper#close(): unconditionalsetRelease+super.close(), no guard, no TOCTOU.ResultSetWrapper#close(): no guard, always callsclose()on the current value, sentinel set infinally. Same behavior as the original.ConnectionWrapper#abort(): correctly caches the real connection in a local before setting the sentinel — this is actually a bugfix over the original code.
|
@gastaldi the cleanup code that is executed is idempotent (it's safe to be called multiple times) and should also be safe to be called concurrently. I would like to avoid the overhead of a full |
franz1981
left a comment
There was a problem hiding this comment.
If the previous way to write/read the field was unguarded - it means it was relying on external synchronization to make it visible (and read it too).
If it was correct before, it means you can keep it as it was. If it was wrong (because it proved to be wrong!) - than acquire/release is correct as you have done it here
replaces direct field access to the wrapped JDBC objects with
VarHandleusing getAcquire/setRelease memory orderingthis provides proper visibility guarantees across threads without the overhead of full volatile semantics