Skip to content

ClosingStream should close streams on thrown exception - #52582

Draft
ahus1 wants to merge 2 commits into
keycloak:mainfrom
ahus1:is-52580-closing-streams-on-exceptions
Draft

ClosingStream should close streams on thrown exception#52582
ahus1 wants to merge 2 commits into
keycloak:mainfrom
ahus1:is-52580-closing-streams-on-exceptions

Conversation

@ahus1

@ahus1 ahus1 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Closes #52580

Summary

Ensure ClosingStream, ClosingIntStream, ClosingLongStream, and ClosingDoubleStream close the underlying stream when a terminal operation throws an exception. Previously, exceptions during terminal operations would bypass close(), leaking JDBC ResultSet cursors.

Also fixes asLongStream(), asDoubleStream() in ClosingIntStream and asDoubleStream() in ClosingLongStream which were incorrectly treated as terminal operations instead of intermediate operations.

Decisions and assumptions

  • try-finally for all terminal operations: Every terminal method (forEach, toArray, reduce, collect, count, min, max, anyMatch, allMatch, noneMatch, findFirst, findAny, toList, sum, average) wraps the delegate call in try-finally to guarantee close() runs regardless of success or failure.

  • try-catch for hasNext() and tryAdvance(): These methods are called repeatedly — closing on every call would break iteration. Instead, they close on exception (via try-catch) and on exhaustion (when returning false). This preserves the existing behavior of closing when the stream is fully consumed.

  • try-finally for forEachRemaining(): Both ClosingIterator.forEachRemaining() and ClosingSpliterator.forEachRemaining() consume all remaining elements in a single call, so try-finally is appropriate here.

  • No change to next(): Iterator.next() is always preceded by hasNext(), which already handles close-on-error and close-on-exhaustion.

  • asLongStream() / asDoubleStream() are intermediate, not terminal: These were incorrectly closing the underlying stream and returning an unwrapped stream. Fixed to return ClosingLongStream / ClosingDoubleStream wrappers, consistent with boxed().

  • No suppressed exception handling in try-finally: If both the terminal operation and close() throw, the close exception masks the original. This matches standard try-finally semantics and is acceptable because Stream.close() internally catches and aggregates exceptions from onClose() handlers, making a throwing close() extremely unlikely.

  • All four stream classes updated consistently: The same pattern is applied to ClosingStream<R>, ClosingIntStream, ClosingLongStream, and ClosingDoubleStream.

  • Tests cover all stream types: Exception-on-terminal tests added for ClosingStream, ClosingIntStream, ClosingLongStream, and ClosingDoubleStream. Additional tests verify that asLongStream() and asDoubleStream() preserve close propagation.

Closes keycloak#52580

Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
@ahus1 ahus1 self-assigned this Sep 9, 2026
@ahus1
ahus1 requested a balanced review from Copilot September 9, 2026 16:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

This PR ensures ClosingStream and its primitive variants reliably close their underlying streams even when terminal operations throw, preventing resource leaks (e.g., JDBC ResultSet cursors).

Changes:

  • Wrapped terminal operations in try/finally to guarantee close() executes on success or failure.
  • Updated iterator/spliterator traversal (hasNext, tryAdvance, forEachRemaining) to close on exhaustion and on thrown exceptions.
  • Added JUnit tests validating closure behavior when terminal operations fail.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
server-spi-private/src/main/java/org/keycloak/utils/ClosingStream.java Ensures close-on-exception for terminal ops; closes on iterator/spliterator errors and exhaustion.
server-spi-private/src/main/java/org/keycloak/utils/ClosingIntStream.java Applies the same close-on-exception behavior to IntStream terminals and traversal helpers.
server-spi-private/src/main/java/org/keycloak/utils/ClosingLongStream.java Applies the same close-on-exception behavior to LongStream terminals and traversal helpers.
server-spi-private/src/main/java/org/keycloak/utils/ClosingDoubleStream.java Applies the same close-on-exception behavior to DoubleStream terminals and traversal helpers.
server-spi-private/src/test/java/org/keycloak/utils/StreamsUtilTest.java Adds regression tests for closing behavior when terminal operations throw.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread server-spi-private/src/main/java/org/keycloak/utils/ClosingIntStream.java Outdated
Comment thread server-spi-private/src/main/java/org/keycloak/utils/ClosingIntStream.java Outdated
Comment thread server-spi-private/src/main/java/org/keycloak/utils/ClosingLongStream.java Outdated
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
@ahus1
ahus1 requested a balanced review from Copilot September 9, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

DoubleStream result = delegate.asDoubleStream();
close();
return result;
return new ClosingDoubleStream(delegate.asDoubleStream());
Comment on lines +128 to +132
try {
delegate.forEach(action);
} finally {
close();
}
Comment on lines +329 to +335
boolean res;
try {
res = iterator.hasNext();
} catch (RuntimeException | Error e) {
close();
throw e;
}

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.actions.TermsAndConditionsTest#termsAccepted

Keycloak CI - Forms IT (firefox)

java.lang.RuntimeException: Could not create statement
	at org.jboss.arquillian.junit.Arquillian.methodBlock(Arquillian.java:307)
	at org.jboss.arquillian.junit.Arquillian$2.evaluate(Arquillian.java:160)
	at org.jboss.arquillian.junit.Arquillian.multiExecute(Arquillian.java:344)
	at org.jboss.arquillian.junit.Arquillian.access$200(Arquillian.java:48)
...

Report flaky test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ClosingStream does not close underlying stream when terminal operation throws an exception

2 participants