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-connectors.git
The following commit(s) were added to refs/heads/main by this push:
new 85b5f4c9f unix-domain-socket: replace deprecated use of Source.queue
(#1857)
85b5f4c9f is described below
commit 85b5f4c9f6cc5c211a609591a77748684c390cd1
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 1 11:57:31 2026 +0100
unix-domain-socket: replace deprecated use of Source.queue (#1857)
* unix-domain-socket: fix one use of Source.queue
* remaining Source.queue case
---
.../impl/UnixDomainSocketImpl.scala | 140 ++++++++++++++++-----
.../scala/docs/scaladsl/UnixDomainSocketSpec.scala | 48 +++++++
2 files changed, 160 insertions(+), 28 deletions(-)
diff --git
a/unix-domain-socket/src/main/scala/org/apache/pekko/stream/connectors/unixdomainsocket/impl/UnixDomainSocketImpl.scala
b/unix-domain-socket/src/main/scala/org/apache/pekko/stream/connectors/unixdomainsocket/impl/UnixDomainSocketImpl.scala
index 040c8bcd7..3612ed6ee 100644
---
a/unix-domain-socket/src/main/scala/org/apache/pekko/stream/connectors/unixdomainsocket/impl/UnixDomainSocketImpl.scala
+++
b/unix-domain-socket/src/main/scala/org/apache/pekko/stream/connectors/unixdomainsocket/impl/UnixDomainSocketImpl.scala
@@ -15,7 +15,16 @@ package org.apache.pekko.stream.connectors.unixdomainsocket
package impl
import org.apache.pekko
-import pekko.actor.{ Cancellable, CoordinatedShutdown, ExtendedActorSystem,
Extension }
+import pekko.actor.{
+ Actor,
+ ActorRef,
+ Cancellable,
+ CoordinatedShutdown,
+ ExtendedActorSystem,
+ Extension,
+ PoisonPill,
+ Props
+}
import pekko.annotation.InternalApi
import pekko.event.{ Logging, LoggingAdapter }
import pekko.stream._
@@ -24,7 +33,7 @@ import
pekko.stream.connectors.unixdomainsocket.scaladsl.UnixDomainSocket.{
OutgoingConnection,
ServerBinding
}
-import pekko.stream.scaladsl.{ Flow, Keep, Sink, Source,
SourceQueueWithComplete }
+import pekko.stream.scaladsl.{ Flow, Keep, Sink, Source }
import pekko.util.ByteString
import pekko.{ Done, NotUsed }
import jnr.enxio.channels.NativeSelectorProvider
@@ -34,6 +43,7 @@ import java.io.IOException
import java.nio.ByteBuffer
import java.nio.channels.{ SelectionKey, Selector }
import java.nio.file.{ Files, Path, Paths }
+import java.util.concurrent.atomic.{ AtomicLong, AtomicReference }
import scala.concurrent.duration.{ Duration, FiniteDuration }
import scala.concurrent.{ ExecutionContext, Future, Promise }
import scala.util.control.NonFatal
@@ -46,15 +56,83 @@ import scala.util.{ Failure, Success, Try }
private[unixdomainsocket] object UnixDomainSocketImpl {
private sealed abstract class ReceiveContext(
- val queue: SourceQueueWithComplete[ByteString],
+ val queue: ReceiveQueue,
val buffer: ByteBuffer)
private case class ReceiveAvailable(
- override val queue: SourceQueueWithComplete[ByteString],
+ override val queue: ReceiveQueue,
override val buffer: ByteBuffer) extends ReceiveContext(queue, buffer)
private case class PendingReceiveAck(
- override val queue: SourceQueueWithComplete[ByteString],
+ override val queue: ReceiveQueue,
override val buffer: ByteBuffer,
- pendingResult: Future[QueueOfferResult]) extends ReceiveContext(queue,
buffer)
+ pendingResult: Future[Done]) extends ReceiveContext(queue, buffer)
+
+ private case object ReceiveAck
+ private case object ReceiveComplete
+
+ private val receiveAckActorCounter = new AtomicLong()
+
+ /**
+ * Receives the acknowledgements of [[Source.actorRefWithBackpressure]] on
behalf of the io thread. The
+ * acknowledgement is only sent once the read-side stream has taken the
element, so completing `pendingAck`
+ * is what tells the io thread that it may read from the socket again.
+ */
+ private final class ReceiveAckActor(pendingAck:
AtomicReference[Promise[Done]], sel: Selector) extends Actor {
+ override def receive: Receive = {
+ case _ =>
+ val ack = pendingAck.getAndSet(null)
+ if (ack ne null) ack.trySuccess(Done)
+ sel.wakeup()
+ }
+ }
+
+ /**
+ * Hands bytes read from the socket to the read-side stream, one element at
a time. `offer` must not be
+ * called again until the returned future has completed - the io thread
enforces this by clearing
+ * `OP_READ` until then.
+ */
+ private final class ReceiveQueue(
+ ref: ActorRef,
+ ackReceiver: ActorRef,
+ pendingAck: AtomicReference[Promise[Done]],
+ val completion: Future[Done]) {
+
+ def offer(bytes: ByteString): Future[Done] = {
+ val ack = Promise[Done]()
+ pendingAck.set(ack)
+ ref.tell(bytes, ackReceiver)
+ ack.future
+ }
+
+ def complete(): Unit = ref.tell(ReceiveComplete, ackReceiver)
+ }
+
+ private def receiveStructures(sel: Selector, system: ExtendedActorSystem)(
+ implicit mat: Materializer,
+ ec: ExecutionContext): (ReceiveQueue, Source[ByteString, NotUsed]) = {
+ val pendingAck = new AtomicReference[Promise[Done]]()
+ val ackReceiver = system.systemActorOf(
+ Props(new ReceiveAckActor(pendingAck, sel)),
+
s"unix-domain-socket-receive-ack-${receiveAckActorCounter.incrementAndGet()}")
+
+ val ((ref, completion), receiveSource) =
+ Source
+ .actorRefWithBackpressure[ByteString](
+ ReceiveAck,
+ { case ReceiveComplete => CompletionStrategy.draining },
+ PartialFunction.empty)
+ .watchTermination(Keep.both)
+ .preMaterialize()
+
+ completion.onComplete { _ =>
+ // no further acknowledgement can arrive, so release the io thread
rather than leaving OP_READ cleared
+ val outstanding = pendingAck.getAndSet(null)
+ if (outstanding ne null) outstanding.tryFailure(new
IOException("Read-side stream terminated"))
+ ackReceiver ! PoisonPill
+ sel.wakeup()
+ }
+
+ (new ReceiveQueue(ref, ackReceiver, pendingAck, completion), receiveSource)
+ }
private sealed abstract class SendContext(
val buffer: ByteBuffer)
@@ -200,7 +278,7 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
queue.complete()
try {
if (!sendReceiveContext.halfClose ||
sendReceiveContext.isOutputShutdown) {
- queue.watchCompletion().onComplete { _ =>
+ queue.completion.onComplete { _ =>
log.debug("Read-side is shutting down")
key.cancel()
try {
@@ -220,7 +298,7 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
case _: ReceiveAvailable
=>
case PendingReceiveAck(receiveQueue, receiveBuffer,
pendingResult) if pendingResult.isCompleted =>
pendingResult.value.get match {
- case Success(QueueOfferResult.Enqueued) =>
+ case Success(_) =>
key.interestOps(key.interestOps() |
SelectionKey.OP_READ)
sendReceiveContext.receive =
ReceiveAvailable(receiveQueue, receiveBuffer)
case e =>
@@ -245,7 +323,8 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
private def acceptKey(
localAddress: JnrUnixSocketAddress,
- incomingConnectionQueue: SourceQueueWithComplete[IncomingConnection],
+ system: ExtendedActorSystem,
+ incomingConnectionQueue: BoundedSourceQueue[IncomingConnection],
halfClose: Boolean,
receiveBufferSize: Int,
sendBufferSize: Int)(sel: Selector, key: SelectionKey)(implicit mat:
Materializer, ec: ExecutionContext): Unit = {
@@ -258,16 +337,21 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
if (acceptedChannel != null) {
acceptedChannel.configureBlocking(false)
- val (context, connectionFlow) = sendReceiveStructures(sel,
receiveBufferSize, sendBufferSize, halfClose)
+ val (context, connectionFlow) = sendReceiveStructures(sel, system,
receiveBufferSize, sendBufferSize, halfClose)
try {
acceptedChannel.register(sel, SelectionKey.OP_READ, context)
} catch { case _: IOException => }
- incomingConnectionQueue.offer(
+ val queued = incomingConnectionQueue.offer(
IncomingConnection(
localAddress = UnixSocketAddress(Paths.get(localAddress.path())),
remoteAddress = UnixSocketAddress(
Paths.get(Option(acceptingChannel.getRemoteSocketAddress).getOrElse(new
JnrUnixSocketAddress("")).path())),
flow = connectionFlow))
+ if (queued != QueueOfferResult.Enqueued) {
+ // the connection could not be handed to the stream - close it rather
than leaving it dangling
+ try acceptedChannel.close()
+ catch { case _: IOException => }
+ }
}
}
@@ -292,17 +376,16 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
}
}
- private def sendReceiveStructures(sel: Selector, receiveBufferSize: Int,
sendBufferSize: Int, halfClose: Boolean)(
+ private def sendReceiveStructures(
+ sel: Selector,
+ system: ExtendedActorSystem,
+ receiveBufferSize: Int,
+ sendBufferSize: Int,
+ halfClose: Boolean)(
implicit mat: Materializer,
ec: ExecutionContext): (SendReceiveContext, Flow[ByteString, ByteString,
NotUsed]) = {
- val (receiveQueue, receiveSource) =
- Source
- .queue[ByteString](2, OverflowStrategy.backpressure)
- .prefixAndTail(0)
- .map(_._2)
- .toMat(Sink.head)(Keep.both)
- .run()
+ val (receiveQueue, receiveSource) = receiveStructures(sel, system)
val sendReceiveContext =
new SendReceiveContext(
SendAvailable(ByteBuffer.allocate(sendBufferSize)),
@@ -355,7 +438,7 @@ private[unixdomainsocket] object UnixDomainSocketImpl {
}
.to(Sink.ignore))
- (sendReceiveContext, Flow.fromSinkAndSource(sendSink,
Source.futureSource(receiveSource)))
+ (sendReceiveContext, Flow.fromSinkAndSource(sendSink, receiveSource))
}
}
@@ -394,9 +477,10 @@ private[unixdomainsocket] abstract class
UnixDomainSocketImpl(system: ExtendedAc
halfClose: Boolean = false): Source[IncomingConnection,
Future[ServerBinding]] = {
val bind: () => Source[IncomingConnection, Future[ServerBinding]] = { () =>
- val (incomingConnectionQueue, incomingConnectionSource) =
+ val ((incomingConnectionQueue, incomingConnectionTermination),
incomingConnectionSource) =
Source
- .queue[IncomingConnection](2, OverflowStrategy.backpressure)
+ .queue[IncomingConnection](backlog)
+ .watchTermination(Keep.both)
.prefixAndTail(0)
.map {
case (_, source) =>
@@ -425,7 +509,7 @@ private[unixdomainsocket] abstract class
UnixDomainSocketImpl(system: ExtendedAc
val registeredKey =
channel.register(sel,
SelectionKey.OP_ACCEPT,
- acceptKey(address, incomingConnectionQueue, halfClose,
receiveBufferSize, sendBufferSize) _)
+ acceptKey(address, system, incomingConnectionQueue, halfClose,
receiveBufferSize, sendBufferSize) _)
try {
channel.socket().bind(address, backlog)
sel.wakeup()
@@ -433,21 +517,21 @@ private[unixdomainsocket] abstract class
UnixDomainSocketImpl(system: ExtendedAc
ServerBinding(UnixSocketAddress(Paths.get(address.path))) { () =>
registeredKey.cancel()
channel.close()
- incomingConnectionQueue.complete()
- incomingConnectionQueue.watchCompletion().map(_ => ())
+ if (!incomingConnectionQueue.isCompleted)
incomingConnectionQueue.complete()
+ incomingConnectionTermination.map(_ => ())
})
} catch {
case e: IOException =>
val withAddress = new IOException(e.getMessage + s" ($address)", e)
registeredKey.cancel()
channel.close()
- incomingConnectionQueue.fail(withAddress)
+ if (!incomingConnectionQueue.isCompleted)
incomingConnectionQueue.fail(withAddress)
serverBinding.failure(withAddress)
case NonFatal(e) =>
registeredKey.cancel()
channel.close()
- incomingConnectionQueue.fail(e)
+ if (!incomingConnectionQueue.isCompleted)
incomingConnectionQueue.fail(e)
serverBinding.failure(e)
}
@@ -475,7 +559,7 @@ private[unixdomainsocket] abstract class
UnixDomainSocketImpl(system: ExtendedAc
case d: FiniteDuration => Some(system.scheduler.scheduleOnce(d, ()
=> channel.close()))
case _ => None
}
- val (context, connectionFlow) = sendReceiveStructures(sel,
receiveBufferSize, sendBufferSize, halfClose)
+ val (context, connectionFlow) = sendReceiveStructures(sel, system,
receiveBufferSize, sendBufferSize, halfClose)
val ra = new JnrUnixSocketAddress(remoteAddress.path.toFile)
val log = system.log
val registeredKey =
diff --git
a/unix-domain-socket/src/test/scala/docs/scaladsl/UnixDomainSocketSpec.scala
b/unix-domain-socket/src/test/scala/docs/scaladsl/UnixDomainSocketSpec.scala
index 01efc09e5..5435afd3c 100644
--- a/unix-domain-socket/src/test/scala/docs/scaladsl/UnixDomainSocketSpec.scala
+++ b/unix-domain-socket/src/test/scala/docs/scaladsl/UnixDomainSocketSpec.scala
@@ -20,6 +20,8 @@ import pekko.stream.connectors.testkit.scaladsl.LogCapturing
import pekko.stream.connectors.unixdomainsocket.UnixSocketAddress
import pekko.stream.connectors.unixdomainsocket.scaladsl.UnixDomainSocket
import pekko.stream.scaladsl.{ Flow, Keep, Sink, Source }
+import pekko.stream.testkit.TestSubscriber
+import pekko.stream.testkit.scaladsl.TestSink
import pekko.stream.{ Materializer, OverflowStrategy }
import pekko.testkit._
import pekko.util.ByteString
@@ -103,6 +105,52 @@ class UnixDomainSocketSpec
binding.futureValue.unbind().futureValue should be(())
}
+ "not deliver received bytes before the consumer asks for them, and resume
reading afterwards" in {
+ val path = dir.resolve("sock-backpressure")
+
+ val serverProbe = Promise[TestSubscriber.Probe[ByteString]]()
+
+ // Source.maybe keeps the server side of the connection open, the probe
gives us manual demand
+ val binding: Future[UnixDomainSocket.ServerBinding] =
+ UnixDomainSocket()
+ .bind(path)
+ .map { connection =>
+
serverProbe.trySuccess(connection.flow.runWith(Source.maybe[ByteString],
TestSink[ByteString]())._2)
+ ()
+ }
+ .to(Sink.ignore)
+ .run()
+
+ binding.futureValue
+
+ // enough separate writes to outlast the stream buffers between the
socket and the consumer,
+ // so the connector really has to stop reading and resume, but far short
of filling a socket buffer
+ val chunks = (1 to 50).map(i => ByteString(f"chunk-$i%03d-" + "x" *
90)).toList
+ val expected = chunks.reduce(_ ++ _)
+
+ Source(chunks)
+ .throttle(5, 10.millis)
+ .via(UnixDomainSocket().outgoingConnection(path))
+ .runWith(Sink.ignore)
+
+ val server = serverProbe.future.futureValue
+ server.ensureSubscription()
+
+ // the writes have been made, but nothing may be emitted while the
consumer has no demand
+ server.expectNoMessage(300.millis)
+
+ // once demand arrives everything that was written is delivered, in
order and without loss,
+ // which also means reading from the socket resumed as elements were
acknowledged
+ @annotation.tailrec
+ def receiveAtLeast(received: ByteString): ByteString =
+ if (received.size >= expected.size) received else
receiveAtLeast(received ++ server.requestNext())
+
+ receiveAtLeast(ByteString.empty) shouldBe expected
+
+ server.cancel()
+ binding.futureValue.unbind().futureValue should be(())
+ }
+
"allow the client to close the connection" in {
val path = dir.resolve("sock3")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]