Skip to content

AG-304 - use var handle in wrappers#188

Merged
barreiro merged 1 commit intoagroal:masterfrom
barreiro:var-handle
May 4, 2026
Merged

AG-304 - use var handle in wrappers#188
barreiro merged 1 commit intoagroal:masterfrom
barreiro:var-handle

Conversation

@barreiro
Copy link
Copy Markdown
Contributor

@barreiro barreiro commented May 4, 2026

replaces direct field access to the wrapped JDBC objects with VarHandle using getAcquire/setRelease memory ordering

this provides proper visibility guarantees across threads without the overhead of full volatile semantics

@barreiro barreiro requested review from gastaldi and graben May 4, 2026 11:36
gastaldi

This comment was marked as outdated.

@gastaldi
Copy link
Copy Markdown
Collaborator

gastaldi commented May 4, 2026

Is the linked JIRA reference correct?

Copy link
Copy Markdown
Collaborator

@gastaldi gastaldi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@barreiro
Copy link
Copy Markdown
Contributor Author

barreiro commented May 4, 2026

Is the linked JIRA reference correct?

it addresses the (potential) interleaving issue. I can feel a new issue specifically for this if you want,

@gastaldi
Copy link
Copy Markdown
Collaborator

gastaldi commented May 4, 2026

No need for a new JIRA issue, it's fine to reuse the same if it's related

Copy link
Copy Markdown
Collaborator

@gastaldi gastaldi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(): unconditional setRelease + super.close(), no guard, no TOCTOU.
  • ResultSetWrapper#close(): no guard, always calls close() on the current value, sentinel set in finally. 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.

@barreiro
Copy link
Copy Markdown
Contributor Author

barreiro commented May 4, 2026

@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 getAndSet operation. the chance of ending up executing the cleanup code twice is low.

Copy link
Copy Markdown

@franz1981 franz1981 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@barreiro barreiro merged commit 7ebe213 into agroal:master May 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants