This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git
The following commit(s) were added to refs/heads/main by this push:
new 14dc8a676c fix: close IO resources left open on abrupt or failing
paths (#3532)
14dc8a676c is described below
commit 14dc8a676ccf127fc3aea5099ab3a0c258c9a815
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 8 09:43:25 2026 +0100
fix: close IO resources left open on abrupt or failing paths (#3532)
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
---
.../pekko/io/dns/internal/ResolvConfParser.scala | 5 ++++-
.../artery/aeron/ArteryAeronUdpTransport.scala | 8 ++++----
.../pekko/stream/io/InputStreamSourceSpec.scala | 20 +++++++++++++++++++-
.../pekko/stream/impl/io/InputStreamSource.scala | 7 +++++++
4 files changed, 34 insertions(+), 6 deletions(-)
diff --git
a/actor/src/main/scala/org/apache/pekko/io/dns/internal/ResolvConfParser.scala
b/actor/src/main/scala/org/apache/pekko/io/dns/internal/ResolvConfParser.scala
index 7678af8537..07f07b7521 100644
---
a/actor/src/main/scala/org/apache/pekko/io/dns/internal/ResolvConfParser.scala
+++
b/actor/src/main/scala/org/apache/pekko/io/dns/internal/ResolvConfParser.scala
@@ -33,7 +33,10 @@ private[dns] object ResolvConfParser {
*/
def parseFile(file: File): Try[ResolvConf] = {
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()
}
}
diff --git
a/remote/src/main/scala/org/apache/pekko/remote/artery/aeron/ArteryAeronUdpTransport.scala
b/remote/src/main/scala/org/apache/pekko/remote/artery/aeron/ArteryAeronUdpTransport.scala
index 93160b85ba..d8be809b91 100644
---
a/remote/src/main/scala/org/apache/pekko/remote/artery/aeron/ArteryAeronUdpTransport.scala
+++
b/remote/src/main/scala/org/apache/pekko/remote/artery/aeron/ArteryAeronUdpTransport.scala
@@ -481,9 +481,9 @@ private[remote] class ArteryAeronUdpTransport(_system:
ExtendedActorSystem, _pro
import java.nio.channels.DatagramChannel
val socket = DatagramChannel.open().socket()
- socket.bind(new InetSocketAddress(hostname, 0))
- val port = socket.getLocalPort
- socket.close()
- port
+ try {
+ socket.bind(new InetSocketAddress(hostname, 0))
+ socket.getLocalPort
+ } finally socket.close()
}
}
diff --git
a/stream-tests/src/test/scala/org/apache/pekko/stream/io/InputStreamSourceSpec.scala
b/stream-tests/src/test/scala/org/apache/pekko/stream/io/InputStreamSourceSpec.scala
index fa6de2b7ed..b2fa0c9fca 100644
---
a/stream-tests/src/test/scala/org/apache/pekko/stream/io/InputStreamSourceSpec.scala
+++
b/stream-tests/src/test/scala/org/apache/pekko/stream/io/InputStreamSourceSpec.scala
@@ -14,7 +14,7 @@
package org.apache.pekko.stream.io
import java.io.{ ByteArrayInputStream, InputStream }
-import java.util.concurrent.CountDownLatch
+import java.util.concurrent.{ CountDownLatch, TimeUnit }
import scala.annotation.nowarn
import scala.util.Success
@@ -123,6 +123,24 @@ class InputStreamSourceSpec extends
StreamSpec(UnboundedMailboxConfig) {
f.failed.futureValue shouldBe an[AbruptStageTerminationException]
}
+ "close the input stream on actor materializer shutdown" in {
+ val mat = ActorMaterializer()
+ val closed = new CountDownLatch(1)
+ val source = StreamConverters.fromInputStream(() =>
+ new InputStream {
+ override def read(): Int = -1
+ override def close(): Unit = closed.countDown()
+ })
+ val pubSink = Sink.asPublisher[ByteString](false)
+ val (f, neverPub) = source.toMat(pubSink)(Keep.both).run()(mat)
+ val c = TestSubscriber.manualProbe[ByteString]()
+ neverPub.subscribe(c)
+ c.expectSubscription()
+ mat.shutdown()
+ f.failed.futureValue shouldBe an[AbruptStageTerminationException]
+ closed.await(3, TimeUnit.SECONDS) shouldBe true
+ }
+
"emit as soon as read" in {
val latch = new CountDownLatch(1)
val probe = StreamConverters
diff --git
a/stream/src/main/scala/org/apache/pekko/stream/impl/io/InputStreamSource.scala
b/stream/src/main/scala/org/apache/pekko/stream/impl/io/InputStreamSource.scala
index 5a483700ad..cc43be35d7 100644
---
a/stream/src/main/scala/org/apache/pekko/stream/impl/io/InputStreamSource.scala
+++
b/stream/src/main/scala/org/apache/pekko/stream/impl/io/InputStreamSource.scala
@@ -101,6 +101,13 @@ private[pekko] final class InputStreamSource(factory: ()
=> InputStream, chunkSi
override def postStop(): Unit = {
if (!isClosed) {
+ // abrupt termination skips the handlers, so this is the last chance
to release the stream.
+ // The stage is already torn down here, so a close failure is not
routed through failStage.
+ try {
+ if (inputStream ne null) inputStream.close()
+ } catch {
+ case NonFatal(ex) => log.debug("Failed to close input stream on
abrupt termination: {}", ex.getMessage)
+ }
mat.tryFailure(new AbruptStageTerminationException(this))
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]