Skip to content

fix(jdbc): serialize statement close and execute - #89

Merged
magnusp merged 1 commit into
mainfrom
fix/49-concurrent-close-execute
Oct 21, 2026
Merged

magnusp merged 1 commit into
mainfrom
fix/49-concurrent-close-execute

Conversation

@magnusp

@magnusp magnusp commented Oct 21, 2026

Copy link
Copy Markdown
Owner

Closes #49. Part of #54.

What changed

  • LibsqlStatement state transitions (execute, executeQuery, close, result-set installation and removal, generated keys, getResultSet, isClosed, closeOnCompletion) now share one private lock.
  • execute/executeQuery perform the network call outside the lock, re-check closed under the lock, and only attach the result set if close has not won. If close won, the produced result set is closed and not attached.
  • close() closes the current result set and generated keys while holding the lock, then marks the statement closed.
  • LibsqlPreparedStatement reuses the same assignResultSet/executeResult paths, so prepared statements get the same guarantee.
  • Added LibsqlStatementConcurrencyTest with a deterministic latch seam (beforeResultSetAssignment) between the closed check and result-set installation. The test starts close while execute is paused in the seam, so the race does not depend on timing luck.

Contract decision

This is a correctness fix, not an xerial parity change. #47 deliberately keeps execute-after-close working; that remains true: the call still executes and does not throw. The one observable tightening is that a result set produced after the statement is already closed is returned closed instead of open. This is required by #49's acceptance criterion that a closed statement never holds an open result set after execute returns, and it also covers the reproduction where close completes before the next execute starts.

Evidence

Failing before the fix (seam and tests present, locking absent):

./mvnw -pl libsql-jdbc -am -Dtest=LibsqlStatementConcurrencyTest -Dsurefire.failIfNoSpecifiedTests=false test
Tests run: 3, Failures: 3, Errors: 0, Skipped: 0
  plainExecuteQueryDoesNotAttachAnOpenResultSetWhenCloseWinsTheRace
  preparedExecuteQueryDoesNotAttachAnOpenResultSetWhenCloseWinsTheRace
  executeQueryOnAClosedStatementReturnsAClosedResultSet

Passing after the fix:

./mvnw -pl libsql-jdbc -am test
Tests run: 165, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

The concurrency test was also run five times in a row, all green. ./mvnw spotless:apply and ./mvnw spotless:check pass.

Deviations

None for the Hrana transport or sqld. The only intentional behavior change beyond the race is closing a result set produced by an execute on an already-closed statement, described under Contract decision.

@magnusp
magnusp force-pushed the fix/49-concurrent-close-execute branch from 3bf7247 to 42f0f66 Compare October 21, 2026 23:53
@magnusp
magnusp merged commit 42eb56c into main Oct 21, 2026
5 checks passed
@magnusp
magnusp deleted the fix/49-concurrent-close-execute branch October 21, 2026 23:57
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.

Concurrent close and execute can orphan a result set

1 participant