fix: close IO resources left open on abrupt or failing paths - #3532
Merged
Conversation
Motivation: Three places under src/main open an IO resource and leave it open on at least one reachable path: - ResolvConfParser.parseFile never closes the Stream returned by Files.lines, so the fd on /etc/resolv.conf stays open until GC. This is the only use of Files.lines in the tree. - InputStreamSource.postStop fails the materialized value without closing the user-supplied InputStream. Every handler path closes it, but abrupt termination (materializer or actor system shutdown) skips the handlers, so the stream leaks. The sibling stages FileSource and OutputStreamGraphStage already close in postStop. - ArteryAeronUdpTransport.autoSelectPort leaks the DatagramChannel when bind throws. Modification: Close the Files.lines stream in a finally; parseLines consumes the iterator eagerly, so an eager close is safe. Close the input stream in InputStreamSource.postStop before failing the promise. The close is inlined rather than delegated to closeInputStream, because that helper routes failures through failStage, which is pointless on an already torn-down stage. A close failure is logged at debug and the promise still fails with AbruptStageTerminationException. Wrap the bind in autoSelectPort in try/finally. Result: None of the three paths leaks a file descriptor. Tests: - sbt "stream-tests/testOnly org.apache.pekko.stream.io.InputStreamSourceSpec" - 9 succeeded, 0 failed. The new case "close the input stream on actor materializer shutdown" fails without the InputStreamSource change (8 succeeded, 1 failed) and passes with it. - sbt "actor/compile" "stream/compile" "remote/compile" - success - sbt "actor/mimaReportBinaryIssues" "stream/mimaReportBinaryIssues" "remote/mimaReportBinaryIssues" - success - Native scalafmt run on the four changed files. - No test for the ResolvConfParser and autoSelectPort fixes: neither changes observable behaviour, and asserting an fd was released is not possible portably from Scala. References: None - found by a resource-leak audit of src/main
nvollmar
approved these changes
Sep 8, 2026
Philippus
reviewed
Sep 8, 2026
Comment on lines
35
to
+39
| Try { | ||
| parseLines(Files.lines(file.toPath).iterator().asScala) | ||
| // the stream holds the file open until closed, and parseLines consumes it eagerly | ||
| val lines = Files.lines(file.toPath) | ||
| try parseLines(lines.iterator().asScala) | ||
| finally lines.close() |
Member
There was a problem hiding this comment.
Maybe use scala.util.Using?
scala.util.Using(Files.lines(file.toPath)) { lines =>
parseLines(lines.iterator().asScala)
}
Member
Author
There was a problem hiding this comment.
The current code base prefers Java try with resources probably because Scala Using is not supported directly by Scala 2.12. If we want to start using Scala Using, I think we should have a general PR to do that across existing call sites. Apparently, it is pretty efficient and shouldn't be significantly different performance wise to try with resources.
| try { | ||
| socket.bind(new InetSocketAddress(hostname, 0)) | ||
| socket.getLocalPort | ||
| } finally socket.close() |
Member
There was a problem hiding this comment.
can also be used here:
Using.resource(DatagramChannel.open().socket()) { socket =>
socket.bind(new InetSocketAddress(hostname, 0))
socket.getLocalPort
}
Philippus
approved these changes
Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
An audit of
src/mainfor resource leaks turned up three places that open an IO resource and leave it open on at least one reachable path:ResolvConfParser.parseFilenever closes theStreamreturned byFiles.lines, so the fd on/etc/resolv.confstays open until GC. This is the only use ofFiles.linesin the tree.InputStreamSource.postStopfails the materialized value without closing the user-suppliedInputStream. Every handler path closes it, but abrupt termination (materializer or actor system shutdown) skips the handlers, so the stream leaks. The sibling stagesFileSourceandOutputStreamGraphStagealready close inpostStop, so this one was the odd one out.ArteryAeronUdpTransport.autoSelectPortleaks theDatagramChannelwhenbindthrows.Modification
Close the
Files.linesstream in afinally.parseLinesconsumes the iterator eagerly, so an eager close is safe.Close the input stream in
InputStreamSource.postStopbefore failing the promise. The close is inlined rather than delegated tocloseInputStream, because that helper routes failures throughfailStage, which is pointless on an already torn-down stage. A close failure is logged at debug and the promise still fails withAbruptStageTerminationException.Wrap the bind in
autoSelectPortintry/finally.All three are method-body changes with no signature change.
Result
None of the three paths leaks a file descriptor.
Tests
sbt "stream-tests/testOnly org.apache.pekko.stream.io.InputStreamSourceSpec"— 9 succeeded, 0 failed. The new case"close the input stream on actor materializer shutdown"fails without theInputStreamSourcechange (8 succeeded, 1 failed) and passes with it.sbt "actor/compile" "stream/compile" "remote/compile"— successsbt "actor/mimaReportBinaryIssues" "stream/mimaReportBinaryIssues" "remote/mimaReportBinaryIssues"— successscalafmtrun on the four changed files.ResolvConfParserandautoSelectPortfixes: neither changes observable behaviour, and asserting that an fd was released is not possible portably from Scala. Happy to add a Linux-guarded fd-count assertion if reviewers want one.References
None - found by a resource-leak audit of
src/main