From 0bba3b10e6d0fa3f4918f8c64a6e3235aee3e594 Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 9 Jan 2013 14:11:24 +0100 Subject: [PATCH 01/66] create akka-io subproject, see #2884 --- akka-io/src/main/scala/akka/io/IO.scala | 0 project/AkkaBuild.scala | 13 +++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 akka-io/src/main/scala/akka/io/IO.scala diff --git a/akka-io/src/main/scala/akka/io/IO.scala b/akka-io/src/main/scala/akka/io/IO.scala new file mode 100644 index 0000000000..e69de29bb2 diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 6bca2ff7c0..5d00a6fbd6 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -73,7 +73,7 @@ object AkkaBuild extends Build { generatedPdf in Sphinx <<= generatedPdf in Sphinx in LocalProject(docs.id) map identity ), - aggregate = Seq(actor, testkit, actorTests, dataflow, remote, remoteTests, camel, cluster, slf4j, agent, transactor, mailboxes, zeroMQ, kernel, akkaSbtPlugin, osgi, osgiAries, docs, contrib, samples) + aggregate = Seq(actor, testkit, actorTests, dataflow, io, remote, remoteTests, camel, cluster, slf4j, agent, transactor, mailboxes, zeroMQ, kernel, akkaSbtPlugin, osgi, osgiAries, docs, contrib, samples) ) lazy val actor = Project( @@ -100,6 +100,13 @@ object AkkaBuild extends Build { settings = defaultSettings ++ OSGi.dataflow ++ cpsPlugin ) + lazy val io = Project( + id = "akka-io", + base = file("akka-io"), + dependencies = Seq(actor), + settings = defaultSettings ++ OSGi.io + ) + lazy val testkit = Project( id = "akka-testkit", base = file("akka-testkit"), @@ -364,7 +371,7 @@ object AkkaBuild extends Build { lazy val docs = Project( id = "akka-docs", base = file("akka-docs"), - dependencies = Seq(actor, testkit % "test->test", mailboxesCommon % "compile;test->test", + dependencies = Seq(actor, testkit % "test->test", mailboxesCommon % "compile;test->test", io, remote, cluster, slf4j, agent, dataflow, transactor, fileMailbox, zeroMQ, camel, osgi, osgiAries), settings = defaultSettings ++ site.settings ++ site.sphinxSupport() ++ site.publishSite ++ sphinxPreprocessing ++ cpsPlugin ++ Seq( sourceDirectory in Sphinx <<= baseDirectory / "rst", @@ -651,6 +658,8 @@ object AkkaBuild extends Build { val fileMailbox = exports(Seq("akka.actor.mailbox.filebased.*")) + val io = exports(Seq("akka.io.*")) + val mailboxesCommon = exports(Seq("akka.actor.mailbox.*"), imports = Seq(protobufImport())) val osgi = exports(Seq("akka.osgi")) ++ Seq(OsgiKeys.privatePackage := Seq("akka.osgi.impl")) From 9daf0e1bd7112d6cb54c5c72ad8cd4fe91fa011d Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 9 Jan 2013 15:19:36 +0100 Subject: [PATCH 02/66] add testkit as a dependency for akka-io, see #2884 --- akka-io/src/test/scala/akka/io/IOSpec.scala | 7 +++++++ project/AkkaBuild.scala | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 akka-io/src/test/scala/akka/io/IOSpec.scala diff --git a/akka-io/src/test/scala/akka/io/IOSpec.scala b/akka-io/src/test/scala/akka/io/IOSpec.scala new file mode 100644 index 0000000000..54c4610b24 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/IOSpec.scala @@ -0,0 +1,7 @@ +package akka.io + +import akka.testkit.AkkaSpec + +class IOSpec extends AkkaSpec { + +} \ No newline at end of file diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index 5d00a6fbd6..d1b1c66aa8 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -103,7 +103,7 @@ object AkkaBuild extends Build { lazy val io = Project( id = "akka-io", base = file("akka-io"), - dependencies = Seq(actor), + dependencies = Seq(actor, testkit % "test->test"), settings = defaultSettings ++ OSGi.io ) From 4062516c84386f29365d4aada08e78a04ac9fea6 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 9 Jan 2013 17:43:25 +0100 Subject: [PATCH 03/66] Add first version of TCP message protocol --- akka-io/src/main/scala/akka/io/Tcp.scala | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 akka-io/src/main/scala/akka/io/Tcp.scala diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala new file mode 100644 index 0000000000..16c0b4f3b3 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.io + +import java.net.InetSocketAddress +import java.nio.channels.{ ServerSocketChannel, SocketChannel } +import akka.actor.ActorRef +import akka.util.ByteString + +object Tcp { + + /// COMMANDS + sealed trait Command + + case class Connect(remoteAddress: InetSocketAddress, + localAddress: Option[InetSocketAddress] = None) extends Command + case class Bind(handler: ActorRef, address: InetSocketAddress, backlog: Int = 100) extends Command + case class Register(handler: ActorRef) extends Command + + // TODO: what about close reasons? + case object Close extends Command + case object ConfirmedClose extends Command + case object Abort extends Command + + trait Write extends Command { + def data: ByteString + def ack: AnyRef + def nack: AnyRef + } + object Write { + def apply(_data: ByteString): Write = new Write { + def data: ByteString = _data + def ack: AnyRef = null + def nack: AnyRef = null + } + } + case object StopReading extends Command + case object ResumeReading extends Command + + /// EVENTS + sealed trait Event + + case class Received(data: ByteString) extends Event + case class Connected(localAddress: InetSocketAddress, remoteAddress: InetSocketAddress) extends Event + + sealed trait Closed extends Event + case object PeerClosed extends Closed + case object ActivelyClosed extends Closed + case object ConfirmedClosed extends Closed + case class Error(cause: Throwable) extends Closed + + /// INTERNAL + case class RegisterClientChannel(channel: SocketChannel) + case class RegisterServerChannel(channel: ServerSocketChannel) + case class CreateConnection(channel: SocketChannel) + case object ChannelConnectable + case object ChannelAcceptable + case object ChannelReadable + case object ChannelWritable + case object AcceptInterest + case object ReadInterest + case object WriteInterest +} From f47f973f3c2add86ae096b7ea8ddc273218fc996 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 10 Jan 2013 11:05:09 +0100 Subject: [PATCH 04/66] add design docs as background info for IO layer, see #2890 --- akka-docs/rst/dev/index.rst | 1 + akka-docs/rst/dev/io-layer.rst | 107 +++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 akka-docs/rst/dev/io-layer.rst diff --git a/akka-docs/rst/dev/index.rst b/akka-docs/rst/dev/index.rst index 17fcb2427c..d812518288 100644 --- a/akka-docs/rst/dev/index.rst +++ b/akka-docs/rst/dev/index.rst @@ -6,6 +6,7 @@ Information for Developers building-akka multi-jvm-testing + io-layer developer-guidelines documentation team diff --git a/akka-docs/rst/dev/io-layer.rst b/akka-docs/rst/dev/io-layer.rst new file mode 100644 index 0000000000..6843b1cd6a --- /dev/null +++ b/akka-docs/rst/dev/io-layer.rst @@ -0,0 +1,107 @@ +.. _io-layer: + +####################### +Design of the I/O Layer +####################### + +The ``akka.io`` package has been developed in collaboration between the Akka +team and Mathias Doenitz & Johannes Rudolph from the `Spray framework`_. It has +been influenced by the experiences with the ``spray-io`` module and adapted for +more general consumption as an actor-based service. + +The Underlying Requirements +=========================== + +In order to be suitable as the basic IO layer for Spray’s HTTP handling as well +as for Akka remoting, the following requirements were driving the design: + +* scalability to millions of concurrent connections + +* lowest possible latency in getting data from the input channel into the + target actor’s mailbox + +* maximize throughput at the same time + +* optional back-pressure in both directions (i.e. throttling local senders as + well as allowing local readers to throttle remote senders where the protocol + allows this) + +* a purely actor-based API with immutable representation of data + +* extensibility for integrating new transports by way of a very lean SPI; the + goal is to not force I/O mechanisms into a lowest common denominator but + instead allow completely protocol-specific user-level APIs. + +The Basic Principle +=================== + +Each transport implementation will be a separate Akka extension, offering an +:class:`ActorRef` representing the main point of entry for client code: this +manager accepts requests for establishing a communications channel (e.g. +connect or listen on a TCP socket). Each communications channel is represented +as one actor which is exposed to the client code for all interaction with this +channel. + +The core piece of the implementation is the transport-specific “selector” actor; +in the example of TCP this would wrap a :class:`java.nio.channels.Selector`. +The channel actors register their interest in readability or writability of the +underlying channel by sending corresponding messages to their assigned selector +actor. An important point for achieving low latency is to hand off the actual +reading and writing to the channel actor, so that the selector actor’s only +responsibility is the management of the underlying selector’s key set and the +actual select operation (which is typically blocking). + +The assignment of channels to selectors is done for the lifetime of a channel +by the manager actor; the natural choice is to have the manager supervise the +selectors, which in turn supervise their channels. In order to allow the +manager to make informed decisions, the selectors keep the manager updated +about their fill level by sending a message every time a channel is terminated. + +Back-pressure for output is enabled by allowing the writer to specify within +the :class:`Write` messages whether it wants to receive an acknowledgement for +enqueuing that write to the O/S kernel. Back-pressure for input is propagated +by back sending a message to the channel actor which will take the underlying +channel out of the selector until a corresponding resume command is received. +In the case of transports with flow control—like TCP—the act of not consuming +data from the stream at the receiving end is propagated back to the sender, +linking these two mechanisms across the network. + +Benefits Resulting from this Design +=================================== + +Staying within the actor model for the whole implementation allows us to remove +the need for explicit thread handling logic, and it also means that there are +no locks involved (besides those which are part of the underlying transport +library). Writing only actor code results in a cleaner implementation, +while Akka’s efficient actor messaging does not impose a high tax for this +benefit. In fact the event-based nature of I/O maps so well to the actor model +that we expect clear performance and especially scalability benefits over +traditional solutions with explicit thread management and synchronization. + +Another benefit of supervision hierarchies is that clean-up of resources comes +naturally: shutting down a selector actor will automatically clean up all +channel actors, allowing proper closing of the channels and sending the +appropriate messages to user-level client actors. DeathWatch allow the channel +actors to notice the demise of their user-level handler actors and terminate in +an orderly fashion in that case as well; this naturally reduces the chances of +leaking open channels. + +The choice of using :class:`ActorRef` for exposing all functionality entails +that these references can be distributed or delegated freely and in general +handled as the user sees fit, including the use of remoting and life-cycle +monitoring (just to name two). + +How to go about Adding a New Transport +====================================== + +The best start is to study the TCP reference implementation to get a good grip +on the basic working principle and then design an implementation which is +similar in spirit, but adapted to the new protocol in question. There are vast +differences between I/O mechanisms (e.g. compare file I/O to a message broker) +and the goal of this I/O layer is explicitly **not** to shoehorn all of them +into a uniform API, which is why only the basic working principle is documented +here. + + +.. _Spray framework: http://spray.io + From 52e393c6c86b63c409a1c44c094e75c0c670d530 Mon Sep 17 00:00:00 2001 From: Roland Date: Thu, 10 Jan 2013 15:45:37 +0100 Subject: [PATCH 05/66] add Tcp IO extension and first draft of settings/options, see #2888 --- akka-io/src/main/resources/reference.conf | 61 ++++++++ akka-io/src/main/scala/akka/io/IO.scala | 17 +++ akka-io/src/main/scala/akka/io/Tcp.scala | 148 +++++++++++++++++++- akka-io/src/test/scala/akka/io/IOSpec.scala | 4 + 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 akka-io/src/main/resources/reference.conf diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf new file mode 100644 index 0000000000..1b502d4c4f --- /dev/null +++ b/akka-io/src/main/resources/reference.conf @@ -0,0 +1,61 @@ +################################# +# Akka IO Reference Config File # +################################# + +# This is the reference config file that contains all the default settings. +# Make your edits/overrides in your application.conf. + +akka { + + io { + + # By default the select loops run on dedicated threads, hence using a + # PinnedDispatcher + pinned-dispatcher { + type = "PinnedDispatcher" + executor = "thread-pool-executor" + thread-pool-executor.allow-core-pool-timeout = off + } + + tcp { + + # The number of selectors to stripe the served channels over; each of + # these will use one select loop on the selector-dispatcher. + nr-of-selectors = 1 + + # Maximum number of open channels supported by this TCP module; there is + # no intrinsic general limit, this setting is meant to enable DoS + # protection by limiting the number of concurrently connected clients. + # Set to 0 to disable. + max-channels = 256000 + + # The select loop can be used in two modes: + # - setting "infinite" will select without a timeout, hogging a thread + # - setting a positive timeout will do a bounded select call, + # enabling sharing of a single thread between multiple selectors + # (in this case you will have to use a different configuration for the + # selector-dispatcher, e.g. using "type=Dispatcher" with size 1) + # - setting it to zero means polling, i.e. calling selectNow() + select-timeout = infinite + + # When trying to create a new connection but the chosen selector is at + # full capacity, retry this many times with different selectors before + # giving up + selector-association-retries = 10 + + # Fully qualified config path which holds the dispatcher configuration + # to be used for running the select() calls in the selectors + selector-dispatcher = "akka.io.pinned-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the read/write worker actors + worker-dispatcher = "akka.actor.default-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the selector management actors + management-dispatcher = "akka.actor.default-dispatcher" + } + + } + +} \ No newline at end of file diff --git a/akka-io/src/main/scala/akka/io/IO.scala b/akka-io/src/main/scala/akka/io/IO.scala index e69de29bb2..e787f1295b 100644 --- a/akka-io/src/main/scala/akka/io/IO.scala +++ b/akka-io/src/main/scala/akka/io/IO.scala @@ -0,0 +1,17 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import akka.actor.{ ActorRef, ActorSystem, ExtensionKey } + +object IO { + + trait Extension extends akka.actor.Extension { + def manager: ActorRef + } + + def apply[T <: Extension](key: ExtensionKey[T])(implicit system: ActorSystem): ActorRef = key(system).manager + +} \ No newline at end of file diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 16c0b4f3b3..91caabaeb7 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -1,5 +1,5 @@ /** - * Copyright (C) 2009-2012 Typesafe Inc. + * Copyright (C) 2009-2013 Typesafe Inc. */ package akka.io @@ -8,17 +8,136 @@ import java.net.InetSocketAddress import java.nio.channels.{ ServerSocketChannel, SocketChannel } import akka.actor.ActorRef import akka.util.ByteString +import akka.actor.ExtensionKey +import akka.actor.ExtendedActorSystem +import akka.actor.ActorSystemImpl +import akka.actor.Props +import java.net.Socket +import java.net.ServerSocket +import scala.concurrent.duration._ +import scala.collection.immutable +import sun.security.tools.KeyTool.Command +import akka.actor.ActorSystem -object Tcp { +object Tcp extends ExtensionKey[TcpExt] { + + // Java API + override def get(system: ActorSystem): TcpExt = system.extension(this) /// COMMANDS sealed trait Command case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None) extends Command - case class Bind(handler: ActorRef, address: InetSocketAddress, backlog: Int = 100) extends Command + case class Bind(handler: ActorRef, + address: InetSocketAddress, + backlog: Int = 100, + options: immutable.Seq[SO.SocketOption] = Nil) extends Command case class Register(handler: ActorRef) extends Command + object SO { + /** + * SocketOption is a package of data (from the user) and associated + * behavior (how to apply that to a socket). + */ + sealed trait SocketOption { + /** + * Action to be taken for this option before calling bind() + */ + def beforeBind(s: ServerSocket): Unit = () + /** + * Action to be taken for this option before calling connect() + */ + def beforeConnect(s: Socket): Unit = () + /** + * Action to be taken for this option after connect returned (i.e. on + * the slave socket for servers). + */ + def afterConnect(s: Socket): Unit = () + } + + // shared socket options + + /** + * [[akka.io.Tcp.SO.SocketOption]] to set the SO_RCVBUF option + * + * For more information see [[java.net.Socket.setReceiveBufferSize]] + */ + case class ReceiveBufferSize(size: Int) extends SocketOption { + require(size > 0, "ReceiveBufferSize must be > 0") + override def beforeBind(s: ServerSocket): Unit = s.setReceiveBufferSize(size) + override def beforeConnect(s: Socket): Unit = s.setReceiveBufferSize(size) + } + + // server socket options + + /** + * [[akka.io.Tcp.SO.SocketOption]] to enable or disable SO_REUSEADDR + * + * For more information see [[java.net.Socket.setReuseAddress]] + */ + case class ReuseAddress(on: Boolean) extends SocketOption { + override def beforeBind(s: ServerSocket): Unit = s.setReuseAddress(on) + override def beforeConnect(s: Socket): Unit = s.setReuseAddress(on) + } + + // general socket options + + /** + * [[akka.io.Tcp.SO.SocketOption]] to enable or disable SO_KEEPALIVE + * + * For more information see [[java.net.Socket.setKeepAlive]] + */ + case class KeepAlive(on: Boolean) extends SocketOption { + override def afterConnect(s: Socket): Unit = s.setKeepAlive(on) + } + + /** + * [[akka.io.Tcp.SO.SocketOption]] to enable or disable OOBINLINE (receipt + * of TCP urgent data) By default, this option is disabled and TCP urgent + * data is silently discarded. + * + * For more information see [[java.net.Socket.setOOBInline]] + */ + case class OOBInline(on: Boolean) extends SocketOption { + override def afterConnect(s: Socket): Unit = s.setOOBInline(on) + } + + /** + * [[akka.io.Tcp.SO.SocketOption]] to set the SO_SNDBUF option. + * + * For more information see [[java.net.Socket.setSendBufferSize]] + */ + case class SendBufferSize(size: Int) extends SocketOption { + require(size > 0, "ReceiveBufferSize must be > 0") + override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) + } + + // SO_LINGER is handled by the Close code + + /** + * [[akka.io.Tcp.SO.SocketOption]] to enable or disable TCP_NODELAY + * (disable or enable Nagle's algorithm) + * + * For more information see [[java.net.Socket.setTcpNoDelay]] + */ + case class TcpNoDelay(on: Boolean) extends SocketOption { + override def afterConnect(s: Socket): Unit = s.setTcpNoDelay(on) + } + + /** + * [[akka.io.Tcp.SO.SocketOption]] to set the traffic class or + * type-of-service octet in the IP header for packets sent from this + * socket. + * + * For more information see [[java.net.Socket.setTrafficClass]] + */ + case class TrafficClass(tc: Int) extends SocketOption { + require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]") + override def afterConnect(s: Socket): Unit = s.setTrafficClass(tc) + } + } + // TODO: what about close reasons? case object Close extends Command case object ConfirmedClose extends Command @@ -63,3 +182,26 @@ object Tcp { case object ReadInterest case object WriteInterest } + +class TcpExt(system: ExtendedActorSystem) extends IO.Extension { + + object Settings { + val config = system.settings.config.getConfig("akka.io.tcp") + import config._ + + val NrOfSelectors = getInt("nr-of-selectors") + val MaxChannels = getInt("max-channels") + val MaxChannelsPerSelector = MaxChannels / NrOfSelectors + val SelectTimeout = + if (getString("select-timeout") == "infinite") Duration.Inf + else Duration(getMilliseconds("select-timeout"), MILLISECONDS) + val SelectorAssociationRetries = getInt("selector-association-retries") + val SelectorDispatcher = getString("selector-dispatcher") + val WorkerDispatcher = getString("worker-dispatcher") + val ManagementDispatcher = getString("management-dispatcher") + } + + val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( + Props.empty.withDispatcher(Settings.ManagementDispatcher), "IO-TCP") + +} \ No newline at end of file diff --git a/akka-io/src/test/scala/akka/io/IOSpec.scala b/akka-io/src/test/scala/akka/io/IOSpec.scala index 54c4610b24..553aef8f4a 100644 --- a/akka-io/src/test/scala/akka/io/IOSpec.scala +++ b/akka-io/src/test/scala/akka/io/IOSpec.scala @@ -1,3 +1,7 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + package akka.io import akka.testkit.AkkaSpec From 091ac35e7bd9a32efb9ca96d975f8378b64481a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Thu, 10 Jan 2013 12:13:14 +0100 Subject: [PATCH 06/66] Created top level manager for TCP based IO #2887 --- akka-io/src/main/scala/akka/io/Tcp.scala | 13 ++++- .../src/main/scala/akka/io/TcpManager.scala | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 akka-io/src/main/scala/akka/io/TcpManager.scala diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 91caabaeb7..cd9859c2d1 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -16,7 +16,6 @@ import java.net.Socket import java.net.ServerSocket import scala.concurrent.duration._ import scala.collection.immutable -import sun.security.tools.KeyTool.Command import akka.actor.ActorSystem object Tcp extends ExtensionKey[TcpExt] { @@ -33,6 +32,7 @@ object Tcp extends ExtensionKey[TcpExt] { address: InetSocketAddress, backlog: Int = 100, options: immutable.Seq[SO.SocketOption] = Nil) extends Command + case object Unbind extends Command case class Register(handler: ActorRef) extends Command object SO { @@ -161,8 +161,11 @@ object Tcp extends ExtensionKey[TcpExt] { /// EVENTS sealed trait Event + case object Bound extends Event + case class Received(data: ByteString) extends Event case class Connected(localAddress: InetSocketAddress, remoteAddress: InetSocketAddress) extends Event + case class CommandFailed(cmd: Command) extends Event sealed trait Closed extends Event case object PeerClosed extends Closed @@ -174,6 +177,12 @@ object Tcp extends ExtensionKey[TcpExt] { case class RegisterClientChannel(channel: SocketChannel) case class RegisterServerChannel(channel: ServerSocketChannel) case class CreateConnection(channel: SocketChannel) + case class Reject(command: Command, commander: ActorRef) + // Retry should be sent by Selector actors to their parent router with retriesLeft decremented. If retries are + // depleted, the selector actor must reply directly to the manager with a Reject (above). + case class Retry(command: Command, retriesLeft: Int, commander: ActorRef) { + require(retriesLeft >= 0, "The upper limit for retries must be nonnegative.") + } case object ChannelConnectable case object ChannelAcceptable case object ChannelReadable @@ -202,6 +211,6 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { } val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( - Props.empty.withDispatcher(Settings.ManagementDispatcher), "IO-TCP") + Props[TcpManager].withDispatcher(Settings.ManagementDispatcher), "IO-TCP") } \ No newline at end of file diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-io/src/main/scala/akka/io/TcpManager.scala new file mode 100644 index 0000000000..344a861580 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpManager.scala @@ -0,0 +1,53 @@ +package akka.io + +import akka.actor.{ OneForOneStrategy, Actor, Props } +import akka.io.Tcp._ +import akka.routing.RandomRouter +import akka.actor.SupervisorStrategy.Restart + +/** + * TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections. + * + * TcpManager is obtainable by calling {{{ IO(TCP) }}} (see [[akka.io.IO]] and [[akka.io.Tcp]]) + * + * == Bind == + * + * To bind and listen to a local address, a [[akka.io.Tcp.Bind]] command must be sent to this actor. If the binding + * was successful, the sender of the [[akka.io.Tcp.Bind]] will be notified with a [[akka.io.Tcp.Bound]] + * message. The sender of the [[akka.io.Tcp.Bound]] message is the Listener actor (an internal actor responsible for + * listening to server events). To unbind the port an [[akka.io.Tcp.Unbind]] message must be sent to the Listener actor. + * + * If the bind request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified + * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. + * + * When an inbound TCP connection is established, the handler will be notified by a [[akka.io.Tcp.Connected]] message. + * The sender of this message is the Connection actor (an internal actor representing the TCP connection). At this point + * the procedure is the same as for outbound connections (see section below). + * + * == Connect == + * + * To initiate a connection to a remote server, a [[akka.io.Tcp.Connect]] message must be sent to this actor. If the + * connection succeeds, the sender will be notified with a [[akka.io.Tcp.Connected]] message. The sender of the + * [[akka.io.Tcp.Connected]] message is the Connection actor (an internal actor representing the TCP connection). Before + * starting to use the connection, a handler should be registered to the Connection actor by sending a [[akka.io.Tcp.Register]] + * message. After a handler has been registered, all incoming data will be sent to the handler in the form of + * [[akka.io.Tcp.Received]] messages. To write data to the connection, a [[akka.io.Tcp.Write]] message should be sent + * to the Connection actor. + * + * If the connect request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified + * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. + * + */ +class TcpManager extends Actor { + val settings = Tcp(context.system).Settings + + val selectorPool = context.actorOf(Props.empty.withRouter(RandomRouter(settings.NrOfSelectors))) + + def receive = { + case c: Connect ⇒ selectorPool forward c + case b: Bind ⇒ selectorPool forward b + case Reject(command, commander) ⇒ commander ! CommandFailed(command) + } +} From ab228c09419f875adbf4c335e888700b1bb18f5a Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 11 Jan 2013 10:03:39 +0100 Subject: [PATCH 07/66] update/improve design docs as background info for IO layer, see #2890 --- akka-docs/rst/dev/io-layer.rst | 111 ++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/akka-docs/rst/dev/io-layer.rst b/akka-docs/rst/dev/io-layer.rst index 6843b1cd6a..9fc955b73a 100644 --- a/akka-docs/rst/dev/io-layer.rst +++ b/akka-docs/rst/dev/io-layer.rst @@ -1,73 +1,83 @@ .. _io-layer: -####################### -Design of the I/O Layer -####################### +################ +I/O Layer Design +################ The ``akka.io`` package has been developed in collaboration between the Akka -team and Mathias Doenitz & Johannes Rudolph from the `Spray framework`_. It has -been influenced by the experiences with the ``spray-io`` module and adapted for +and `spray.io`_ teams. Its design incorporates the experiences with the +``spray-io`` module along with improvements that were jointly developed for more general consumption as an actor-based service. -The Underlying Requirements -=========================== +Requirements +============ -In order to be suitable as the basic IO layer for Spray’s HTTP handling as well -as for Akka remoting, the following requirements were driving the design: +In order to form a general and extensible IO layer basis for a wide range of +applications, with Akka remoting and spray HTTP being the initial ones, the +following requirements were established as key drivers for the design: * scalability to millions of concurrent connections -* lowest possible latency in getting data from the input channel into the +* lowest possible latency in getting data from an input channel into the target actor’s mailbox -* maximize throughput at the same time +* maximal throughput * optional back-pressure in both directions (i.e. throttling local senders as - well as allowing local readers to throttle remote senders where the protocol - allows this) + well as allowing local readers to throttle remote senders, where allowed by + the protocol) -* a purely actor-based API with immutable representation of data +* a purely actor-based API with immutable data representation * extensibility for integrating new transports by way of a very lean SPI; the goal is to not force I/O mechanisms into a lowest common denominator but instead allow completely protocol-specific user-level APIs. -The Basic Principle -=================== +Basic Architecture +================== -Each transport implementation will be a separate Akka extension, offering an -:class:`ActorRef` representing the main point of entry for client code: this -manager accepts requests for establishing a communications channel (e.g. -connect or listen on a TCP socket). Each communications channel is represented -as one actor which is exposed to the client code for all interaction with this -channel. +Each transport implementation will be made available as a separate Akka +extension, offering an :class:`ActorRef` representing the initial point of +contact for client code. This "manager" accepts requests for establishing a +communications channel (e.g. connect or listen on a TCP socket). Each +communications channel is represented by one dedicated actor, which is exposed +to client code for all interaction with this channel over its entire lifetime. -The core piece of the implementation is the transport-specific “selector” actor; -in the example of TCP this would wrap a :class:`java.nio.channels.Selector`. -The channel actors register their interest in readability or writability of the -underlying channel by sending corresponding messages to their assigned selector -actor. An important point for achieving low latency is to hand off the actual -reading and writing to the channel actor, so that the selector actor’s only -responsibility is the management of the underlying selector’s key set and the -actual select operation (which is typically blocking). +The central element of the implementation is the transport-specific “selector” +actor; in the case of TCP this would wrap a :class:`java.nio.channels.Selector`. +The channel actors register their interest in readability or writability of +their channel by sending corresponding messages to their assigned selector +actor. However, the actual channel reading and writing is performed by the +channel actors themselves, which frees the selector actors from time-consuming +tasks and thereby ensures low latency. The selector actor's only responsibility +is the management of the underlying selector's key set and the actual select +operation, which is the only operation to typically block. -The assignment of channels to selectors is done for the lifetime of a channel -by the manager actor; the natural choice is to have the manager supervise the -selectors, which in turn supervise their channels. In order to allow the -manager to make informed decisions, the selectors keep the manager updated -about their fill level by sending a message every time a channel is terminated. +The assignment of channels to selectors is performed by the manager actor and +remains unchanged for the entire lifetime of a channel. Thereby the management +actor "stripes" new channels across one or more selector actors based on some +implementation-specific distribution logic. This logic may be delegated (in +part) to the selectors actors, which could, for example, choose to reject the +assignment of a new channel when they consider themselves to be at capacity. -Back-pressure for output is enabled by allowing the writer to specify within -the :class:`Write` messages whether it wants to receive an acknowledgement for -enqueuing that write to the O/S kernel. Back-pressure for input is propagated -by back sending a message to the channel actor which will take the underlying -channel out of the selector until a corresponding resume command is received. -In the case of transports with flow control—like TCP—the act of not consuming -data from the stream at the receiving end is propagated back to the sender, -linking these two mechanisms across the network. +The manager actor creates (and therefore supervises) the selector actors, which +in turn create and supervise their channel actors. The actor hierarchy of one +single transport implementation therefore consists of three distinct actor +levels, with the management actor at the top-, the channel actors at the leaf- +and the selector actors at the mid-level. -Benefits Resulting from this Design -=================================== +Back-pressure for output is enabled by allowing the user to specify within its +:class:`Write` messages whether it wants to receive an acknowledgement for +enqueuing that write to the O/S kernel. Back-pressure for input is enabled by +sending the channel actor a message which temporarily disables read interest +for the channel until reading is re-enabled with a corresponding resume command. +In the case of transports with flow control—like TCP—the act of not +consuming data at the receiving end (thereby causing them to remain in the +kernels read buffers) is propagated back to the sender, linking these two +mechanisms across the network. + +Design Benefits +=============== Staying within the actor model for the whole implementation allows us to remove the need for explicit thread handling logic, and it also means that there are @@ -81,7 +91,7 @@ traditional solutions with explicit thread management and synchronization. Another benefit of supervision hierarchies is that clean-up of resources comes naturally: shutting down a selector actor will automatically clean up all channel actors, allowing proper closing of the channels and sending the -appropriate messages to user-level client actors. DeathWatch allow the channel +appropriate messages to user-level client actors. DeathWatch allows the channel actors to notice the demise of their user-level handler actors and terminate in an orderly fashion in that case as well; this naturally reduces the chances of leaking open channels. @@ -95,13 +105,12 @@ How to go about Adding a New Transport ====================================== The best start is to study the TCP reference implementation to get a good grip -on the basic working principle and then design an implementation which is +on the basic working principle and then design an implementation, which is similar in spirit, but adapted to the new protocol in question. There are vast differences between I/O mechanisms (e.g. compare file I/O to a message broker) and the goal of this I/O layer is explicitly **not** to shoehorn all of them -into a uniform API, which is why only the basic working principle is documented -here. +into a uniform API, which is why only the basic architecture ideas are +documented here. - -.. _Spray framework: http://spray.io +.. _spray.io: http://spray.io From be9abae1e32fda97fa15713ebf83f7b2d3c5f25e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 15 Jan 2013 18:08:45 +0100 Subject: [PATCH 08/66] tcp connection actors, see #2886 --- akka-io/src/main/resources/reference.conf | 11 +- akka-io/src/main/scala/akka/io/Tcp.scala | 87 ++-- .../main/scala/akka/io/TcpConnection.scala | 225 ++++++++++ .../scala/akka/io/TcpIncomingConnection.scala | 26 ++ .../src/main/scala/akka/io/TcpManager.scala | 4 +- .../scala/akka/io/TcpOutgoingConnection.scala | 51 +++ .../akka/io/ThreadLocalDirectBuffer.scala | 32 ++ .../scala/akka/io/TcpConnectionSpec.scala | 414 ++++++++++++++++++ 8 files changed, 804 insertions(+), 46 deletions(-) create mode 100644 akka-io/src/main/scala/akka/io/TcpConnection.scala create mode 100644 akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala create mode 100644 akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala create mode 100644 akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala create mode 100644 akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 1b502d4c4f..ebcd1fa2e9 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -54,8 +54,17 @@ akka { # Fully qualified config path which holds the dispatcher configuration # for the selector management actors management-dispatcher = "akka.actor.default-dispatcher" + + # The size of the thread-local direct buffers used to read or write + # network data from the kernel. Those buffer directly add to the footprint + # of the threads from the dispatcher tcp connection actors are using. + direct-buffer-size = 524288 + + # The duration a connection actor waits for a `Register` message from + # its commander before aborting the connection. + register-timeout = 5s } } -} \ No newline at end of file +} diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index cd9859c2d1..9d7e754b98 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -31,31 +31,30 @@ object Tcp extends ExtensionKey[TcpExt] { case class Bind(handler: ActorRef, address: InetSocketAddress, backlog: Int = 100, - options: immutable.Seq[SO.SocketOption] = Nil) extends Command + options: immutable.Seq[SocketOption] = Nil) extends Command case object Unbind extends Command case class Register(handler: ActorRef) extends Command - object SO { + /** + * SocketOption is a package of data (from the user) and associated + * behavior (how to apply that to a socket). + */ + sealed trait SocketOption { /** - * SocketOption is a package of data (from the user) and associated - * behavior (how to apply that to a socket). + * Action to be taken for this option before calling bind() */ - sealed trait SocketOption { - /** - * Action to be taken for this option before calling bind() - */ - def beforeBind(s: ServerSocket): Unit = () - /** - * Action to be taken for this option before calling connect() - */ - def beforeConnect(s: Socket): Unit = () - /** - * Action to be taken for this option after connect returned (i.e. on - * the slave socket for servers). - */ - def afterConnect(s: Socket): Unit = () - } - + def beforeBind(s: ServerSocket): Unit = () + /** + * Action to be taken for this option before calling connect() + */ + def beforeConnect(s: Socket): Unit = () + /** + * Action to be taken for this option after connect returned (i.e. on + * the slave socket for servers). + */ + def afterConnect(s: Socket): Unit = () + } + object SO { // shared socket options /** @@ -109,7 +108,7 @@ object Tcp extends ExtensionKey[TcpExt] { * For more information see [[java.net.Socket.setSendBufferSize]] */ case class SendBufferSize(size: Int) extends SocketOption { - require(size > 0, "ReceiveBufferSize must be > 0") + require(size > 0, "SendBufferSize must be > 0") override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) } @@ -139,39 +138,37 @@ object Tcp extends ExtensionKey[TcpExt] { } // TODO: what about close reasons? - case object Close extends Command - case object ConfirmedClose extends Command - case object Abort extends Command + sealed trait CloseCommand extends Command - trait Write extends Command { - def data: ByteString - def ack: AnyRef - def nack: AnyRef - } + case object Close extends CloseCommand + case object ConfirmedClose extends CloseCommand + case object Abort extends CloseCommand + + case class Write(data: ByteString, ack: AnyRef) extends Command object Write { - def apply(_data: ByteString): Write = new Write { - def data: ByteString = _data - def ack: AnyRef = null - def nack: AnyRef = null - } + val Empty: Write = Write(ByteString.empty, null) + def apply(data: ByteString): Write = + if (data.isEmpty) Empty else Write(data, null) } + case object StopReading extends Command case object ResumeReading extends Command /// EVENTS sealed trait Event - case object Bound extends Event - case class Received(data: ByteString) extends Event - case class Connected(localAddress: InetSocketAddress, remoteAddress: InetSocketAddress) extends Event + case class Connected(remoteAddress: InetSocketAddress, localAddress: InetSocketAddress) extends Event case class CommandFailed(cmd: Command) extends Event + case object Bound extends Event + case object Unbound extends Event - sealed trait Closed extends Event - case object PeerClosed extends Closed - case object ActivelyClosed extends Closed - case object ConfirmedClosed extends Closed - case class Error(cause: Throwable) extends Closed + sealed trait ConnectionClosed extends Event + case object Closed extends ConnectionClosed + case object Aborted extends ConnectionClosed + case object ConfirmedClosed extends ConnectionClosed + case object PeerClosed extends ConnectionClosed + case class ErrorClose(cause: Throwable) extends ConnectionClosed /// INTERNAL case class RegisterClientChannel(channel: SocketChannel) @@ -208,9 +205,13 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val SelectorDispatcher = getString("selector-dispatcher") val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") + val DirectBufferSize = getInt("direct-buffer-size") + val RegisterTimeout = + if (getString("register-timeout") == "infinite") Duration.Undefined + else Duration(getMilliseconds("register-timeout"), MILLISECONDS) } val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( Props[TcpManager].withDispatcher(Settings.ManagementDispatcher), "IO-TCP") -} \ No newline at end of file +} diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala new file mode 100644 index 0000000000..707e1664de --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -0,0 +1,225 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import java.net.InetSocketAddress +import java.io.IOException +import java.nio.channels.SocketChannel +import scala.util.control.NonFatal +import scala.collection.immutable +import scala.concurrent.duration._ +import akka.actor._ +import akka.util.ByteString +import Tcp._ + +/** + * Base class for TcpIncomingConnection and TcpOutgoingConnection. + */ +abstract class TcpConnection(val selector: ActorRef, + val channel: SocketChannel) extends Actor with ThreadLocalDirectBuffer with ActorLogging { + + channel.configureBlocking(false) + + var pendingWrite: Write = Write.Empty // a write "queue" of size 1 for holding one unfinished write command + def writePending = pendingWrite ne Write.Empty + + def registerTimeout = Tcp(context.system).Settings.RegisterTimeout + + // STATES + + /** connection established, waiting for registration from user handler */ + def waitingForRegistration(commander: ActorRef): Receive = { + case Register(handler) ⇒ + log.debug("{} registered as connection handler", handler) + selector ! ReadInterest + + context.setReceiveTimeout(Duration.Undefined) + context.watch(handler) // sign death pact + + context.become(connected(handler)) + + case cmd: CloseCommand ⇒ + handleClose(commander, closeResponse(cmd)) + + case ReceiveTimeout ⇒ + // TODO: just shutting down, as we do here, presents a race condition to the user + // Should we introduce a dedicated `Registered` event message to notify the user of successful registration? + log.warning("Configured registration timeout of {} expired, stopping", registerTimeout) + context.stop(self) + } + + /** normal connected state */ + def connected(handler: ActorRef): Receive = { + case StopReading ⇒ selector ! StopReading + case ResumeReading ⇒ selector ! ReadInterest + case ChannelReadable ⇒ doRead(handler) + + case write: Write if writePending ⇒ + log.debug("Dropping write because queue is full") + handler ! CommandFailed(write) + + case write: Write ⇒ doWrite(handler, write) + case ChannelWritable ⇒ doWrite(handler, pendingWrite) + + case cmd: CloseCommand ⇒ handleClose(handler, closeResponse(cmd)) + } + + /** connection is closing but a write has to be finished first */ + def closingWithPendingWrite(handler: ActorRef, closedEvent: ConnectionClosed): Receive = { + case StopReading ⇒ selector ! StopReading + case ResumeReading ⇒ selector ! ReadInterest + case ChannelReadable ⇒ doRead(handler) + + case ChannelWritable ⇒ + doWrite(handler, pendingWrite) + if (!writePending) // writing is now finished + handleClose(handler, closedEvent) + + case Abort ⇒ handleClose(handler, Aborted) + } + + /** connection is closed on our side and we're waiting from confirmation from the other side */ + def closing(handler: ActorRef): Receive = { + case StopReading ⇒ selector ! StopReading + case ResumeReading ⇒ selector ! ReadInterest + case ChannelReadable ⇒ doRead(handler) + case Abort ⇒ handleClose(handler, Aborted) + } + + // AUXILIARIES and IMPLEMENTATION + + /** use in subclasses to start the common machinery above once a channel is connected */ + def completeConnect(commander: ActorRef, options: immutable.Seq[SocketOption]): Unit = { + options.foreach(_.afterConnect(channel.socket)) + + commander ! Connected( + channel.socket.getRemoteSocketAddress.asInstanceOf[InetSocketAddress], + channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]) + + context.setReceiveTimeout(registerTimeout) + context.become(waitingForRegistration(commander)) + } + + def doRead(handler: ActorRef): Unit = { + val buffer = directBuffer() + + try { + log.debug("Trying to read from channel") + val readBytes = channel.read(buffer) + buffer.flip() + + if (readBytes > 0) { + log.debug("Read {} bytes", readBytes) + handler ! Received(ByteString(buffer).take(readBytes)) + if (readBytes == buffer.capacity()) + // directly try reading more because we exhausted our buffer + self ! ChannelReadable + else selector ! ReadInterest + } else if (readBytes == 0) { + log.debug("Read nothing. Registering read interest with selector") + selector ! ReadInterest + } else if (readBytes == -1) { + log.debug("Read returned end-of-stream") + doCloseConnection(handler, closeReason) + } else throw new IllegalStateException("Unexpected value returned from read: " + readBytes) + + } catch { + case e: IOException ⇒ handleError(handler, e) + } + } + + def doWrite(handler: ActorRef, write: Write): Unit = { + val data = write.data + + val buffer = directBuffer() + data.copyToBuffer(buffer) + buffer.flip() + + try { + log.debug("Trying to write to channel") + val writtenBytes = channel.write(buffer) + log.debug("Wrote {} bytes", writtenBytes) + pendingWrite = consume(write, writtenBytes) + + if (writePending) selector ! WriteInterest // still data to write + else if (write.ack != null) handler ! write.ack // everything written + } catch { + case e: IOException ⇒ handleError(handler, e) + } + } + + def closeReason = + if (channel.socket.isOutputShutdown) ConfirmedClosed + else PeerClosed + + def handleClose(handler: ActorRef, closedEvent: ConnectionClosed): Unit = + if (closedEvent == Aborted) { // close instantly + log.debug("Got Abort command. RESETing connection.") + doCloseConnection(handler, closedEvent) + + } else if (writePending) { // finish writing first + log.debug("Got Close command but write is still pending.") + context.become(closingWithPendingWrite(handler, closedEvent)) + + } else if (closedEvent == ConfirmedClosed) { // shutdown output and wait for confirmation + log.debug("Got ConfirmedClose command, sending FIN.") + channel.socket.shutdownOutput() + context.become(closing(handler)) + + } else { // close now + log.debug("Got Close command, closing connection.") + doCloseConnection(handler, closedEvent) + } + + def doCloseConnection(handler: ActorRef, closedEvent: ConnectionClosed): Unit = { + if (closedEvent == Aborted) abort() + else channel.close() + + handler ! closedEvent + context.stop(self) + } + + def closeResponse(closeCommand: CloseCommand): ConnectionClosed = + closeCommand match { + case Close ⇒ Closed + case Abort ⇒ Aborted + case ConfirmedClose ⇒ ConfirmedClosed + } + + def handleError(handler: ActorRef, exception: IOException): Unit = { + exception.setStackTrace(Array.empty) + handler ! ErrorClose(exception) + throw exception + } + + def abort(): Unit = { + try channel.socket.setSoLinger(true, 0) // causes the following close() to send TCP RST + catch { + case NonFatal(e) ⇒ + // setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574 + // (also affected: OS/X Java 1.6.0_37) + log.debug("setSoLinger(true, 0) failed with {}", e) + } + channel.close() + } + + override def postStop(): Unit = + if (channel.isOpen) + abort() + + /** Returns a new write with `numBytes` removed from the front */ + def consume(write: Write, numBytes: Int): Write = + write match { + case Write.Empty if numBytes == 0 ⇒ write + case _ ⇒ + numBytes match { + case 0 ⇒ write + case x if x == write.data.length ⇒ Write.Empty + case _ ⇒ + require(numBytes > 0 && numBytes < write.data.length) + write.copy(data = write.data.drop(numBytes)) + } + } +} diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala new file mode 100644 index 0000000000..009621f032 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import java.nio.channels.SocketChannel +import scala.collection.immutable +import akka.actor.ActorRef +import Tcp.SocketOption + +/** + * An actor handling the connection state machine for an incoming, already connected + * SocketChannel. + */ +class TcpIncomingConnection(_selector: ActorRef, + _channel: SocketChannel, + handler: ActorRef, + options: immutable.Seq[SocketOption]) extends TcpConnection(_selector, _channel) { + + context.watch(handler) // sign death pact + + completeConnect(handler, options) + + def receive = PartialFunction.empty +} diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-io/src/main/scala/akka/io/TcpManager.scala index 344a861580..350a70eead 100644 --- a/akka-io/src/main/scala/akka/io/TcpManager.scala +++ b/akka-io/src/main/scala/akka/io/TcpManager.scala @@ -46,8 +46,8 @@ class TcpManager extends Actor { val selectorPool = context.actorOf(Props.empty.withRouter(RandomRouter(settings.NrOfSelectors))) def receive = { - case c: Connect ⇒ selectorPool forward c - case b: Bind ⇒ selectorPool forward b + case c: Connect ⇒ selectorPool forward c + case b: Bind ⇒ selectorPool forward b case Reject(command, commander) ⇒ commander ! CommandFailed(command) } } diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala new file mode 100644 index 0000000000..6df56c696d --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import java.net.InetSocketAddress +import java.io.IOException +import java.nio.channels.SocketChannel +import scala.collection.immutable +import akka.actor.ActorRef +import Tcp._ + +/** + * An actor handling the connection state machine for an outgoing connection + * to be established. + */ +class TcpOutgoingConnection(_selector: ActorRef, + commander: ActorRef, + remoteAddress: InetSocketAddress, + localAddress: Option[InetSocketAddress], + options: immutable.Seq[SocketOption]) + extends TcpConnection(_selector, SocketChannel.open()) { + context.watch(commander) // sign death pact + + localAddress.foreach(channel.socket.bind) + options.foreach(_.beforeConnect(channel.socket)) + + log.debug("Attempting connection to {}", remoteAddress) + if (channel.connect(remoteAddress)) + completeConnect(commander, options) + else { + selector ! RegisterClientChannel(channel) + context.become(connecting(commander, options)) + } + + def receive: Receive = PartialFunction.empty + + def connecting(commander: ActorRef, options: immutable.Seq[SocketOption]): Receive = { + case ChannelConnectable ⇒ + try { + val connected = channel.finishConnect() + assert(connected, "Connectable channel failed to connect") + log.debug("Connection established") + completeConnect(commander, options) + } catch { + case e: IOException ⇒ handleError(commander, e) + } + } + +} diff --git a/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala b/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala new file mode 100644 index 0000000000..ab0b38510c --- /dev/null +++ b/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import java.nio.ByteBuffer +import akka.actor.Actor + +/** + * Allows an actor to get a thread local direct buffer of a size defined in the + * configuration of the actor system. An underlying assumption is that all of + * the threads which call `getDirectBuffer` are owned by the actor system. + */ +trait ThreadLocalDirectBuffer { _: Actor ⇒ + def directBuffer(): ByteBuffer = { + val result = ThreadLocalDirectBuffer.threadLocalBuffer.get() + if (result == null) { + val size = Tcp(context.system).Settings.DirectBufferSize + val newBuffer = ByteBuffer.allocateDirect(size) + ThreadLocalDirectBuffer.threadLocalBuffer.set(newBuffer) + newBuffer + } else { + result.clear() + result + } + } +} + +object ThreadLocalDirectBuffer { + private val threadLocalBuffer = new ThreadLocal[ByteBuffer] +} diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala new file mode 100644 index 0000000000..2a5d3690f7 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -0,0 +1,414 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.io + +import scala.annotation.tailrec + +import java.nio.channels.{ SelectionKey, SocketChannel, ServerSocketChannel } +import java.nio.ByteBuffer +import java.nio.channels.spi.SelectorProvider +import java.io.IOException +import java.net._ +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.util.control.NonFatal +import akka.actor.{ ActorRef, Props, Actor, Terminated } +import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } +import akka.util.ByteString +import Tcp._ +import java.util.concurrent.CountDownLatch + +class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { + val port = 45679 + val localhost = InetAddress.getLocalHost + val serverAddress = new InetSocketAddress(localhost, port) + + "An outgoing connection" must { + // common behavior + + "set socket options before connecting" in withLocalServer() { localServer ⇒ + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = + createConnectionActor(selector.ref, userHandler.ref, options = Vector(SO.ReuseAddress(true))) + val clientChannel = connectionActor.underlyingActor.channel + clientChannel.socket.getReuseAddress must be(true) + } + + "set socket options after connecting" in withLocalServer() { localServer ⇒ + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = + createConnectionActor(selector.ref, userHandler.ref, options = Vector(SO.KeepAlive(true))) + val clientChannel = connectionActor.underlyingActor.channel + clientChannel.socket.getKeepAlive must be(false) // only set after connection is established + selector.send(connectionActor, ChannelConnectable) + clientChannel.socket.getKeepAlive must be(true) + } + + "send incoming data to user" in withEstablishedConnection() { setup ⇒ + import setup._ + serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII"))) + // emulate selector behavior + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsgPF(remaining) { + case Received(data) if data.decodeString("ASCII") == "testdata" ⇒ + } + // have two packets in flight before the selector notices + serverSideChannel.write(ByteBuffer.wrap("testdata2".getBytes("ASCII"))) + serverSideChannel.write(ByteBuffer.wrap("testdata3".getBytes("ASCII"))) + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsgPF(remaining) { + case Received(data) if data.decodeString("ASCII") == "testdata2testdata3" ⇒ + } + } + + "write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒ + import setup._ + serverSideChannel.configureBlocking(false) + object Ack + val write = Write(ByteString("testdata"), Ack) + val buffer = ByteBuffer.allocate(100) + serverSideChannel.read(buffer) must be(0) + + // emulate selector behavior + connectionHandler.send(connectionActor, write) + connectionHandler.expectMsg(Ack) + serverSideChannel.read(buffer) must be(8) + buffer.flip() + ByteString(buffer).take(8).decodeString("ASCII") must be("testdata") + } + + "stop writing in cases of backpressure and resume afterwards" in + withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + import setup._ + object Ack1 + object Ack2 + + //serverSideChannel.configureBlocking(false) + clientSideChannel.socket.setSendBufferSize(1024) + + // producing backpressure by sending much more than currently fits into + // our send buffer + val firstWrite = writeCmd(Ack1) + + // try to write the buffer but since the SO_SNDBUF is too small + // it will have to keep the rest of the piece and send it + // when possible + connectionHandler.send(connectionActor, firstWrite) + selector.expectMsg(WriteInterest) + + // send another write which should fail immediately + // because we don't store more than one piece in flight + val secondWrite = writeCmd(Ack2) + connectionHandler.send(connectionActor, secondWrite) + connectionHandler.expectMsg(CommandFailed(secondWrite)) + + // there will be immediately more space in the send buffer because + // some data will have been sent by now, so we assume we can write + // again, but still it can't write everything + selector.send(connectionActor, ChannelWritable) + + // both buffers should now be filled so no more writing + // is possible + setup.pullFromServerSide(TestSize) + connectionHandler.expectMsg(Ack1) + } + + "respect StopReading and ResumeReading" in withEstablishedConnection() { setup ⇒ + import setup._ + connectionHandler.send(connectionActor, StopReading) + + // the selector interprets StopReading to deregister interest + // for reading + selector.expectMsg(StopReading) + connectionHandler.send(connectionActor, ResumeReading) + selector.expectMsg(ReadInterest) + } + + "close the connection" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + import setup._ + + // we should test here that a pending write command is properly finished first + object Ack + // set an artificially small send buffer size so that the write is queued + // inside the connection actor + clientSideChannel.socket.setSendBufferSize(1024) + + // we send a write and a close command directly afterwards + connectionHandler.send(connectionActor, writeCmd(Ack)) + connectionHandler.send(connectionActor, Close) + + setup.pullFromServerSide(TestSize) + connectionHandler.expectMsg(Ack) + connectionHandler.expectMsg(Closed) + connectionActor.isTerminated must be(true) + + val buffer = ByteBuffer.allocate(1) + serverSideChannel.read(buffer) must be(-1) + } + + "abort the connection" in withEstablishedConnection() { setup ⇒ + import setup._ + + connectionHandler.send(connectionActor, Abort) + connectionHandler.expectMsg(Aborted) + + assertThisConnectionActorTerminated() + + val buffer = ByteBuffer.allocate(1) + val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException] + thrown.getMessage must be("Connection reset by peer") + } + + "close the connection and confirm" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + import setup._ + + // we should test here that a pending write command is properly finished first + object Ack + // set an artificially small send buffer size so that the write is queued + // inside the connection actor + clientSideChannel.socket.setSendBufferSize(1024) + + // we send a write and a close command directly afterwards + connectionHandler.send(connectionActor, writeCmd(Ack)) + connectionHandler.send(connectionActor, ConfirmedClose) + + connectionHandler.expectNoMsg(100.millis) + setup.pullFromServerSide(TestSize) + connectionHandler.expectMsg(Ack) + + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectNoMsg(100.millis) // not yet + + val buffer = ByteBuffer.allocate(1) + serverSideChannel.read(buffer) must be(-1) + serverSideChannel.close() + + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsg(ConfirmedClosed) + + assertThisConnectionActorTerminated() + } + + "report when peer closed the connection" in withEstablishedConnection() { setup ⇒ + import setup._ + + serverSideChannel.close() + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsg(PeerClosed) + + assertThisConnectionActorTerminated() + } + "report when peer aborted the connection" in withEstablishedConnection() { setup ⇒ + import setup._ + + abortClose(serverSideChannel) + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsgPF(remaining) { + case ErrorClose(exc: IOException) ⇒ exc.getMessage must be("Connection reset by peer") + } + // wait a while + connectionHandler.expectNoMsg(200.millis) + + assertThisConnectionActorTerminated() + } + "report when peer closed the connection when trying to write" in withEstablishedConnection() { setup ⇒ + import setup._ + + abortClose(serverSideChannel) + connectionHandler.send(connectionActor, Write(ByteString("testdata"))) + connectionHandler.expectMsgPF(remaining) { + case ErrorClose(_: IOException) ⇒ // ok + } + + assertThisConnectionActorTerminated() + } + + // error conditions + "report failed connection attempt while not registered" in withLocalServer() { localServer ⇒ + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = createConnectionActor(selector.ref, userHandler.ref) + val clientSideChannel = connectionActor.underlyingActor.channel + selector.expectMsg(RegisterClientChannel(clientSideChannel)) + + // close instead of accept + localServer.close() + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsgPF() { + case ErrorClose(e) ⇒ e.getMessage must be("Connection reset by peer") + } + + assertActorTerminated(connectionActor) + } + + "report failed connection attempt when target is unreachable" in { + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = createConnectionActor(selector.ref, userHandler.ref, serverAddress = new InetSocketAddress("127.0.0.1", 63186)) + val clientSideChannel = connectionActor.underlyingActor.channel + selector.expectMsg(RegisterClientChannel(clientSideChannel)) + val sel = SelectorProvider.provider().openSelector() + val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) + sel.select(200) + + key.isConnectable must be(true) + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsgPF() { + case ErrorClose(e) ⇒ e.getMessage must be("Connection refused") + } + + assertActorTerminated(connectionActor) + } + + "time out when Connected isn't answered with Register" in withLocalServer() { localServer ⇒ + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = createConnectionActor(selector.ref, userHandler.ref) + val clientSideChannel = connectionActor.underlyingActor.channel + selector.expectMsg(RegisterClientChannel(clientSideChannel)) + localServer.accept() + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) + + assertActorTerminated(connectionActor) + } + + "close the connection when user handler dies while connecting" in withLocalServer() { localServer ⇒ + val userHandler = system.actorOf(Props(new Actor { + def receive = PartialFunction.empty + })) + val selector = TestProbe() + val connectionActor = createConnectionActor(selector.ref, userHandler) + val clientSideChannel = connectionActor.underlyingActor.channel + selector.expectMsg(RegisterClientChannel(clientSideChannel)) + system.stop(userHandler) + assertActorTerminated(connectionActor) + } + + "close the connection when connection handler dies while connected" in withEstablishedConnection() { setup ⇒ + import setup._ + watch(connectionHandler.ref) + watch(connectionActor) + system.stop(connectionHandler.ref) + expectMsgType[Terminated].actor must be(connectionHandler.ref) + expectMsgType[Terminated].actor must be(connectionActor) + } + } + + def withLocalServer(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: ServerSocketChannel ⇒ Any): Unit = { + val localServer = ServerSocketChannel.open() + try { + setServerSocketOptions(localServer) + localServer.socket.bind(serverAddress) + localServer.configureBlocking(false) + body(localServer) + } finally localServer.close() + } + + case class Setup( + userHandler: TestProbe, + connectionHandler: TestProbe, + selector: TestProbe, + connectionActor: TestActorRef[TcpOutgoingConnection], + clientSideChannel: SocketChannel, + serverSideChannel: SocketChannel) { + + val buffer = ByteBuffer.allocate(TestSize) + @tailrec final def pullFromServerSide(remaining: Int): Unit = + if (remaining > 0) { + if (selector.msgAvailable) { + selector.expectMsg(WriteInterest) + selector.send(connectionActor, ChannelWritable) + } + buffer.clear() + val read = serverSideChannel.read(buffer) + if (read == 0) + throw new IllegalStateException("Didn't make any progress") + else if (read == -1) + throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) + + pullFromServerSide(remaining - read) + } + + def assertThisConnectionActorTerminated(): Unit = { + assertActorTerminated(connectionActor) + clientSideChannel must not be ('open) + } + } + def withEstablishedConnection(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: Setup ⇒ Any): Unit = withLocalServer(setServerSocketOptions) { localServer ⇒ + val userHandler = TestProbe() + val connectionHandler = TestProbe() + val selector = TestProbe() + val connectionActor = createConnectionActor(selector.ref, userHandler.ref) + val clientSideChannel = connectionActor.underlyingActor.channel + + selector.expectMsg(RegisterClientChannel(clientSideChannel)) + + localServer.configureBlocking(true) + val serverSideChannel = localServer.accept() + + serverSideChannel must not be (null) + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) + userHandler.send(connectionActor, Register(connectionHandler.ref)) + selector.expectMsg(ReadInterest) + + body { + Setup( + userHandler, + connectionHandler, + selector, + connectionActor, + clientSideChannel, + serverSideChannel) + } + } + + val TestSize = 10000 + + def writeCmd(ack: AnyRef) = + Write(ByteString(Array.fill[Byte](TestSize)(0)), ack) + + def setSmallRcvBuffer(channel: ServerSocketChannel): Unit = + channel.socket.setReceiveBufferSize(1024) + + def createConnectionActor( + selector: ActorRef, + commander: ActorRef, + serverAddress: InetSocketAddress = serverAddress, + localAddress: Option[InetSocketAddress] = None, + options: immutable.Seq[Tcp.SocketOption] = Nil): TestActorRef[TcpOutgoingConnection] = { + + TestActorRef( + new TcpOutgoingConnection(selector, commander, serverAddress, localAddress, options) { + override def postRestart(reason: Throwable) { + // ensure we never restart + context.stop(self) + } + }) + } + + def abortClose(channel: SocketChannel): Unit = { + try channel.socket.setSoLinger(true, 0) // causes the following close() to send TCP RST + catch { + case NonFatal(e) ⇒ + // setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574 + // (also affected: OS/X Java 1.6.0_37) + log.debug("setSoLinger(true, 0) failed with {}", e) + } + channel.close() + } + + def abort(channel: SocketChannel) { + channel.socket.setSoLinger(true, 0) + channel.close() + } + def assertActorTerminated(connectionActor: TestActorRef[TcpOutgoingConnection]): Unit = { + watch(connectionActor) + expectMsgType[Terminated].actor must be(connectionActor) + } +} From 27d111b1f5f679967e7da6896006a1d13666c2b1 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 16 Jan 2013 15:21:11 +0100 Subject: [PATCH 09/66] tcp selector and listener actors, extension of tcp manager actor, see #2885 and #2887 --- akka-io/src/main/resources/reference.conf | 31 +-- akka-io/src/main/scala/akka/io/Tcp.scala | 89 ++++---- .../src/main/scala/akka/io/TcpListener.scala | 76 +++++++ .../src/main/scala/akka/io/TcpManager.scala | 53 ++++- .../scala/akka/io/TcpOutgoingConnection.scala | 2 +- .../src/main/scala/akka/io/TcpSelector.scala | 205 ++++++++++++++++++ akka-io/src/test/scala/akka/io/IOSpec.scala | 11 - .../scala/akka/io/TcpConnectionSpec.scala | 10 +- .../test/scala/akka/io/TcpListenerSpec.scala | 62 ++++++ 9 files changed, 460 insertions(+), 79 deletions(-) create mode 100644 akka-io/src/main/scala/akka/io/TcpListener.scala create mode 100644 akka-io/src/main/scala/akka/io/TcpSelector.scala delete mode 100644 akka-io/src/test/scala/akka/io/IOSpec.scala create mode 100644 akka-io/src/test/scala/akka/io/TcpListenerSpec.scala diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index ebcd1fa2e9..6104ac96c5 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -38,11 +38,25 @@ akka { # - setting it to zero means polling, i.e. calling selectNow() select-timeout = infinite - # When trying to create a new connection but the chosen selector is at - # full capacity, retry this many times with different selectors before - # giving up + # When trying to assign a new connection to a selector and the chosen + # selector is at full capacity, retry selector choosing and assignment + # this many times before giving up selector-association-retries = 10 - + + # The maximum number of connection that are accepted in one go, + # higher numbers decrease latency, lower numbers increase fairness on + # the worker-dispatcher + batch-accept-limit = 10 + + # The size of the thread-local direct buffers used to read or write + # network data from the kernel. Those buffer directly add to the footprint + # of the threads from the dispatcher tcp connection actors are using. + direct-buffer-size = 524288 + + # The duration a connection actor waits for a `Register` message from + # its commander before aborting the connection. + register-timeout = 5s + # Fully qualified config path which holds the dispatcher configuration # to be used for running the select() calls in the selectors selector-dispatcher = "akka.io.pinned-dispatcher" @@ -54,15 +68,6 @@ akka { # Fully qualified config path which holds the dispatcher configuration # for the selector management actors management-dispatcher = "akka.actor.default-dispatcher" - - # The size of the thread-local direct buffers used to read or write - # network data from the kernel. Those buffer directly add to the footprint - # of the threads from the dispatcher tcp connection actors are using. - direct-buffer-size = 524288 - - # The duration a connection actor waits for a `Register` message from - # its commander before aborting the connection. - register-timeout = 5s } } diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 9d7e754b98..7d0421f42a 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -23,18 +23,6 @@ object Tcp extends ExtensionKey[TcpExt] { // Java API override def get(system: ActorSystem): TcpExt = system.extension(this) - /// COMMANDS - sealed trait Command - - case class Connect(remoteAddress: InetSocketAddress, - localAddress: Option[InetSocketAddress] = None) extends Command - case class Bind(handler: ActorRef, - address: InetSocketAddress, - backlog: Int = 100, - options: immutable.Seq[SocketOption] = Nil) extends Command - case object Unbind extends Command - case class Register(handler: ActorRef) extends Command - /** * SocketOption is a package of data (from the user) and associated * behavior (how to apply that to a socket). @@ -54,11 +42,12 @@ object Tcp extends ExtensionKey[TcpExt] { */ def afterConnect(s: Socket): Unit = () } + + // shared socket options object SO { - // shared socket options /** - * [[akka.io.Tcp.SO.SocketOption]] to set the SO_RCVBUF option + * [[akka.io.Tcp.SocketOption]] to set the SO_RCVBUF option * * For more information see [[java.net.Socket.setReceiveBufferSize]] */ @@ -71,7 +60,7 @@ object Tcp extends ExtensionKey[TcpExt] { // server socket options /** - * [[akka.io.Tcp.SO.SocketOption]] to enable or disable SO_REUSEADDR + * [[akka.io.Tcp.SocketOption]] to enable or disable SO_REUSEADDR * * For more information see [[java.net.Socket.setReuseAddress]] */ @@ -83,7 +72,7 @@ object Tcp extends ExtensionKey[TcpExt] { // general socket options /** - * [[akka.io.Tcp.SO.SocketOption]] to enable or disable SO_KEEPALIVE + * [[akka.io.Tcp.SocketOption]] to enable or disable SO_KEEPALIVE * * For more information see [[java.net.Socket.setKeepAlive]] */ @@ -92,7 +81,7 @@ object Tcp extends ExtensionKey[TcpExt] { } /** - * [[akka.io.Tcp.SO.SocketOption]] to enable or disable OOBINLINE (receipt + * [[akka.io.Tcp.SocketOption]] to enable or disable OOBINLINE (receipt * of TCP urgent data) By default, this option is disabled and TCP urgent * data is silently discarded. * @@ -103,19 +92,19 @@ object Tcp extends ExtensionKey[TcpExt] { } /** - * [[akka.io.Tcp.SO.SocketOption]] to set the SO_SNDBUF option. + * [[akka.io.Tcp.SocketOption]] to set the SO_SNDBUF option. * * For more information see [[java.net.Socket.setSendBufferSize]] */ case class SendBufferSize(size: Int) extends SocketOption { - require(size > 0, "SendBufferSize must be > 0") + require(size > 0, "ReceiveBufferSize must be > 0") override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) } // SO_LINGER is handled by the Close code /** - * [[akka.io.Tcp.SO.SocketOption]] to enable or disable TCP_NODELAY + * [[akka.io.Tcp.SocketOption]] to enable or disable TCP_NODELAY * (disable or enable Nagle's algorithm) * * For more information see [[java.net.Socket.setTcpNoDelay]] @@ -125,7 +114,7 @@ object Tcp extends ExtensionKey[TcpExt] { } /** - * [[akka.io.Tcp.SO.SocketOption]] to set the traffic class or + * [[akka.io.Tcp.SocketOption]] to set the traffic class or * type-of-service octet in the IP header for packets sent from this * socket. * @@ -137,9 +126,28 @@ object Tcp extends ExtensionKey[TcpExt] { } } - // TODO: what about close reasons? - sealed trait CloseCommand extends Command + case class Stats(channelsOpened: Long, channelsClosed: Long, selectorStats: Seq[SelectorStats]) { + def channelsOpen = channelsOpened - channelsClosed + } + case class SelectorStats(channelsOpened: Long, channelsClosed: Long) { + def channelsOpen = channelsOpened - channelsClosed + } + + /// COMMANDS + sealed trait Command + + case class Connect(remoteAddress: InetSocketAddress, + localAddress: Option[InetSocketAddress] = None, + options: immutable.Seq[SocketOption] = Nil) extends Command + case class Bind(handler: ActorRef, + endpoint: InetSocketAddress, + backlog: Int = 100, + options: immutable.Seq[SocketOption] = Nil) extends Command + case class Register(handler: ActorRef) extends Command + case object Unbind extends Command + + sealed trait CloseCommand extends Command case object Close extends CloseCommand case object ConfirmedClose extends CloseCommand case object Abort extends CloseCommand @@ -154,6 +162,8 @@ object Tcp extends ExtensionKey[TcpExt] { case object StopReading extends Command case object ResumeReading extends Command + case object GetStats extends Command + /// EVENTS sealed trait Event @@ -171,15 +181,12 @@ object Tcp extends ExtensionKey[TcpExt] { case class ErrorClose(cause: Throwable) extends ConnectionClosed /// INTERNAL - case class RegisterClientChannel(channel: SocketChannel) - case class RegisterServerChannel(channel: ServerSocketChannel) - case class CreateConnection(channel: SocketChannel) - case class Reject(command: Command, commander: ActorRef) - // Retry should be sent by Selector actors to their parent router with retriesLeft decremented. If retries are - // depleted, the selector actor must reply directly to the manager with a Reject (above). - case class Retry(command: Command, retriesLeft: Int, commander: ActorRef) { - require(retriesLeft >= 0, "The upper limit for retries must be nonnegative.") - } + case class RegisterOutgoingConnection(channel: SocketChannel) + case class RegisterServerSocketChannel(channel: ServerSocketChannel) + case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, options: immutable.Seq[SocketOption]) + case class CreateConnection(channel: SocketChannel, handler: ActorRef, options: immutable.Seq[SocketOption]) + case class Reject(command: Command, retriesLeft: Int, commander: ActorRef) + case class Retry(command: Command, retriesLeft: Int, commander: ActorRef) case object ChannelConnectable case object ChannelAcceptable case object ChannelReadable @@ -197,21 +204,29 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val NrOfSelectors = getInt("nr-of-selectors") val MaxChannels = getInt("max-channels") - val MaxChannelsPerSelector = MaxChannels / NrOfSelectors val SelectTimeout = if (getString("select-timeout") == "infinite") Duration.Inf else Duration(getMilliseconds("select-timeout"), MILLISECONDS) val SelectorAssociationRetries = getInt("selector-association-retries") - val SelectorDispatcher = getString("selector-dispatcher") - val WorkerDispatcher = getString("worker-dispatcher") - val ManagementDispatcher = getString("management-dispatcher") + val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getInt("direct-buffer-size") val RegisterTimeout = if (getString("register-timeout") == "infinite") Duration.Undefined else Duration(getMilliseconds("register-timeout"), MILLISECONDS) + val SelectorDispatcher = getString("selector-dispatcher") + val WorkerDispatcher = getString("worker-dispatcher") + val ManagementDispatcher = getString("management-dispatcher") + + require(NrOfSelectors > 0, "nr-of-selectors must be > 0") + require(MaxChannels >= 0, "max-channels must be >= 0") + require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative") + require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0") + require(BatchAcceptLimit > 0, "batch-accept-limit must be > 0") + + val MaxChannelsPerSelector = MaxChannels / NrOfSelectors } val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( - Props[TcpManager].withDispatcher(Settings.ManagementDispatcher), "IO-TCP") + Props.empty.withDispatcher(Settings.ManagementDispatcher), "IO-TCP") } diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala new file mode 100644 index 0000000000..1f339be702 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.io + +import java.net.InetSocketAddress +import java.nio.channels.ServerSocketChannel +import scala.annotation.tailrec +import scala.collection.immutable +import scala.util.control.NonFatal +import akka.actor.{ ActorLogging, ActorRef, Actor } +import Tcp._ + +class TcpListener(manager: ActorRef, + selector: ActorRef, + handler: ActorRef, + endpoint: InetSocketAddress, + backlog: Int, + bindCommander: ActorRef, + options: immutable.Seq[SocketOption]) extends Actor with ActorLogging { + + val batchAcceptLimit = Tcp(context.system).Settings.BatchAcceptLimit + val channel = { + val serverSocketChannel = ServerSocketChannel.open + serverSocketChannel.configureBlocking(false) + val socket = serverSocketChannel.socket + options.foreach(_.beforeBind(socket)) + socket.bind(endpoint, backlog) // will blow up the actor constructor if the bind fails + serverSocketChannel + } + selector ! RegisterServerSocketChannel(channel) + context.watch(bindCommander) // sign death pact + log.debug("Successfully bound to {}", endpoint) + + def receive: Receive = { + case Bound ⇒ + bindCommander ! Bound + context.become(bound) + } + + def bound: Receive = { + case ChannelAcceptable ⇒ + acceptAllPending(batchAcceptLimit) + + case Unbind ⇒ + log.debug("Unbinding endpoint {}", endpoint) + channel.close() + sender ! Unbound + log.debug("Unbound endpoint {}, stopping listener", endpoint) + context.stop(self) + } + + @tailrec final def acceptAllPending(limit: Int): Unit = + if (limit > 0) { + val socketChannel = + try channel.accept() + catch { + case NonFatal(e) ⇒ log.error(e, "Accept error: could not accept new connection due to {}", e); null + } + if (socketChannel != null) { + log.debug("New connection accepted") + manager ! RegisterIncomingConnection(socketChannel, handler, options) + selector ! AcceptInterest + acceptAllPending(limit - 1) + } + } + + override def postStop() { + try channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing ServerSocketChannel") + } + } + +} diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-io/src/main/scala/akka/io/TcpManager.scala index 350a70eead..bba033dc9b 100644 --- a/akka-io/src/main/scala/akka/io/TcpManager.scala +++ b/akka-io/src/main/scala/akka/io/TcpManager.scala @@ -1,14 +1,21 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + package akka.io -import akka.actor.{ OneForOneStrategy, Actor, Props } -import akka.io.Tcp._ +import scala.concurrent.Future +import scala.concurrent.duration._ +import akka.actor.{ ActorLogging, Actor, Props } import akka.routing.RandomRouter -import akka.actor.SupervisorStrategy.Restart +import akka.util.Timeout +import akka.pattern.{ ask, pipe } +import Tcp._ /** * TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections. * - * TcpManager is obtainable by calling {{{ IO(TCP) }}} (see [[akka.io.IO]] and [[akka.io.Tcp]]) + * TcpManager is obtainable by calling {{{ IO(Tcp) }}} (see [[akka.io.IO]] and [[akka.io.Tcp]]) * * == Bind == * @@ -30,9 +37,9 @@ import akka.actor.SupervisorStrategy.Restart * To initiate a connection to a remote server, a [[akka.io.Tcp.Connect]] message must be sent to this actor. If the * connection succeeds, the sender will be notified with a [[akka.io.Tcp.Connected]] message. The sender of the * [[akka.io.Tcp.Connected]] message is the Connection actor (an internal actor representing the TCP connection). Before - * starting to use the connection, a handler should be registered to the Connection actor by sending a [[akka.io.Tcp.Register]] - * message. After a handler has been registered, all incoming data will be sent to the handler in the form of - * [[akka.io.Tcp.Received]] messages. To write data to the connection, a [[akka.io.Tcp.Write]] message should be sent + * starting to use the connection, a handler must be registered to the Connection actor by sending a [[akka.io.Tcp.Register]] + * command message. After a handler has been registered, all incoming data will be sent to the handler in the form of + * [[akka.io.Tcp.Received]] messages. To write data to the connection, a [[akka.io.Tcp.Write]] message must be sent * to the Connection actor. * * If the connect request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors @@ -40,14 +47,36 @@ import akka.actor.SupervisorStrategy.Restart * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. * */ -class TcpManager extends Actor { +class TcpManager extends Actor with ActorLogging { val settings = Tcp(context.system).Settings + val selectorNr = Iterator.from(0) - val selectorPool = context.actorOf(Props.empty.withRouter(RandomRouter(settings.NrOfSelectors))) + val selectorPool = context.actorOf( + props = Props(new TcpSelector(self)).withRouter(RandomRouter(settings.NrOfSelectors)), + name = selectorNr.next().toString) def receive = { - case c: Connect ⇒ selectorPool forward c - case b: Bind ⇒ selectorPool forward b - case Reject(command, commander) ⇒ commander ! CommandFailed(command) + case RegisterIncomingConnection(channel, handler, options) ⇒ + selectorPool ! CreateConnection(channel, handler, options) + + case c: Connect ⇒ + selectorPool forward c + + case b: Bind ⇒ + selectorPool forward b + + case Reject(command, 0, commander) ⇒ + log.warning("Command '{}' failed since all {} selectors are at capacity", command, context.children.size) + commander ! CommandFailed(command) + + case Reject(command, retriesLeft, commander) ⇒ + log.warning("Command '{}' rejected by {} with {} retries left, retrying...", command, sender, retriesLeft) + selectorPool ! Retry(command, retriesLeft - 1, commander) + + case GetStats ⇒ + import context.dispatcher + implicit val timeout: Timeout = 1 second span + val seqFuture = Future.traverse(context.children)(_.ask(GetStats).mapTo[SelectorStats]) + seqFuture.map(s ⇒ Stats(s.map(_.channelsOpen).sum, s.map(_.channelsClosed).sum, s.toSeq)) pipeTo sender } } diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index 6df56c696d..cfdc7a2af0 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -30,7 +30,7 @@ class TcpOutgoingConnection(_selector: ActorRef, if (channel.connect(remoteAddress)) completeConnect(commander, options) else { - selector ! RegisterClientChannel(channel) + selector ! RegisterOutgoingConnection(channel) context.become(connecting(commander, options)) } diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala new file mode 100644 index 0000000000..189755c079 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -0,0 +1,205 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.io + +import java.lang.Runnable +import java.nio.channels.spi.SelectorProvider +import java.nio.channels.{ ServerSocketChannel, SelectionKey, SocketChannel } +import java.nio.channels.SelectionKey._ +import scala.util.control.NonFatal +import scala.collection.immutable.HashMap +import scala.concurrent.duration._ +import akka.actor._ +import Tcp._ + +class TcpSelector(manager: ActorRef) extends Actor with ActorLogging { + @volatile var childrenKeys = HashMap.empty[String, SelectionKey] + var channelsOpened = 0L + var channelsClosed = 0L + val sequenceNumber = Iterator.from(0) + val settings = Tcp(context.system).Settings + val selectorManagementDispatcher = context.system.dispatchers.lookup(settings.SelectorDispatcher) + val selector = SelectorProvider.provider.openSelector + val doSelect: () ⇒ Int = + settings.SelectTimeout match { + case Duration.Zero ⇒ () ⇒ selector.selectNow() + case Duration.Inf ⇒ () ⇒ selector.select() + case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) + } + + selectorManagementDispatcher.execute(select) // start selection "loop" + + def receive: Receive = { + case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) + case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) + case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) + + case CreateConnection(channel, handler, options) ⇒ + val connection = context.actorOf( + props = Props( + creator = () ⇒ new TcpIncomingConnection(self, channel, handler, options), + dispatcher = settings.WorkerDispatcher), + name = nextName) + execute(registerIncomingConnection(channel, handler)) + context.watch(connection) + channelsOpened += 1 + + case cmd: Connect ⇒ + handleConnect(cmd, settings.SelectorAssociationRetries, sender) + + case Retry(cmd: Connect, retriesLeft, commander) ⇒ + handleConnect(cmd, retriesLeft, commander) + + case RegisterOutgoingConnection(channel) ⇒ + execute(registerOutgoingConnection(channel, sender)) + + case cmd: Bind ⇒ + handleBind(cmd, settings.SelectorAssociationRetries, sender) + + case Retry(cmd: Bind, retriesLeft, commander) ⇒ + handleBind(cmd, retriesLeft, commander) + + case RegisterServerSocketChannel(channel) ⇒ + execute(registerListener(channel, sender)) + + case Terminated(child) ⇒ + execute(unregister(child)) + channelsClosed += 1 + + case GetStats ⇒ + sender ! SelectorStats(channelsOpened, channelsClosed) + } + + override def postStop() { + try { + import scala.collection.JavaConverters._ + selector.keys.asScala.foreach(_.channel.close()) + selector.close() + } catch { + case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") + } + } + + // we can never recover from failures of a connection or listener child + override def supervisorStrategy = SupervisorStrategy.stoppingStrategy + + def handleConnect(cmd: Connect, retriesLeft: Int, commander: ActorRef): Unit = { + log.debug("Executing {}", cmd) + if (canHandleMoreChannels) { + val connection = context.actorOf( + props = Props( + creator = () ⇒ new TcpOutgoingConnection(self, commander, cmd.remoteAddress, cmd.localAddress, cmd.options), + dispatcher = settings.WorkerDispatcher), + name = nextName) + context.watch(connection) + channelsOpened += 1 + } else sender ! Reject(cmd, retriesLeft, commander) + } + + def handleBind(cmd: Bind, retriesLeft: Int, commander: ActorRef): Unit = { + log.debug("Executing {}", cmd) + if (canHandleMoreChannels) { + val listener = context.actorOf( + props = Props( + creator = () ⇒ new TcpListener(manager, self, cmd.handler, cmd.endpoint, cmd.backlog, commander, cmd.options), + dispatcher = settings.WorkerDispatcher), + name = nextName) + context.watch(listener) + channelsOpened += 1 + } else sender ! Reject(cmd, retriesLeft, commander) + } + + def nextName = sequenceNumber.next().toString + + def canHandleMoreChannels = childrenKeys.size < settings.MaxChannelsPerSelector + + //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// + + def execute(task: Task): Unit = { + selectorManagementDispatcher.execute(task) + selector.wakeup() + } + + def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = + childrenKeys = childrenKeys.updated(child.path.name, key) + + def registerOutgoingConnection(channel: SocketChannel, connection: ActorRef) = + new Task { + def tryRun() { + val key = channel.register(selector, OP_CONNECT, connection) + updateKeyMap(connection, key) + } + } + + def registerListener(channel: ServerSocketChannel, listener: ActorRef) = + new Task { + def tryRun() { + val key = channel.register(selector, OP_ACCEPT, listener) + updateKeyMap(listener, key) + listener ! Bound + } + } + + def registerIncomingConnection(channel: SocketChannel, connection: ActorRef) = + new Task { + def tryRun() { + // we only enable reading after the user-level connection handler has registered + val key = channel.register(selector, 0, connection) + updateKeyMap(connection, key) + } + } + + // TODO: evaluate whether we could run this on the TcpSelector actor itself rather than + // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap + // rather than an unsynchronized one, but since switching interest ops is so frequent + // the change might be beneficial, provided the underlying implementation really is thread-safe + // and behaves consistently on all platforms. + def enableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps | op) + } + } + + def unregister(child: ActorRef) = + new Task { + def tryRun() { + childrenKeys = childrenKeys - child.path.name + } + } + + val select = new Task { + def tryRun() { + if (doSelect() > 0) { + val keys = selector.selectedKeys + val iterator = keys.iterator() + while (iterator.hasNext) { + val key = iterator.next + val connection = key.attachment.asInstanceOf[ActorRef] + if (key.isValid) { + if (key.isReadable) connection ! ChannelReadable + if (key.isWritable) connection ! ChannelWritable + else if (key.isAcceptable) connection ! ChannelAcceptable + else if (key.isConnectable) connection ! ChannelConnectable + key.interestOps(0) // prevent immediate reselection by always clearing + } else log.warning("Invalid selection key: {}", key) + } + keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected + } + selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks + } + } + + abstract class Task extends Runnable { + def tryRun() + def run() { + try tryRun() + catch { + case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) + } + } + } +} \ No newline at end of file diff --git a/akka-io/src/test/scala/akka/io/IOSpec.scala b/akka-io/src/test/scala/akka/io/IOSpec.scala deleted file mode 100644 index 553aef8f4a..0000000000 --- a/akka-io/src/test/scala/akka/io/IOSpec.scala +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (C) 2009-2013 Typesafe Inc. - */ - -package akka.io - -import akka.testkit.AkkaSpec - -class IOSpec extends AkkaSpec { - -} \ No newline at end of file diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 2a5d3690f7..31da412b33 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -233,7 +233,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val selector = TestProbe() val connectionActor = createConnectionActor(selector.ref, userHandler.ref) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterClientChannel(clientSideChannel)) + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) // close instead of accept localServer.close() @@ -250,7 +250,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val selector = TestProbe() val connectionActor = createConnectionActor(selector.ref, userHandler.ref, serverAddress = new InetSocketAddress("127.0.0.1", 63186)) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterClientChannel(clientSideChannel)) + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) val sel = SelectorProvider.provider().openSelector() val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) sel.select(200) @@ -269,7 +269,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val selector = TestProbe() val connectionActor = createConnectionActor(selector.ref, userHandler.ref) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterClientChannel(clientSideChannel)) + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) localServer.accept() selector.send(connectionActor, ChannelConnectable) userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) @@ -284,7 +284,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val selector = TestProbe() val connectionActor = createConnectionActor(selector.ref, userHandler) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterClientChannel(clientSideChannel)) + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) system.stop(userHandler) assertActorTerminated(connectionActor) } @@ -346,7 +346,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val connectionActor = createConnectionActor(selector.ref, userHandler.ref) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterClientChannel(clientSideChannel)) + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) localServer.configureBlocking(true) val serverSideChannel = localServer.accept() diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala new file mode 100644 index 0000000000..addf9f7f16 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import java.net.{ Socket, InetSocketAddress } +import java.nio.channels.ServerSocketChannel +import scala.concurrent.duration._ +import scala.util.Success +import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } +import akka.util.Timeout +import akka.pattern.ask +import Tcp._ + +class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { + val port = 47323 + + "A TcpListener" must { + val manager = TestProbe() + val selector = TestProbe() + val handler = TestProbe() + val handlerRef = handler.ref + val bindCommander = TestProbe() + val endpoint = new InetSocketAddress("localhost", port) + val listener = TestActorRef(new TcpListener(manager.ref, selector.ref, handler.ref, endpoint, 100, + bindCommander.ref, Nil)) + var serverSocketChannel: Option[ServerSocketChannel] = None + + "register its ServerSocketChannel with its selector" in { + val RegisterServerSocketChannel(channel) = selector.receiveOne(Duration.Zero) + serverSocketChannel = Some(channel) + } + + "let the Bind commander know when binding is completed" in { + listener ! Bound + bindCommander.expectMsg(Bound) + } + + "accept two acceptable connections at once and register them with the manager" in { + new Socket("localhost", port) + new Socket("localhost", port) + new Socket("localhost", port) + listener ! ChannelAcceptable + val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) + val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) + } + + "accept one more connection and register it with the manager" in { + listener ! ChannelAcceptable + val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) + } + + "react to Unbind commands by closing the ServerSocketChannel, replying with Unbound and stopping itself" in { + implicit val timeout: Timeout = 1 second span + listener.ask(Unbind).value must equal(Some(Success(Unbound))) + serverSocketChannel.get.isOpen must equal(false) + listener.isTerminated must equal(true) + } + } + +} \ No newline at end of file From e22c80655d861ac996ef56ced2e56222cf7e914d Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 16 Jan 2013 16:59:55 +0100 Subject: [PATCH 10/66] refactor tests to reuse common connection setup --- .../scala/akka/io/TcpConnectionSpec.scala | 125 ++++++++++-------- 1 file changed, 68 insertions(+), 57 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 31da412b33..934cdf3fd8 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -32,7 +32,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val userHandler = TestProbe() val selector = TestProbe() val connectionActor = - createConnectionActor(selector.ref, userHandler.ref, options = Vector(SO.ReuseAddress(true))) + createConnectionActor(options = Vector(SO.ReuseAddress(true)))(selector.ref, userHandler.ref) val clientChannel = connectionActor.underlyingActor.channel clientChannel.socket.getReuseAddress must be(true) } @@ -41,7 +41,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val userHandler = TestProbe() val selector = TestProbe() val connectionActor = - createConnectionActor(selector.ref, userHandler.ref, options = Vector(SO.KeepAlive(true))) + createConnectionActor(options = Vector(SO.KeepAlive(true)))(selector.ref, userHandler.ref) val clientChannel = connectionActor.underlyingActor.channel clientChannel.socket.getKeepAlive must be(false) // only set after connection is established selector.send(connectionActor, ChannelConnectable) @@ -228,15 +228,11 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } // error conditions - "report failed connection attempt while not registered" in withLocalServer() { localServer ⇒ - val userHandler = TestProbe() - val selector = TestProbe() - val connectionActor = createConnectionActor(selector.ref, userHandler.ref) - val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) - + "report failed connection attempt while not accepted" in withUnacceptedConnection() { setup ⇒ + import setup._ // close instead of accept localServer.close() + selector.send(connectionActor, ChannelConnectable) userHandler.expectMsgPF() { case ErrorClose(e) ⇒ e.getMessage must be("Connection reset by peer") @@ -245,31 +241,27 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertActorTerminated(connectionActor) } - "report failed connection attempt when target is unreachable" in { - val userHandler = TestProbe() - val selector = TestProbe() - val connectionActor = createConnectionActor(selector.ref, userHandler.ref, serverAddress = new InetSocketAddress("127.0.0.1", 63186)) - val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) - val sel = SelectorProvider.provider().openSelector() - val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) - sel.select(200) + val UnknownAddress = new InetSocketAddress("127.0.0.1", 63186) + "report failed connection attempt when target is unreachable" in + withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnknownAddress)) { setup ⇒ + import setup._ - key.isConnectable must be(true) - selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgPF() { - case ErrorClose(e) ⇒ e.getMessage must be("Connection refused") + val sel = SelectorProvider.provider().openSelector() + val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) + sel.select(200) + + key.isConnectable must be(true) + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsgPF() { + case ErrorClose(e) ⇒ e.getMessage must be("Connection refused") + } + + assertActorTerminated(connectionActor) } - assertActorTerminated(connectionActor) - } + "time out when Connected isn't answered with Register" in withUnacceptedConnection() { setup ⇒ + import setup._ - "time out when Connected isn't answered with Register" in withLocalServer() { localServer ⇒ - val userHandler = TestProbe() - val selector = TestProbe() - val connectionActor = createConnectionActor(selector.ref, userHandler.ref) - val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) localServer.accept() selector.send(connectionActor, ChannelConnectable) userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) @@ -277,15 +269,12 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertActorTerminated(connectionActor) } - "close the connection when user handler dies while connecting" in withLocalServer() { localServer ⇒ - val userHandler = system.actorOf(Props(new Actor { - def receive = PartialFunction.empty - })) - val selector = TestProbe() - val connectionActor = createConnectionActor(selector.ref, userHandler) - val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) - system.stop(userHandler) + "close the connection when user handler dies while connecting" in withUnacceptedConnection() { setup ⇒ + import setup._ + + // simulate death of userHandler test probe + userHandler.send(connectionActor, akka.actor.Terminated(userHandler.ref)(false, false)) + assertActorTerminated(connectionActor) } @@ -309,14 +298,22 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } finally localServer.close() } - case class Setup( + case class UnacceptedSetup( + localServer: ServerSocketChannel, userHandler: TestProbe, - connectionHandler: TestProbe, selector: TestProbe, connectionActor: TestActorRef[TcpOutgoingConnection], - clientSideChannel: SocketChannel, + clientSideChannel: SocketChannel) + case class RegisteredSetup( + unregisteredSetup: UnacceptedSetup, + connectionHandler: TestProbe, serverSideChannel: SocketChannel) { + def userHandler: TestProbe = unregisteredSetup.userHandler + def selector: TestProbe = unregisteredSetup.selector + def connectionActor: TestActorRef[TcpOutgoingConnection] = unregisteredSetup.connectionActor + def clientSideChannel: SocketChannel = unregisteredSetup.clientSideChannel + val buffer = ByteBuffer.allocate(TestSize) @tailrec final def pullFromServerSide(remaining: Int): Unit = if (remaining > 0) { @@ -339,14 +336,29 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") clientSideChannel must not be ('open) } } - def withEstablishedConnection(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: Setup ⇒ Any): Unit = withLocalServer(setServerSocketOptions) { localServer ⇒ - val userHandler = TestProbe() - val connectionHandler = TestProbe() - val selector = TestProbe() - val connectionActor = createConnectionActor(selector.ref, userHandler.ref) - val clientSideChannel = connectionActor.underlyingActor.channel + def withUnacceptedConnection( + setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ (), + connectionActorCons: (ActorRef, ActorRef) ⇒ TestActorRef[TcpOutgoingConnection] = createConnectionActor())(body: UnacceptedSetup ⇒ Any): Unit = - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) + withLocalServer(setServerSocketOptions) { localServer ⇒ + val userHandler = TestProbe() + val selector = TestProbe() + val connectionActor = connectionActorCons(selector.ref, userHandler.ref) + val clientSideChannel = connectionActor.underlyingActor.channel + + selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) + + body { + UnacceptedSetup( + localServer, + userHandler, + selector, + connectionActor, + clientSideChannel) + } + } + def withEstablishedConnection(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: RegisteredSetup ⇒ Any): Unit = withUnacceptedConnection(setServerSocketOptions) { unregisteredSetup ⇒ + import unregisteredSetup._ localServer.configureBlocking(true) val serverSideChannel = localServer.accept() @@ -354,16 +366,15 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") serverSideChannel must not be (null) selector.send(connectionActor, ChannelConnectable) userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) + + val connectionHandler = TestProbe() userHandler.send(connectionActor, Register(connectionHandler.ref)) selector.expectMsg(ReadInterest) body { - Setup( - userHandler, + RegisteredSetup( + unregisteredSetup, connectionHandler, - selector, - connectionActor, - clientSideChannel, serverSideChannel) } } @@ -377,11 +388,11 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") channel.socket.setReceiveBufferSize(1024) def createConnectionActor( - selector: ActorRef, - commander: ActorRef, serverAddress: InetSocketAddress = serverAddress, localAddress: Option[InetSocketAddress] = None, - options: immutable.Seq[Tcp.SocketOption] = Nil): TestActorRef[TcpOutgoingConnection] = { + options: immutable.Seq[Tcp.SocketOption] = Nil)( + selector: ActorRef, + commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { TestActorRef( new TcpOutgoingConnection(selector, commander, serverAddress, localAddress, options) { From e11c3fe6bbbb5b184f04679afbcdef6a340f8739 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 16 Jan 2013 16:57:56 +0100 Subject: [PATCH 11/66] fix assertion error typo --- akka-io/src/main/scala/akka/io/Tcp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 7d0421f42a..c3f49a762e 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -97,7 +97,7 @@ object Tcp extends ExtensionKey[TcpExt] { * For more information see [[java.net.Socket.setSendBufferSize]] */ case class SendBufferSize(size: Int) extends SocketOption { - require(size > 0, "ReceiveBufferSize must be > 0") + require(size > 0, "SendBufferSize must be > 0") override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) } From 18aecef4bd5ad90f4db945a4a0bb8def4ede6928 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 17 Jan 2013 14:45:50 +0100 Subject: [PATCH 12/66] fix issues discussed in the pull request --- akka-io/src/main/resources/reference.conf | 9 +- akka-io/src/main/scala/akka/io/Tcp.scala | 19 +++- .../main/scala/akka/io/TcpConnection.scala | 106 ++++++++++++------ .../scala/akka/io/TcpConnectionSpec.scala | 70 ++++++------ 4 files changed, 131 insertions(+), 73 deletions(-) diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 6104ac96c5..c48a246f5d 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -48,15 +48,20 @@ akka { # the worker-dispatcher batch-accept-limit = 10 - # The size of the thread-local direct buffers used to read or write + # The number of bytes per thread-local direct buffer used to read or write # network data from the kernel. Those buffer directly add to the footprint - # of the threads from the dispatcher tcp connection actors are using. + # of all threads from the dispatcher which TCP connection actors are using. direct-buffer-size = 524288 # The duration a connection actor waits for a `Register` message from # its commander before aborting the connection. register-timeout = 5s + # Enable fine grained logging of what goes on inside the implementation. + # Be aware that this may log more than once per message sent to the actors + # of the tcp implementation. + trace-logging = off + # Fully qualified config path which holds the dispatcher configuration # to be used for running the select() calls in the selectors selector-dispatcher = "akka.io.pinned-dispatcher" diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index c3f49a762e..af2c30d0e2 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -152,11 +152,21 @@ object Tcp extends ExtensionKey[TcpExt] { case object ConfirmedClose extends CloseCommand case object Abort extends CloseCommand - case class Write(data: ByteString, ack: AnyRef) extends Command + case object NoAck + + /** + * Write data to the TCP connection. If no ack is needed use the special + * `NoAck` object. + */ + case class Write(data: ByteString, ack: AnyRef) extends Command { + require(ack ne null, "ack must be non-null. Use NoAck if you don't want acks.") + + def wantsAck: Boolean = ack ne NoAck + } object Write { - val Empty: Write = Write(ByteString.empty, null) + val Empty: Write = Write(ByteString.empty, NoAck) def apply(data: ByteString): Write = - if (data.isEmpty) Empty else Write(data, null) + if (data.isEmpty) Empty else Write(data, NoAck) } case object StopReading extends Command @@ -178,7 +188,7 @@ object Tcp extends ExtensionKey[TcpExt] { case object Aborted extends ConnectionClosed case object ConfirmedClosed extends ConnectionClosed case object PeerClosed extends ConnectionClosed - case class ErrorClose(cause: Throwable) extends ConnectionClosed + case class ErrorClose(cause: String) extends ConnectionClosed /// INTERNAL case class RegisterOutgoingConnection(channel: SocketChannel) @@ -216,6 +226,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val SelectorDispatcher = getString("selector-dispatcher") val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") + val TraceLogging = getBoolean("trace-logging") require(NrOfSelectors > 0, "nr-of-selectors must be > 0") require(MaxChannels >= 0, "max-channels must be >= 0") diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 707e1664de..f7589dd6d3 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -13,26 +13,35 @@ import scala.concurrent.duration._ import akka.actor._ import akka.util.ByteString import Tcp._ +import annotation.tailrec /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ abstract class TcpConnection(val selector: ActorRef, val channel: SocketChannel) extends Actor with ThreadLocalDirectBuffer with ActorLogging { + val tcp = Tcp(context.system) channel.configureBlocking(false) var pendingWrite: Write = Write.Empty // a write "queue" of size 1 for holding one unfinished write command + var pendingWriteCommander: ActorRef = null + + // Needed to send the ConnectionClosed message in the postStop handler. + // First element is the handler, second the particular close message. + var closedMessage: (ActorRef, ConnectionClosed) = null + def writePending = pendingWrite ne Write.Empty - def registerTimeout = Tcp(context.system).Settings.RegisterTimeout + def registerTimeout = tcp.Settings.RegisterTimeout + def traceLoggingEnabled = tcp.Settings.TraceLogging // STATES /** connection established, waiting for registration from user handler */ def waitingForRegistration(commander: ActorRef): Receive = { case Register(handler) ⇒ - log.debug("{} registered as connection handler", handler) + if (traceLoggingEnabled) log.debug("{} registered as connection handler", handler) selector ! ReadInterest context.setReceiveTimeout(Duration.Undefined) @@ -44,8 +53,8 @@ abstract class TcpConnection(val selector: ActorRef, handleClose(commander, closeResponse(cmd)) case ReceiveTimeout ⇒ - // TODO: just shutting down, as we do here, presents a race condition to the user - // Should we introduce a dedicated `Registered` event message to notify the user of successful registration? + // after sending `Register` user should watch this actor to make sure + // it didn't die because of the timeout log.warning("Configured registration timeout of {} expired, stopping", registerTimeout) context.stop(self) } @@ -57,11 +66,18 @@ abstract class TcpConnection(val selector: ActorRef, case ChannelReadable ⇒ doRead(handler) case write: Write if writePending ⇒ - log.debug("Dropping write because queue is full") - handler ! CommandFailed(write) + if (traceLoggingEnabled) log.debug("Dropping write because queue is full") + sender ! CommandFailed(write) - case write: Write ⇒ doWrite(handler, write) - case ChannelWritable ⇒ doWrite(handler, pendingWrite) + case write: Write if write.data.isEmpty ⇒ + if (write.wantsAck) + sender ! write.ack + + case write: Write ⇒ + pendingWriteCommander = sender + pendingWrite = write + doWrite(handler) + case ChannelWritable ⇒ doWrite(handler) case cmd: CloseCommand ⇒ handleClose(handler, closeResponse(cmd)) } @@ -73,7 +89,7 @@ abstract class TcpConnection(val selector: ActorRef, case ChannelReadable ⇒ doRead(handler) case ChannelWritable ⇒ - doWrite(handler, pendingWrite) + doWrite(handler) if (!writePending) // writing is now finished handleClose(handler, closedEvent) @@ -111,17 +127,17 @@ abstract class TcpConnection(val selector: ActorRef, buffer.flip() if (readBytes > 0) { - log.debug("Read {} bytes", readBytes) - handler ! Received(ByteString(buffer).take(readBytes)) + if (traceLoggingEnabled) log.debug("Read {} bytes", readBytes) + handler ! Received(ByteString(buffer)) if (readBytes == buffer.capacity()) // directly try reading more because we exhausted our buffer self ! ChannelReadable else selector ! ReadInterest } else if (readBytes == 0) { - log.debug("Read nothing. Registering read interest with selector") + if (traceLoggingEnabled) log.debug("Read nothing. Registering read interest with selector") selector ! ReadInterest } else if (readBytes == -1) { - log.debug("Read returned end-of-stream") + if (traceLoggingEnabled) log.debug("Read returned end-of-stream") doCloseConnection(handler, closeReason) } else throw new IllegalStateException("Unexpected value returned from read: " + readBytes) @@ -130,7 +146,8 @@ abstract class TcpConnection(val selector: ActorRef, } } - def doWrite(handler: ActorRef, write: Write): Unit = { + def doWrite(handler: ActorRef): Unit = { + val write = pendingWrite val data = write.data val buffer = directBuffer() @@ -138,13 +155,15 @@ abstract class TcpConnection(val selector: ActorRef, buffer.flip() try { - log.debug("Trying to write to channel") val writtenBytes = channel.write(buffer) - log.debug("Wrote {} bytes", writtenBytes) + if (traceLoggingEnabled) log.debug("Wrote {} bytes", writtenBytes) pendingWrite = consume(write, writtenBytes) if (writePending) selector ! WriteInterest // still data to write - else if (write.ack != null) handler ! write.ack // everything written + else if (write.wantsAck) { + pendingWriteCommander ! write.ack + pendingWriteCommander = null + } // everything written } catch { case e: IOException ⇒ handleError(handler, e) } @@ -156,20 +175,20 @@ abstract class TcpConnection(val selector: ActorRef, def handleClose(handler: ActorRef, closedEvent: ConnectionClosed): Unit = if (closedEvent == Aborted) { // close instantly - log.debug("Got Abort command. RESETing connection.") + if (traceLoggingEnabled) log.debug("Got Abort command. RESETing connection.") doCloseConnection(handler, closedEvent) } else if (writePending) { // finish writing first - log.debug("Got Close command but write is still pending.") + if (traceLoggingEnabled) log.debug("Got Close command but write is still pending.") context.become(closingWithPendingWrite(handler, closedEvent)) } else if (closedEvent == ConfirmedClosed) { // shutdown output and wait for confirmation - log.debug("Got ConfirmedClose command, sending FIN.") + if (traceLoggingEnabled) log.debug("Got ConfirmedClose command, sending FIN.") channel.socket.shutdownOutput() context.become(closing(handler)) } else { // close now - log.debug("Got Close command, closing connection.") + if (traceLoggingEnabled) log.debug("Got Close command, closing connection.") doCloseConnection(handler, closedEvent) } @@ -177,7 +196,8 @@ abstract class TcpConnection(val selector: ActorRef, if (closedEvent == Aborted) abort() else channel.close() - handler ! closedEvent + closedMessage = (handler, closedEvent) + context.stop(self) } @@ -189,10 +209,18 @@ abstract class TcpConnection(val selector: ActorRef, } def handleError(handler: ActorRef, exception: IOException): Unit = { - exception.setStackTrace(Array.empty) - handler ! ErrorClose(exception) + closedMessage = (handler, ErrorClose(extractMsg(exception))) + throw exception } + @tailrec private[this] def extractMsg(t: Throwable): String = + if (t == null) "unknown" + else { + t.getMessage match { + case null | "" ⇒ extractMsg(t.getCause) + case msg ⇒ msg + } + } def abort(): Unit = { try channel.socket.setSoLinger(true, 0) // causes the following close() to send TCP RST @@ -200,26 +228,34 @@ abstract class TcpConnection(val selector: ActorRef, case NonFatal(e) ⇒ // setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574 // (also affected: OS/X Java 1.6.0_37) - log.debug("setSoLinger(true, 0) failed with {}", e) + if (traceLoggingEnabled) log.debug("setSoLinger(true, 0) failed with {}", e) } channel.close() } - override def postStop(): Unit = + override def postStop(): Unit = { + if (closedMessage != null) { + val msg = closedMessage._2 + closedMessage._1 ! msg + + if (writePending) + pendingWriteCommander ! msg + } + if (channel.isOpen) abort() + } + + override def postRestart(reason: Throwable): Unit = + throw new IllegalStateException("Restarting not supported for connection actors.") /** Returns a new write with `numBytes` removed from the front */ def consume(write: Write, numBytes: Int): Write = - write match { - case Write.Empty if numBytes == 0 ⇒ write + numBytes match { + case 0 ⇒ write + case x if x == write.data.length ⇒ Write.Empty case _ ⇒ - numBytes match { - case 0 ⇒ write - case x if x == write.data.length ⇒ Write.Empty - case _ ⇒ - require(numBytes > 0 && numBytes < write.data.length) - write.copy(data = write.data.drop(numBytes)) - } + require(numBytes > 0 && numBytes < write.data.length) + write.copy(data = write.data.drop(numBytes)) } } diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 934cdf3fd8..6dd5810e17 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -53,29 +53,32 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII"))) // emulate selector behavior selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgPF(remaining) { - case Received(data) if data.decodeString("ASCII") == "testdata" ⇒ - } + connectionHandler.expectMsgType[Received].data.decodeString("ASCII") must be("testdata") // have two packets in flight before the selector notices serverSideChannel.write(ByteBuffer.wrap("testdata2".getBytes("ASCII"))) serverSideChannel.write(ByteBuffer.wrap("testdata3".getBytes("ASCII"))) selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgPF(remaining) { - case Received(data) if data.decodeString("ASCII") == "testdata2testdata3" ⇒ - } + connectionHandler.expectMsgType[Received].data.decodeString("ASCII") must be("testdata2testdata3") } "write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒ import setup._ serverSideChannel.configureBlocking(false) + object Ack + val writer = TestProbe() + + // directly acknowledge an empty write + writer.send(connectionActor, Write(ByteString.empty, Ack)) + writer.expectMsg(Ack) + val write = Write(ByteString("testdata"), Ack) val buffer = ByteBuffer.allocate(100) serverSideChannel.read(buffer) must be(0) - // emulate selector behavior - connectionHandler.send(connectionActor, write) - connectionHandler.expectMsg(Ack) + writer.send(connectionActor, write) + // make sure the writer gets the ack + writer.expectMsg(Ack) serverSideChannel.read(buffer) must be(8) buffer.flip() ByteString(buffer).take(8).decodeString("ASCII") must be("testdata") @@ -90,6 +93,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") //serverSideChannel.configureBlocking(false) clientSideChannel.socket.setSendBufferSize(1024) + val writer = TestProbe() + // producing backpressure by sending much more than currently fits into // our send buffer val firstWrite = writeCmd(Ack1) @@ -97,14 +102,18 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // try to write the buffer but since the SO_SNDBUF is too small // it will have to keep the rest of the piece and send it // when possible - connectionHandler.send(connectionActor, firstWrite) + writer.send(connectionActor, firstWrite) selector.expectMsg(WriteInterest) // send another write which should fail immediately // because we don't store more than one piece in flight val secondWrite = writeCmd(Ack2) - connectionHandler.send(connectionActor, secondWrite) - connectionHandler.expectMsg(CommandFailed(secondWrite)) + writer.send(connectionActor, secondWrite) + writer.expectMsg(CommandFailed(secondWrite)) + + // reject even empty writes + writer.send(connectionActor, Write.Empty) + writer.expectMsg(CommandFailed(Write.Empty)) // there will be immediately more space in the send buffer because // some data will have been sent by now, so we assume we can write @@ -113,8 +122,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // both buffers should now be filled so no more writing // is possible - setup.pullFromServerSide(TestSize) - connectionHandler.expectMsg(Ack1) + pullFromServerSide(TestSize) + writer.expectMsg(Ack1) } "respect StopReading and ResumeReading" in withEstablishedConnection() { setup ⇒ @@ -141,10 +150,10 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.send(connectionActor, writeCmd(Ack)) connectionHandler.send(connectionActor, Close) - setup.pullFromServerSide(TestSize) + pullFromServerSide(TestSize) connectionHandler.expectMsg(Ack) connectionHandler.expectMsg(Closed) - connectionActor.isTerminated must be(true) + assertThisConnectionActorTerminated() val buffer = ByteBuffer.allocate(1) serverSideChannel.read(buffer) must be(-1) @@ -177,7 +186,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.send(connectionActor, ConfirmedClose) connectionHandler.expectNoMsg(100.millis) - setup.pullFromServerSide(TestSize) + pullFromServerSide(TestSize) connectionHandler.expectMsg(Ack) selector.send(connectionActor, ChannelReadable) @@ -207,9 +216,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") abortClose(serverSideChannel) selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgPF(remaining) { - case ErrorClose(exc: IOException) ⇒ exc.getMessage must be("Connection reset by peer") - } + connectionHandler.expectMsgType[ErrorClose].cause must be("Connection reset by peer") // wait a while connectionHandler.expectNoMsg(200.millis) @@ -218,11 +225,13 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") "report when peer closed the connection when trying to write" in withEstablishedConnection() { setup ⇒ import setup._ + val writer = TestProbe() + abortClose(serverSideChannel) - connectionHandler.send(connectionActor, Write(ByteString("testdata"))) - connectionHandler.expectMsgPF(remaining) { - case ErrorClose(_: IOException) ⇒ // ok - } + writer.send(connectionActor, Write(ByteString("testdata"))) + // bother writer and handler should get the message + writer.expectMsgType[ErrorClose] + connectionHandler.expectMsgType[ErrorClose] assertThisConnectionActorTerminated() } @@ -234,9 +243,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") localServer.close() selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgPF() { - case ErrorClose(e) ⇒ e.getMessage must be("Connection reset by peer") - } + userHandler.expectMsgType[ErrorClose].cause must be("Connection reset by peer") assertActorTerminated(connectionActor) } @@ -252,9 +259,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") key.isConnectable must be(true) selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgPF() { - case ErrorClose(e) ⇒ e.getMessage must be("Connection refused") - } + userHandler.expectMsgType[ErrorClose].cause must be("Connection refused") assertActorTerminated(connectionActor) } @@ -419,7 +424,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") channel.close() } def assertActorTerminated(connectionActor: TestActorRef[TcpOutgoingConnection]): Unit = { - watch(connectionActor) - expectMsgType[Terminated].actor must be(connectionActor) + val watcher = TestProbe() + watcher.watch(connectionActor) + watcher.expectMsgType[Terminated].actor must be(connectionActor) } } From 7d89aefb634e6dd2c31ed8f13b6963be4f338352 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 17 Jan 2013 14:31:35 +0100 Subject: [PATCH 13/66] use a direct buffer pool for buffers needed for channel.read/write The major advantage of this approach is that a Write is only copied once into its direct buffer and this direct buffer is kept until it is written fully. --- akka-io/src/main/resources/reference.conf | 11 +-- .../scala/akka/io/DirectByteBufferPool.scala | 67 +++++++++++++++++++ akka-io/src/main/scala/akka/io/Tcp.scala | 2 + .../main/scala/akka/io/TcpConnection.scala | 62 ++++++++--------- .../akka/io/ThreadLocalDirectBuffer.scala | 32 --------- 5 files changed, 105 insertions(+), 69 deletions(-) create mode 100644 akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala delete mode 100644 akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index c48a246f5d..3595cd12f5 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -48,10 +48,13 @@ akka { # the worker-dispatcher batch-accept-limit = 10 - # The number of bytes per thread-local direct buffer used to read or write - # network data from the kernel. Those buffer directly add to the footprint - # of all threads from the dispatcher which TCP connection actors are using. - direct-buffer-size = 524288 + # The number of bytes per direct buffer in the pool used to read or write + # network data from the kernel. + direct-buffer-size = 131072 + + # The maximal number of direct buffers kept in the direct buffer pool for + # reuse. + max-direct-buffer-pool-size = 1000 # The duration a connection actor waits for a `Register` message from # its commander before aborting the connection. diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala new file mode 100644 index 0000000000..4fd558a162 --- /dev/null +++ b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -0,0 +1,67 @@ +package akka.io + +import java.util.concurrent.atomic.AtomicInteger +import java.nio.ByteBuffer +import annotation.tailrec + +trait WithBufferPool { + def tcp: TcpExt + + def acquireBuffer(): ByteBuffer = + tcp.bufferPool.acquire() + + def acquireBuffer(size: Int): ByteBuffer = + tcp.bufferPool.acquire(size) + + def releaseBuffer(buffer: ByteBuffer) = + tcp.bufferPool.release(buffer) +} + +/** + * A buffer pool which keeps direct buffers of a specified default size. + * If a buffer bigger than the default size is requested it is created + * but will not be pooled on release. + * + * This implementation is very loosely based on the one from Netty. + */ +class DirectByteBufferPool(bufferSize: Int, maxPoolSize: Int) { + private val Unlocked = 0 + private val Locked = 1 + + private[this] val state = new AtomicInteger(Unlocked) + @volatile private[this] var pool: List[ByteBuffer] = Nil + @volatile private[this] var poolSize: Int = 0 + + private[this] def allocate(size: Int): ByteBuffer = + ByteBuffer.allocateDirect(size) + + def acquire(size: Int = bufferSize): ByteBuffer = { + if (poolSize == 0 || size > bufferSize) allocate(size) + else takeBufferFromPool() + } + + def release(buf: ByteBuffer): Unit = + if (buf.capacity() <= bufferSize && poolSize < maxPoolSize) + addBufferToPool(buf) + + @tailrec + final def takeBufferFromPool(): ByteBuffer = + if (state.compareAndSet(Unlocked, Locked)) + try pool match { + case Nil ⇒ allocate(bufferSize) // we have no more buffer available, so create a new one + case buf :: tail ⇒ + pool = tail + poolSize -= 1 + buf + } finally state.set(Unlocked) + else takeBufferFromPool() // spin while locked + + @tailrec + final def addBufferToPool(buf: ByteBuffer): Unit = + if (state.compareAndSet(Unlocked, Locked)) { + buf.clear() // ensure that we never have dirty buffers in the pool + pool = buf :: pool + poolSize += 1 + state.set(Unlocked) + } else addBufferToPool(buf) // spin while locked +} diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index af2c30d0e2..4107ee3e82 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -220,6 +220,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val SelectorAssociationRetries = getInt("selector-association-retries") val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getInt("direct-buffer-size") + val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") val RegisterTimeout = if (getString("register-timeout") == "infinite") Duration.Undefined else Duration(getMilliseconds("register-timeout"), MILLISECONDS) @@ -240,4 +241,5 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( Props.empty.withDispatcher(Settings.ManagementDispatcher), "IO-TCP") + val bufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) } diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index f7589dd6d3..03069f0d01 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -14,24 +14,24 @@ import akka.actor._ import akka.util.ByteString import Tcp._ import annotation.tailrec +import java.nio.ByteBuffer /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ abstract class TcpConnection(val selector: ActorRef, - val channel: SocketChannel) extends Actor with ThreadLocalDirectBuffer with ActorLogging { + val channel: SocketChannel) extends Actor with ActorLogging with WithBufferPool { val tcp = Tcp(context.system) channel.configureBlocking(false) - var pendingWrite: Write = Write.Empty // a write "queue" of size 1 for holding one unfinished write command - var pendingWriteCommander: ActorRef = null + var pendingWrite: PendingWrite = null // Needed to send the ConnectionClosed message in the postStop handler. // First element is the handler, second the particular close message. var closedMessage: (ActorRef, ConnectionClosed) = null - def writePending = pendingWrite ne Write.Empty + def writePending = pendingWrite ne null def registerTimeout = tcp.Settings.RegisterTimeout def traceLoggingEnabled = tcp.Settings.TraceLogging @@ -74,8 +74,8 @@ abstract class TcpConnection(val selector: ActorRef, sender ! write.ack case write: Write ⇒ - pendingWriteCommander = sender - pendingWrite = write + pendingWrite = createWrite(write) + doWrite(handler) case ChannelWritable ⇒ doWrite(handler) @@ -119,16 +119,17 @@ abstract class TcpConnection(val selector: ActorRef, } def doRead(handler: ActorRef): Unit = { - val buffer = directBuffer() + val buffer = acquireBuffer() try { - log.debug("Trying to read from channel") val readBytes = channel.read(buffer) buffer.flip() if (readBytes > 0) { if (traceLoggingEnabled) log.debug("Read {} bytes", readBytes) handler ! Received(ByteString(buffer)) + releaseBuffer(buffer) + if (readBytes == buffer.capacity()) // directly try reading more because we exhausted our buffer self ! ChannelReadable @@ -147,23 +148,16 @@ abstract class TcpConnection(val selector: ActorRef, } def doWrite(handler: ActorRef): Unit = { - val write = pendingWrite - val data = write.data - - val buffer = directBuffer() - data.copyToBuffer(buffer) - buffer.flip() - try { - val writtenBytes = channel.write(buffer) - if (traceLoggingEnabled) log.debug("Wrote {} bytes", writtenBytes) - pendingWrite = consume(write, writtenBytes) + val writtenBytes = channel.write(pendingWrite.buffer) + if (traceLoggingEnabled) log.debug("Wrote {} bytes to channel", writtenBytes) - if (writePending) selector ! WriteInterest // still data to write - else if (write.wantsAck) { - pendingWriteCommander ! write.ack - pendingWriteCommander = null - } // everything written + if (pendingWrite.hasData) selector ! WriteInterest // still data to write + else if (pendingWrite.wantsAck) { // everything written + pendingWrite.commander ! pendingWrite.ack + releaseBuffer(pendingWrite.buffer) + pendingWrite = null + } } catch { case e: IOException ⇒ handleError(handler, e) } @@ -239,7 +233,7 @@ abstract class TcpConnection(val selector: ActorRef, closedMessage._1 ! msg if (writePending) - pendingWriteCommander ! msg + pendingWrite.commander ! msg } if (channel.isOpen) @@ -249,13 +243,15 @@ abstract class TcpConnection(val selector: ActorRef, override def postRestart(reason: Throwable): Unit = throw new IllegalStateException("Restarting not supported for connection actors.") - /** Returns a new write with `numBytes` removed from the front */ - def consume(write: Write, numBytes: Int): Write = - numBytes match { - case 0 ⇒ write - case x if x == write.data.length ⇒ Write.Empty - case _ ⇒ - require(numBytes > 0 && numBytes < write.data.length) - write.copy(data = write.data.drop(numBytes)) - } + private[TcpConnection] case class PendingWrite(commander: ActorRef, ack: AnyRef, buffer: ByteBuffer) { + def hasData = buffer.remaining() > 0 + def wantsAck = ack ne NoAck + } + def createWrite(write: Write): PendingWrite = { + val buffer = acquireBuffer(write.data.length) + write.data.copyToBuffer(buffer) + buffer.flip() + + PendingWrite(sender, write.ack, buffer) + } } diff --git a/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala b/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala deleted file mode 100644 index ab0b38510c..0000000000 --- a/akka-io/src/main/scala/akka/io/ThreadLocalDirectBuffer.scala +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (C) 2009-2013 Typesafe Inc. - */ - -package akka.io - -import java.nio.ByteBuffer -import akka.actor.Actor - -/** - * Allows an actor to get a thread local direct buffer of a size defined in the - * configuration of the actor system. An underlying assumption is that all of - * the threads which call `getDirectBuffer` are owned by the actor system. - */ -trait ThreadLocalDirectBuffer { _: Actor ⇒ - def directBuffer(): ByteBuffer = { - val result = ThreadLocalDirectBuffer.threadLocalBuffer.get() - if (result == null) { - val size = Tcp(context.system).Settings.DirectBufferSize - val newBuffer = ByteBuffer.allocateDirect(size) - ThreadLocalDirectBuffer.threadLocalBuffer.set(newBuffer) - newBuffer - } else { - result.clear() - result - } - } -} - -object ThreadLocalDirectBuffer { - private val threadLocalBuffer = new ThreadLocal[ByteBuffer] -} From 9bcca4003a967b1acab3474081e6026f8c505e90 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 17 Jan 2013 15:07:00 +0100 Subject: [PATCH 14/66] in tests make sure to get a server address that is highly probable to be currently unbound --- .../src/test/scala/akka/io/TcpConnectionSpec.scala | 11 ++++------- .../src/test/scala/akka/io/TcpListenerSpec.scala | 12 +++++------- .../scala/akka/io/TemporaryServerAddress.scala | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 6dd5810e17..0265781c90 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -14,16 +14,13 @@ import java.net._ import scala.collection.immutable import scala.concurrent.duration._ import scala.util.control.NonFatal -import akka.actor.{ ActorRef, Props, Actor, Terminated } +import akka.actor.{ ActorRef, Terminated } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import akka.util.ByteString import Tcp._ -import java.util.concurrent.CountDownLatch class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { - val port = 45679 - val localhost = InetAddress.getLocalHost - val serverAddress = new InetSocketAddress(localhost, port) + val serverAddress = TemporaryServerAddress.get("127.0.0.1") "An outgoing connection" must { // common behavior @@ -248,9 +245,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertActorTerminated(connectionActor) } - val UnknownAddress = new InetSocketAddress("127.0.0.1", 63186) + val UnboundAddress = TemporaryServerAddress.get("127.0.0.1") "report failed connection attempt when target is unreachable" in - withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnknownAddress)) { setup ⇒ + withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ import setup._ val sel = SelectorProvider.provider().openSelector() diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index addf9f7f16..0619038c54 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -14,15 +14,13 @@ import akka.pattern.ask import Tcp._ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { - val port = 47323 - "A TcpListener" must { val manager = TestProbe() val selector = TestProbe() val handler = TestProbe() val handlerRef = handler.ref val bindCommander = TestProbe() - val endpoint = new InetSocketAddress("localhost", port) + val endpoint = TemporaryServerAddress.get("127.0.0.1") val listener = TestActorRef(new TcpListener(manager.ref, selector.ref, handler.ref, endpoint, 100, bindCommander.ref, Nil)) var serverSocketChannel: Option[ServerSocketChannel] = None @@ -38,9 +36,9 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "accept two acceptable connections at once and register them with the manager" in { - new Socket("localhost", port) - new Socket("localhost", port) - new Socket("localhost", port) + new Socket("localhost", endpoint.getPort) + new Socket("localhost", endpoint.getPort) + new Socket("localhost", endpoint.getPort) listener ! ChannelAcceptable val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) @@ -59,4 +57,4 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } } -} \ No newline at end of file +} diff --git a/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala b/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala new file mode 100644 index 0000000000..f006ed70e5 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala @@ -0,0 +1,14 @@ +package akka.io + +import java.nio.channels.ServerSocketChannel +import java.net.InetSocketAddress + +object TemporaryServerAddress { + def get(address: String): InetSocketAddress = { + val serverSocket = ServerSocketChannel.open() + serverSocket.socket.bind(new InetSocketAddress(address, 0)) + val port = serverSocket.socket.getLocalPort + serverSocket.close() + new InetSocketAddress(address, port) + } +} From 7384e07d9b2317becf9452e4c611e35ee15b9fe5 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 17 Jan 2013 17:29:44 +0100 Subject: [PATCH 15/66] improvements from the first round of feedback, see #2885 and #2887 --- akka-io/src/main/resources/reference.conf | 8 +- .../scala/akka/io/DirectByteBufferPool.scala | 10 +- akka-io/src/main/scala/akka/io/Tcp.scala | 49 +++--- .../main/scala/akka/io/TcpConnection.scala | 39 ++--- .../scala/akka/io/TcpIncomingConnection.scala | 4 +- .../src/main/scala/akka/io/TcpListener.scala | 32 ++-- .../src/main/scala/akka/io/TcpManager.scala | 35 +--- .../scala/akka/io/TcpOutgoingConnection.scala | 16 +- .../src/main/scala/akka/io/TcpSelector.scala | 154 ++++++++++-------- .../scala/akka/io/TcpConnectionSpec.scala | 6 +- .../test/scala/akka/io/TcpListenerSpec.scala | 99 +++++++---- .../akka/io/TemporaryServerAddress.scala | 7 +- 12 files changed, 248 insertions(+), 211 deletions(-) diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 3595cd12f5..a907706b02 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -23,10 +23,12 @@ akka { # these will use one select loop on the selector-dispatcher. nr-of-selectors = 1 - # Maximum number of open channels supported by this TCP module; there is + # Maximum number of open channels supported by this TCP module; there is # no intrinsic general limit, this setting is meant to enable DoS - # protection by limiting the number of concurrently connected clients. - # Set to 0 to disable. + # protection by limiting the number of concurrently connected clients. + # Also note that this is a "soft" limit; in certain cases the implementation + # will accept a few connections more than the number configured here. + # Set to 0 for "unlimited". max-channels = 256000 # The select loop can be used in two modes: diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala index 4fd558a162..3a1f00b20e 100644 --- a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala +++ b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -32,7 +32,7 @@ class DirectByteBufferPool(bufferSize: Int, maxPoolSize: Int) { @volatile private[this] var pool: List[ByteBuffer] = Nil @volatile private[this] var poolSize: Int = 0 - private[this] def allocate(size: Int): ByteBuffer = + private def allocate(size: Int): ByteBuffer = ByteBuffer.allocateDirect(size) def acquire(size: Int = bufferSize): ByteBuffer = { @@ -44,8 +44,12 @@ class DirectByteBufferPool(bufferSize: Int, maxPoolSize: Int) { if (buf.capacity() <= bufferSize && poolSize < maxPoolSize) addBufferToPool(buf) + // TODO: check whether limiting the spin count in the following two methods is beneficial + // (e.g. never limit more than 1000 times), since both methods could fall back to not + // using the buffer at all (take fallback: create a new buffer, add fallback: just drop) + @tailrec - final def takeBufferFromPool(): ByteBuffer = + private def takeBufferFromPool(): ByteBuffer = if (state.compareAndSet(Unlocked, Locked)) try pool match { case Nil ⇒ allocate(bufferSize) // we have no more buffer available, so create a new one @@ -57,7 +61,7 @@ class DirectByteBufferPool(bufferSize: Int, maxPoolSize: Int) { else takeBufferFromPool() // spin while locked @tailrec - final def addBufferToPool(buf: ByteBuffer): Unit = + private def addBufferToPool(buf: ByteBuffer): Unit = if (state.compareAndSet(Unlocked, Locked)) { buf.clear() // ensure that we never have dirty buffers in the pool pool = buf :: pool diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 4107ee3e82..fc8beae4f9 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -17,6 +17,7 @@ import java.net.ServerSocket import scala.concurrent.duration._ import scala.collection.immutable import akka.actor.ActorSystem +import com.typesafe.config.Config object Tcp extends ExtensionKey[TcpExt] { @@ -126,24 +127,16 @@ object Tcp extends ExtensionKey[TcpExt] { } } - case class Stats(channelsOpened: Long, channelsClosed: Long, selectorStats: Seq[SelectorStats]) { - def channelsOpen = channelsOpened - channelsClosed - } - - case class SelectorStats(channelsOpened: Long, channelsClosed: Long) { - def channelsOpen = channelsOpened - channelsClosed - } - /// COMMANDS sealed trait Command case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None, - options: immutable.Seq[SocketOption] = Nil) extends Command + options: immutable.Traversable[SocketOption] = Nil) extends Command case class Bind(handler: ActorRef, endpoint: InetSocketAddress, backlog: Int = 100, - options: immutable.Seq[SocketOption] = Nil) extends Command + options: immutable.Traversable[SocketOption] = Nil) extends Command case class Register(handler: ActorRef) extends Command case object Unbind extends Command @@ -172,8 +165,6 @@ object Tcp extends ExtensionKey[TcpExt] { case object StopReading extends Command case object ResumeReading extends Command - case object GetStats extends Command - /// EVENTS sealed trait Event @@ -193,10 +184,9 @@ object Tcp extends ExtensionKey[TcpExt] { /// INTERNAL case class RegisterOutgoingConnection(channel: SocketChannel) case class RegisterServerSocketChannel(channel: ServerSocketChannel) - case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, options: immutable.Seq[SocketOption]) - case class CreateConnection(channel: SocketChannel, handler: ActorRef, options: immutable.Seq[SocketOption]) - case class Reject(command: Command, retriesLeft: Int, commander: ActorRef) - case class Retry(command: Command, retriesLeft: Int, commander: ActorRef) + case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, + options: immutable.Traversable[SocketOption]) extends Command + case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } case object ChannelConnectable case object ChannelAcceptable case object ChannelReadable @@ -208,22 +198,26 @@ object Tcp extends ExtensionKey[TcpExt] { class TcpExt(system: ExtendedActorSystem) extends IO.Extension { - object Settings { - val config = system.settings.config.getConfig("akka.io.tcp") + val Settings = new Settings(system.settings.config.getConfig("akka.io.tcp")) + class Settings private[TcpExt] (config: Config) { import config._ val NrOfSelectors = getInt("nr-of-selectors") val MaxChannels = getInt("max-channels") - val SelectTimeout = - if (getString("select-timeout") == "infinite") Duration.Inf - else Duration(getMilliseconds("select-timeout"), MILLISECONDS) + val SelectTimeout = getString("select-timeout") match { + case "infinite" ⇒ Duration.Inf + case x ⇒ Duration(x) + } + if (getString("select-timeout") == "infinite") Duration.Inf + else Duration(getMilliseconds("select-timeout"), MILLISECONDS) val SelectorAssociationRetries = getInt("selector-association-retries") val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getInt("direct-buffer-size") val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") - val RegisterTimeout = - if (getString("register-timeout") == "infinite") Duration.Undefined - else Duration(getMilliseconds("register-timeout"), MILLISECONDS) + val RegisterTimeout = getString("register-timeout") match { + case "infinite" ⇒ Duration.Undefined + case x ⇒ Duration(x) + } val SelectorDispatcher = getString("selector-dispatcher") val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") @@ -238,8 +232,11 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val MaxChannelsPerSelector = MaxChannels / NrOfSelectors } - val manager = system.asInstanceOf[ActorSystemImpl].systemActorOf( - Props.empty.withDispatcher(Settings.ManagementDispatcher), "IO-TCP") + val manager = { + system.asInstanceOf[ActorSystemImpl].systemActorOf( + props = Props(new TcpManager(this)).withDispatcher(Settings.ManagementDispatcher), + name = "IO-TCP") + } val bufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) } diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 03069f0d01..265b005fa8 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -20,11 +20,9 @@ import java.nio.ByteBuffer * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ abstract class TcpConnection(val selector: ActorRef, - val channel: SocketChannel) extends Actor with ActorLogging with WithBufferPool { - val tcp = Tcp(context.system) - - channel.configureBlocking(false) - + val channel: SocketChannel, + val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { + import tcp.Settings._ var pendingWrite: PendingWrite = null // Needed to send the ConnectionClosed message in the postStop handler. @@ -33,15 +31,12 @@ abstract class TcpConnection(val selector: ActorRef, def writePending = pendingWrite ne null - def registerTimeout = tcp.Settings.RegisterTimeout - def traceLoggingEnabled = tcp.Settings.TraceLogging - // STATES /** connection established, waiting for registration from user handler */ def waitingForRegistration(commander: ActorRef): Receive = { case Register(handler) ⇒ - if (traceLoggingEnabled) log.debug("{} registered as connection handler", handler) + if (TraceLogging) log.debug("{} registered as connection handler", handler) selector ! ReadInterest context.setReceiveTimeout(Duration.Undefined) @@ -55,7 +50,7 @@ abstract class TcpConnection(val selector: ActorRef, case ReceiveTimeout ⇒ // after sending `Register` user should watch this actor to make sure // it didn't die because of the timeout - log.warning("Configured registration timeout of {} expired, stopping", registerTimeout) + log.warning("Configured registration timeout of {} expired, stopping", RegisterTimeout) context.stop(self) } @@ -66,7 +61,7 @@ abstract class TcpConnection(val selector: ActorRef, case ChannelReadable ⇒ doRead(handler) case write: Write if writePending ⇒ - if (traceLoggingEnabled) log.debug("Dropping write because queue is full") + if (TraceLogging) log.debug("Dropping write because queue is full") sender ! CommandFailed(write) case write: Write if write.data.isEmpty ⇒ @@ -107,14 +102,14 @@ abstract class TcpConnection(val selector: ActorRef, // AUXILIARIES and IMPLEMENTATION /** use in subclasses to start the common machinery above once a channel is connected */ - def completeConnect(commander: ActorRef, options: immutable.Seq[SocketOption]): Unit = { + def completeConnect(commander: ActorRef, options: immutable.Traversable[SocketOption]): Unit = { options.foreach(_.afterConnect(channel.socket)) commander ! Connected( channel.socket.getRemoteSocketAddress.asInstanceOf[InetSocketAddress], channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]) - context.setReceiveTimeout(registerTimeout) + context.setReceiveTimeout(RegisterTimeout) context.become(waitingForRegistration(commander)) } @@ -126,7 +121,7 @@ abstract class TcpConnection(val selector: ActorRef, buffer.flip() if (readBytes > 0) { - if (traceLoggingEnabled) log.debug("Read {} bytes", readBytes) + if (TraceLogging) log.debug("Read {} bytes", readBytes) handler ! Received(ByteString(buffer)) releaseBuffer(buffer) @@ -135,10 +130,10 @@ abstract class TcpConnection(val selector: ActorRef, self ! ChannelReadable else selector ! ReadInterest } else if (readBytes == 0) { - if (traceLoggingEnabled) log.debug("Read nothing. Registering read interest with selector") + if (TraceLogging) log.debug("Read nothing. Registering read interest with selector") selector ! ReadInterest } else if (readBytes == -1) { - if (traceLoggingEnabled) log.debug("Read returned end-of-stream") + if (TraceLogging) log.debug("Read returned end-of-stream") doCloseConnection(handler, closeReason) } else throw new IllegalStateException("Unexpected value returned from read: " + readBytes) @@ -150,7 +145,7 @@ abstract class TcpConnection(val selector: ActorRef, def doWrite(handler: ActorRef): Unit = { try { val writtenBytes = channel.write(pendingWrite.buffer) - if (traceLoggingEnabled) log.debug("Wrote {} bytes to channel", writtenBytes) + if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) if (pendingWrite.hasData) selector ! WriteInterest // still data to write else if (pendingWrite.wantsAck) { // everything written @@ -169,20 +164,20 @@ abstract class TcpConnection(val selector: ActorRef, def handleClose(handler: ActorRef, closedEvent: ConnectionClosed): Unit = if (closedEvent == Aborted) { // close instantly - if (traceLoggingEnabled) log.debug("Got Abort command. RESETing connection.") + if (TraceLogging) log.debug("Got Abort command. RESETing connection.") doCloseConnection(handler, closedEvent) } else if (writePending) { // finish writing first - if (traceLoggingEnabled) log.debug("Got Close command but write is still pending.") + if (TraceLogging) log.debug("Got Close command but write is still pending.") context.become(closingWithPendingWrite(handler, closedEvent)) } else if (closedEvent == ConfirmedClosed) { // shutdown output and wait for confirmation - if (traceLoggingEnabled) log.debug("Got ConfirmedClose command, sending FIN.") + if (TraceLogging) log.debug("Got ConfirmedClose command, sending FIN.") channel.socket.shutdownOutput() context.become(closing(handler)) } else { // close now - if (traceLoggingEnabled) log.debug("Got Close command, closing connection.") + if (TraceLogging) log.debug("Got Close command, closing connection.") doCloseConnection(handler, closedEvent) } @@ -222,7 +217,7 @@ abstract class TcpConnection(val selector: ActorRef, case NonFatal(e) ⇒ // setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574 // (also affected: OS/X Java 1.6.0_37) - if (traceLoggingEnabled) log.debug("setSoLinger(true, 0) failed with {}", e) + if (TraceLogging) log.debug("setSoLinger(true, 0) failed with {}", e) } channel.close() } diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala index 009621f032..902dde0233 100644 --- a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -15,8 +15,10 @@ import Tcp.SocketOption */ class TcpIncomingConnection(_selector: ActorRef, _channel: SocketChannel, + _tcp: TcpExt, handler: ActorRef, - options: immutable.Seq[SocketOption]) extends TcpConnection(_selector, _channel) { + options: immutable.Traversable[SocketOption]) + extends TcpConnection(_selector, _channel, _tcp) { context.watch(handler) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index 1f339be702..cf40d35a23 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -12,15 +12,15 @@ import scala.util.control.NonFatal import akka.actor.{ ActorLogging, ActorRef, Actor } import Tcp._ -class TcpListener(manager: ActorRef, - selector: ActorRef, +class TcpListener(selector: ActorRef, handler: ActorRef, endpoint: InetSocketAddress, backlog: Int, bindCommander: ActorRef, - options: immutable.Seq[SocketOption]) extends Actor with ActorLogging { + settings: TcpExt#Settings, + options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { - val batchAcceptLimit = Tcp(context.system).Settings.BatchAcceptLimit + context.watch(handler) // sign death pact val channel = { val serverSocketChannel = ServerSocketChannel.open serverSocketChannel.configureBlocking(false) @@ -30,7 +30,6 @@ class TcpListener(manager: ActorRef, serverSocketChannel } selector ! RegisterServerSocketChannel(channel) - context.watch(bindCommander) // sign death pact log.debug("Successfully bound to {}", endpoint) def receive: Receive = { @@ -41,7 +40,14 @@ class TcpListener(manager: ActorRef, def bound: Receive = { case ChannelAcceptable ⇒ - acceptAllPending(batchAcceptLimit) + acceptAllPending(settings.BatchAcceptLimit) + + case CommandFailed(RegisterIncomingConnection(socketChannel, _, _)) ⇒ + log.warning("Could not register incoming connection since capacity limit is reached, closing connection") + try socketChannel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing channel") + } case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) @@ -60,15 +66,19 @@ class TcpListener(manager: ActorRef, } if (socketChannel != null) { log.debug("New connection accepted") - manager ! RegisterIncomingConnection(socketChannel, handler, options) - selector ! AcceptInterest + socketChannel.configureBlocking(false) + context.parent ! RegisterIncomingConnection(socketChannel, handler, options) acceptAllPending(limit - 1) } - } + } else selector ! AcceptInterest override def postStop() { - try channel.close() - catch { + try { + if (channel.isOpen) { + log.debug("Closing serverSocketChannel after being stopped") + channel.close() + } + } catch { case NonFatal(e) ⇒ log.error(e, "Error closing ServerSocketChannel") } } diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-io/src/main/scala/akka/io/TcpManager.scala index bba033dc9b..6f2046d75f 100644 --- a/akka-io/src/main/scala/akka/io/TcpManager.scala +++ b/akka-io/src/main/scala/akka/io/TcpManager.scala @@ -4,12 +4,8 @@ package akka.io -import scala.concurrent.Future -import scala.concurrent.duration._ import akka.actor.{ ActorLogging, Actor, Props } import akka.routing.RandomRouter -import akka.util.Timeout -import akka.pattern.{ ask, pipe } import Tcp._ /** @@ -47,36 +43,13 @@ import Tcp._ * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. * */ -class TcpManager extends Actor with ActorLogging { - val settings = Tcp(context.system).Settings - val selectorNr = Iterator.from(0) +class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { val selectorPool = context.actorOf( - props = Props(new TcpSelector(self)).withRouter(RandomRouter(settings.NrOfSelectors)), - name = selectorNr.next().toString) + props = Props(new TcpSelector(self, tcp)).withRouter(RandomRouter(tcp.Settings.NrOfSelectors)), + name = "selectors") def receive = { - case RegisterIncomingConnection(channel, handler, options) ⇒ - selectorPool ! CreateConnection(channel, handler, options) - - case c: Connect ⇒ - selectorPool forward c - - case b: Bind ⇒ - selectorPool forward b - - case Reject(command, 0, commander) ⇒ - log.warning("Command '{}' failed since all {} selectors are at capacity", command, context.children.size) - commander ! CommandFailed(command) - - case Reject(command, retriesLeft, commander) ⇒ - log.warning("Command '{}' rejected by {} with {} retries left, retrying...", command, sender, retriesLeft) - selectorPool ! Retry(command, retriesLeft - 1, commander) - - case GetStats ⇒ - import context.dispatcher - implicit val timeout: Timeout = 1 second span - val seqFuture = Future.traverse(context.children)(_.ask(GetStats).mapTo[SelectorStats]) - seqFuture.map(s ⇒ Stats(s.map(_.channelsOpen).sum, s.map(_.channelsClosed).sum, s.toSeq)) pipeTo sender + case x @ (_: Connect | _: Bind) ⇒ selectorPool forward x } } diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index cfdc7a2af0..b33e6605e8 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -16,11 +16,13 @@ import Tcp._ * to be established. */ class TcpOutgoingConnection(_selector: ActorRef, + _tcp: TcpExt, commander: ActorRef, remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress], - options: immutable.Seq[SocketOption]) - extends TcpConnection(_selector, SocketChannel.open()) { + options: immutable.Traversable[SocketOption]) + extends TcpConnection(_selector, TcpOutgoingConnection.newSocketChannel(), _tcp) { + context.watch(commander) // sign death pact localAddress.foreach(channel.socket.bind) @@ -36,7 +38,7 @@ class TcpOutgoingConnection(_selector: ActorRef, def receive: Receive = PartialFunction.empty - def connecting(commander: ActorRef, options: immutable.Seq[SocketOption]): Receive = { + def connecting(commander: ActorRef, options: immutable.Traversable[SocketOption]): Receive = { case ChannelConnectable ⇒ try { val connected = channel.finishConnect() @@ -49,3 +51,11 @@ class TcpOutgoingConnection(_selector: ActorRef, } } + +object TcpOutgoingConnection { + private def newSocketChannel() = { + val channel = SocketChannel.open() + channel.configureBlocking(false) + channel + } +} diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 189755c079..48f1c5ab36 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -14,69 +14,58 @@ import scala.concurrent.duration._ import akka.actor._ import Tcp._ -class TcpSelector(manager: ActorRef) extends Actor with ActorLogging { - @volatile var childrenKeys = HashMap.empty[String, SelectionKey] - var channelsOpened = 0L - var channelsClosed = 0L - val sequenceNumber = Iterator.from(0) - val settings = Tcp(context.system).Settings - val selectorManagementDispatcher = context.system.dispatchers.lookup(settings.SelectorDispatcher) - val selector = SelectorProvider.provider.openSelector - val doSelect: () ⇒ Int = - settings.SelectTimeout match { - case Duration.Zero ⇒ () ⇒ selector.selectNow() - case Duration.Inf ⇒ () ⇒ selector.select() - case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) - } +class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLogging { + import tcp.Settings._ - selectorManagementDispatcher.execute(select) // start selection "loop" + @volatile var childrenKeys = HashMap.empty[String, SelectionKey] + val sequenceNumber = Iterator.from(0) + val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) + val selector = SelectorProvider.provider.openSelector + val OP_READ_AND_WRITE = OP_READ + OP_WRITE // compile-time constant def receive: Receive = { case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) - case CreateConnection(channel, handler, options) ⇒ - val connection = context.actorOf( - props = Props( - creator = () ⇒ new TcpIncomingConnection(self, channel, handler, options), - dispatcher = settings.WorkerDispatcher), - name = nextName) - execute(registerIncomingConnection(channel, handler)) - context.watch(connection) - channelsOpened += 1 + case cmd: RegisterIncomingConnection ⇒ + handleIncomingConnection(cmd, SelectorAssociationRetries) case cmd: Connect ⇒ - handleConnect(cmd, settings.SelectorAssociationRetries, sender) + handleConnect(cmd, SelectorAssociationRetries) - case Retry(cmd: Connect, retriesLeft, commander) ⇒ - handleConnect(cmd, retriesLeft, commander) + case cmd: Bind ⇒ + handleBind(cmd, SelectorAssociationRetries) case RegisterOutgoingConnection(channel) ⇒ execute(registerOutgoingConnection(channel, sender)) - case cmd: Bind ⇒ - handleBind(cmd, settings.SelectorAssociationRetries, sender) - - case Retry(cmd: Bind, retriesLeft, commander) ⇒ - handleBind(cmd, retriesLeft, commander) - case RegisterServerSocketChannel(channel) ⇒ execute(registerListener(channel, sender)) + case Retry(command, 0) ⇒ + log.warning("Command '{}' failed since all selectors are at capacity", command) + sender ! CommandFailed(command) + + case Retry(cmd: RegisterIncomingConnection, retriesLeft) ⇒ + handleIncomingConnection(cmd, retriesLeft) + + case Retry(cmd: Connect, retriesLeft) ⇒ + handleConnect(cmd, retriesLeft) + + case Retry(cmd: Bind, retriesLeft) ⇒ + handleBind(cmd, retriesLeft) + case Terminated(child) ⇒ execute(unregister(child)) - channelsClosed += 1 - - case GetStats ⇒ - sender ! SelectorStats(channelsOpened, channelsClosed) } override def postStop() { try { - import scala.collection.JavaConverters._ - selector.keys.asScala.foreach(_.channel.close()) - selector.close() + try { + val iterator = selector.keys.iterator + while (iterator.hasNext) iterator.next().channel.close() + } finally selector.close() } catch { case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") } @@ -85,35 +74,43 @@ class TcpSelector(manager: ActorRef) extends Actor with ActorLogging { // we can never recover from failures of a connection or listener child override def supervisorStrategy = SupervisorStrategy.stoppingStrategy - def handleConnect(cmd: Connect, retriesLeft: Int, commander: ActorRef): Unit = { + def handleIncomingConnection(cmd: RegisterIncomingConnection, retriesLeft: Int): Unit = + withCapacityProtection(cmd, retriesLeft) { + import cmd._ + val connection = spawnChild(() ⇒ new TcpIncomingConnection(self, channel, tcp, handler, options)) + execute(registerIncomingConnection(channel, connection)) + } + + def handleConnect(cmd: Connect, retriesLeft: Int): Unit = + withCapacityProtection(cmd, retriesLeft) { + import cmd._ + val commander = sender + spawnChild(() ⇒ new TcpOutgoingConnection(self, tcp, commander, remoteAddress, localAddress, options)) + } + + def handleBind(cmd: Bind, retriesLeft: Int): Unit = + withCapacityProtection(cmd, retriesLeft) { + import cmd._ + val commander = sender + spawnChild(() ⇒ new TcpListener(self, handler, endpoint, backlog, commander, tcp.Settings, options)) + } + + def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { log.debug("Executing {}", cmd) - if (canHandleMoreChannels) { - val connection = context.actorOf( - props = Props( - creator = () ⇒ new TcpOutgoingConnection(self, commander, cmd.remoteAddress, cmd.localAddress, cmd.options), - dispatcher = settings.WorkerDispatcher), - name = nextName) - context.watch(connection) - channelsOpened += 1 - } else sender ! Reject(cmd, retriesLeft, commander) + if (MaxChannelsPerSelector == 0 || childrenKeys.size < MaxChannelsPerSelector) { + body + } else { + log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) + context.parent forward Retry(cmd, retriesLeft - 1) + } } - def handleBind(cmd: Bind, retriesLeft: Int, commander: ActorRef): Unit = { - log.debug("Executing {}", cmd) - if (canHandleMoreChannels) { - val listener = context.actorOf( - props = Props( - creator = () ⇒ new TcpListener(manager, self, cmd.handler, cmd.endpoint, cmd.backlog, commander, cmd.options), - dispatcher = settings.WorkerDispatcher), - name = nextName) - context.watch(listener) - channelsOpened += 1 - } else sender ! Reject(cmd, retriesLeft, commander) - } - - def nextName = sequenceNumber.next().toString - - def canHandleMoreChannels = childrenKeys.size < settings.MaxChannelsPerSelector + def spawnChild(creator: () ⇒ Actor) = + context.watch { + context.actorOf( + props = Props(creator, dispatcher = WorkerDispatcher), + name = sequenceNumber.next().toString) + } //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// @@ -172,18 +169,28 @@ class TcpSelector(manager: ActorRef) extends Actor with ActorLogging { } val select = new Task { + val doSelect: () ⇒ Int = + SelectTimeout match { + case Duration.Zero ⇒ () ⇒ selector.selectNow() + case Duration.Inf ⇒ () ⇒ selector.select() + case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) + } def tryRun() { if (doSelect() > 0) { val keys = selector.selectedKeys val iterator = keys.iterator() while (iterator.hasNext) { val key = iterator.next - val connection = key.attachment.asInstanceOf[ActorRef] if (key.isValid) { - if (key.isReadable) connection ! ChannelReadable - if (key.isWritable) connection ! ChannelWritable - else if (key.isAcceptable) connection ! ChannelAcceptable - else if (key.isConnectable) connection ! ChannelConnectable + val connection = key.attachment.asInstanceOf[ActorRef] + key.readyOps match { + case OP_READ ⇒ connection ! ChannelReadable + case OP_WRITE ⇒ connection ! ChannelWritable + case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable + case x if (x & OP_ACCEPT) > 0 ⇒ connection ! ChannelAcceptable + case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable + case x ⇒ log.warning("Invalid readyOps: {}", x) + } key.interestOps(0) // prevent immediate reselection by always clearing } else log.warning("Invalid selection key: {}", key) } @@ -193,12 +200,15 @@ class TcpSelector(manager: ActorRef) extends Actor with ActorLogging { } } + selectorManagementDispatcher.execute(select) // start selection "loop" + abstract class Task extends Runnable { def tryRun() def run() { try tryRun() catch { - case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) + case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown + case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) } } } diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 0265781c90..9dc925a390 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -20,7 +20,7 @@ import akka.util.ByteString import Tcp._ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { - val serverAddress = TemporaryServerAddress.get("127.0.0.1") + val serverAddress = TemporaryServerAddress("127.0.0.1") "An outgoing connection" must { // common behavior @@ -245,7 +245,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertActorTerminated(connectionActor) } - val UnboundAddress = TemporaryServerAddress.get("127.0.0.1") + val UnboundAddress = TemporaryServerAddress("127.0.0.1") "report failed connection attempt when target is unreachable" in withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ import setup._ @@ -397,7 +397,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { TestActorRef( - new TcpOutgoingConnection(selector, commander, serverAddress, localAddress, options) { + new TcpOutgoingConnection(selector, Tcp(system), commander, serverAddress, localAddress, options) { override def postRestart(reason: Throwable) { // ensure we never restart context.stop(self) diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index 0619038c54..8275170b8f 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -4,56 +4,85 @@ package akka.io -import java.net.{ Socket, InetSocketAddress } -import java.nio.channels.ServerSocketChannel +import java.net.Socket import scala.concurrent.duration._ -import scala.util.Success +import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } -import akka.util.Timeout -import akka.pattern.ask import Tcp._ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { + "A TcpListener" must { - val manager = TestProbe() - val selector = TestProbe() - val handler = TestProbe() - val handlerRef = handler.ref - val bindCommander = TestProbe() - val endpoint = TemporaryServerAddress.get("127.0.0.1") - val listener = TestActorRef(new TcpListener(manager.ref, selector.ref, handler.ref, endpoint, 100, - bindCommander.ref, Nil)) - var serverSocketChannel: Option[ServerSocketChannel] = None "register its ServerSocketChannel with its selector" in { - val RegisterServerSocketChannel(channel) = selector.receiveOne(Duration.Zero) - serverSocketChannel = Some(channel) + val setup = ListenerSetup(autoBind = false) + import setup._ + + selector.expectMsgType[RegisterServerSocketChannel] } "let the Bind commander know when binding is completed" in { + ListenerSetup() + } + + "accept acceptable connections register them with its parent" in { + val setup = ListenerSetup() + import setup._ + + new Socket(endpoint.getHostName, endpoint.getPort) + new Socket(endpoint.getHostName, endpoint.getPort) + new Socket(endpoint.getHostName, endpoint.getPort) + + listener ! ChannelAcceptable + + val handlerRef = handler.ref + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } + parent.expectNoMsg(100.millis) + + listener ! ChannelAcceptable + + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } + } + + "react to Unbind commands by replying with Unbound and stopping itself" in { + val setup = ListenerSetup() + import setup._ + + val unbindCommander = TestProbe() + unbindCommander.send(listener, Unbind) + + unbindCommander.expectMsg(Unbound) + parent.expectMsgType[Terminated].actor must be(listener) + } + } + + val counter = Iterator.from(0) + + case class ListenerSetup(autoBind: Boolean = true) { + val selector = TestProbe() + val handler = TestProbe() + val bindCommander = TestProbe() + val parent = TestProbe() + val endpoint = TemporaryServerAddress("127.0.0.1") + private val parentRef = TestActorRef(new ListenerParent) + + if (autoBind) { listener ! Bound bindCommander.expectMsg(Bound) } - "accept two acceptable connections at once and register them with the manager" in { - new Socket("localhost", endpoint.getPort) - new Socket("localhost", endpoint.getPort) - new Socket("localhost", endpoint.getPort) - listener ! ChannelAcceptable - val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) - val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) - } - - "accept one more connection and register it with the manager" in { - listener ! ChannelAcceptable - val RegisterIncomingConnection(_, `handlerRef`, Nil) = manager.receiveOne(Duration.Zero) - } - - "react to Unbind commands by closing the ServerSocketChannel, replying with Unbound and stopping itself" in { - implicit val timeout: Timeout = 1 second span - listener.ask(Unbind).value must equal(Some(Success(Unbound))) - serverSocketChannel.get.isOpen must equal(false) - listener.isTerminated must equal(true) + def listener = parentRef.underlyingActor.listener + private class ListenerParent extends Actor { + val listener = context.actorOf( + props = Props(new TcpListener(selector.ref, handler.ref, endpoint, 100, bindCommander.ref, + Tcp(system).Settings, Nil)), + name = "test-listener-" + counter.next()) + parent.watch(listener) + def receive: Receive = { + case msg ⇒ parent.ref forward msg + } + override def supervisorStrategy = SupervisorStrategy.stoppingStrategy } } diff --git a/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala b/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala index f006ed70e5..20b4e3c16b 100644 --- a/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala +++ b/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala @@ -1,10 +1,15 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + package akka.io import java.nio.channels.ServerSocketChannel import java.net.InetSocketAddress object TemporaryServerAddress { - def get(address: String): InetSocketAddress = { + + def apply(address: String = "127.0.0.1"): InetSocketAddress = { val serverSocket = ServerSocketChannel.open() serverSocket.socket.bind(new InetSocketAddress(address, 0)) val port = serverSocket.socket.getLocalPort From 2f54a5b4d2822f9189bc9d41ab38a5e8d2140613 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 18 Jan 2013 13:20:17 +0100 Subject: [PATCH 16/66] more tests, smaller improvements --- akka-io/src/main/scala/akka/io/Tcp.scala | 8 +- .../main/scala/akka/io/TcpConnection.scala | 4 +- .../src/main/scala/akka/io/TcpListener.scala | 2 +- .../test/scala/akka/io/IntegrationSpec.scala | 83 +++++++++++++++++++ .../scala/akka/io/TcpConnectionSpec.scala | 58 ++++++++----- .../test/scala/akka/io/TcpListenerSpec.scala | 65 +++++++++------ .../src/test/scala/akka/io/TestUtils.scala | 37 +++++++++ 7 files changed, 203 insertions(+), 54 deletions(-) create mode 100644 akka-io/src/test/scala/akka/io/IntegrationSpec.scala create mode 100644 akka-io/src/test/scala/akka/io/TestUtils.scala diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index fc8beae4f9..ea8892c0c9 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -151,10 +151,10 @@ object Tcp extends ExtensionKey[TcpExt] { * Write data to the TCP connection. If no ack is needed use the special * `NoAck` object. */ - case class Write(data: ByteString, ack: AnyRef) extends Command { - require(ack ne null, "ack must be non-null. Use NoAck if you don't want acks.") + case class Write(data: ByteString, ack: Any) extends Command { + require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.") - def wantsAck: Boolean = ack ne NoAck + def wantsAck: Boolean = ack != NoAck } object Write { val Empty: Write = Write(ByteString.empty, NoAck) @@ -208,8 +208,6 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { case "infinite" ⇒ Duration.Inf case x ⇒ Duration(x) } - if (getString("select-timeout") == "infinite") Duration.Inf - else Duration(getMilliseconds("select-timeout"), MILLISECONDS) val SelectorAssociationRetries = getInt("selector-association-retries") val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getInt("direct-buffer-size") diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 265b005fa8..e8cc98ce72 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -238,9 +238,9 @@ abstract class TcpConnection(val selector: ActorRef, override def postRestart(reason: Throwable): Unit = throw new IllegalStateException("Restarting not supported for connection actors.") - private[TcpConnection] case class PendingWrite(commander: ActorRef, ack: AnyRef, buffer: ByteBuffer) { + private[TcpConnection] case class PendingWrite(commander: ActorRef, ack: Any, buffer: ByteBuffer) { def hasData = buffer.remaining() > 0 - def wantsAck = ack ne NoAck + def wantsAck = ack != NoAck } def createWrite(write: Write): PendingWrite = { val buffer = acquireBuffer(write.data.length) diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index cf40d35a23..5bf456faf3 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -43,7 +43,7 @@ class TcpListener(selector: ActorRef, acceptAllPending(settings.BatchAcceptLimit) case CommandFailed(RegisterIncomingConnection(socketChannel, _, _)) ⇒ - log.warning("Could not register incoming connection since capacity limit is reached, closing connection") + log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection") try socketChannel.close() catch { case NonFatal(e) ⇒ log.error(e, "Error closing channel") diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala new file mode 100644 index 0000000000..f69b8347d0 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import akka.testkit.{ TestProbe, AkkaSpec } +import akka.actor.ActorRef +import akka.util.ByteString +import Tcp._ +import TestUtils._ + +class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { + + "The TCP transport implementation" should { + + "properly bind a test server" in new TestSetup + + "allow connecting to the test server" in new TestSetup { + establishNewClientConnection() + } + + "allow connecting to and disconnecting from the test server" in new TestSetup { + val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() + clientHandler.send(clientConnection, Close) + clientHandler.expectMsg(Closed) + serverHandler.expectMsg(PeerClosed) + verifyActorTermination(clientConnection) + verifyActorTermination(serverConnection) + } + + "properly complete one client/server request/response cycle" in new TestSetup { + val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() + + clientHandler.send(clientConnection, Write(ByteString("Captain on the bridge!"), 42)) + clientHandler.expectMsg(42) + serverHandler.expectMsgType[Received].data.decodeString("ASCII") must be("Captain on the bridge!") + + serverHandler.send(serverConnection, Write(ByteString("For the king!"), 4242)) + serverHandler.expectMsg(4242) + clientHandler.expectMsgType[Received].data.decodeString("ASCII") must be("For the king!") + + serverHandler.send(serverConnection, Close) + serverHandler.expectMsg(Closed) + clientHandler.expectMsg(PeerClosed) + + verifyActorTermination(clientConnection) + verifyActorTermination(serverConnection) + } + + ////////////////////////////////////// + ///////// more tests to come ///////// + ////////////////////////////////////// + } + + class TestSetup { + val bindHandler = TestProbe() + val endpoint = TemporaryServerAddress() + + bindServer() + + def bindServer(): Unit = { + val bindCommander = TestProbe() + bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint)) + bindCommander.expectMsg(Bound) + } + + def establishNewClientConnection(): (TestProbe, ActorRef, TestProbe, ActorRef) = { + val connectCommander = TestProbe() + connectCommander.send(IO(Tcp), Connect(endpoint)) + val Connected(`endpoint`, localAddress) = connectCommander.expectMsgType[Connected] + val clientHandler = TestProbe() + connectCommander.sender ! Register(clientHandler.ref) + + val Connected(`localAddress`, `endpoint`) = bindHandler.expectMsgType[Connected] + val serverHandler = TestProbe() + bindHandler.sender ! Register(serverHandler.ref) + + (clientHandler, connectCommander.sender, serverHandler, bindHandler.sender) + } + } + +} \ No newline at end of file diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 9dc925a390..f826be7f17 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -17,10 +17,11 @@ import scala.util.control.NonFatal import akka.actor.{ ActorRef, Terminated } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import akka.util.ByteString +import TestUtils._ import Tcp._ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { - val serverAddress = TemporaryServerAddress("127.0.0.1") + val serverAddress = TemporaryServerAddress() "An outgoing connection" must { // common behavior @@ -45,7 +46,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") clientChannel.socket.getKeepAlive must be(true) } - "send incoming data to user" in withEstablishedConnection() { setup ⇒ + "send incoming data to the connection handler" in withEstablishedConnection() { setup ⇒ import setup._ serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII"))) // emulate selector behavior @@ -69,16 +70,24 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") writer.send(connectionActor, Write(ByteString.empty, Ack)) writer.expectMsg(Ack) - val write = Write(ByteString("testdata"), Ack) + // reply to write commander with Ack + val ackedWrite = Write(ByteString("testdata"), Ack) val buffer = ByteBuffer.allocate(100) serverSideChannel.read(buffer) must be(0) - - writer.send(connectionActor, write) - // make sure the writer gets the ack + writer.send(connectionActor, ackedWrite) writer.expectMsg(Ack) serverSideChannel.read(buffer) must be(8) buffer.flip() - ByteString(buffer).take(8).decodeString("ASCII") must be("testdata") + + // not reply to write commander for writes without Ack + val unackedWrite = Write(ByteString("morestuff!")) + buffer.clear() + serverSideChannel.read(buffer) must be(0) + writer.send(connectionActor, unackedWrite) + writer.expectNoMsg() + serverSideChannel.read(buffer) must be(10) + buffer.flip() + ByteString(buffer).take(10).decodeString("ASCII") must be("morestuff!") } "stop writing in cases of backpressure and resume afterwards" in @@ -134,7 +143,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") selector.expectMsg(ReadInterest) } - "close the connection" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + "close the connection and reply with `Closed` upon reception of a `Close` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ import setup._ // we should test here that a pending write command is properly finished first @@ -145,18 +154,28 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // we send a write and a close command directly afterwards connectionHandler.send(connectionActor, writeCmd(Ack)) - connectionHandler.send(connectionActor, Close) + val closeCommander = TestProbe() + closeCommander.send(connectionActor, Close) pullFromServerSide(TestSize) connectionHandler.expectMsg(Ack) connectionHandler.expectMsg(Closed) + closeCommander.expectMsg(Closed) assertThisConnectionActorTerminated() val buffer = ByteBuffer.allocate(1) serverSideChannel.read(buffer) must be(-1) } - "abort the connection" in withEstablishedConnection() { setup ⇒ + "send only one `Closed` event to the handler, if the handler commanded the Close" in withEstablishedConnection() { setup ⇒ + import setup._ + + connectionHandler.send(connectionActor, Close) + connectionHandler.expectMsg(Closed) + connectionHandler.expectNoMsg() + } + + "abort the connection and reply with `Aborted` upong reception of an `Abort` command" in withEstablishedConnection() { setup ⇒ import setup._ connectionHandler.send(connectionActor, Abort) @@ -169,7 +188,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") thrown.getMessage must be("Connection reset by peer") } - "close the connection and confirm" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ import setup._ // we should test here that a pending write command is properly finished first @@ -242,10 +261,10 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") selector.send(connectionActor, ChannelConnectable) userHandler.expectMsgType[ErrorClose].cause must be("Connection reset by peer") - assertActorTerminated(connectionActor) + verifyActorTermination(connectionActor) } - val UnboundAddress = TemporaryServerAddress("127.0.0.1") + val UnboundAddress = TemporaryServerAddress() "report failed connection attempt when target is unreachable" in withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ import setup._ @@ -258,7 +277,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") selector.send(connectionActor, ChannelConnectable) userHandler.expectMsgType[ErrorClose].cause must be("Connection refused") - assertActorTerminated(connectionActor) + verifyActorTermination(connectionActor) } "time out when Connected isn't answered with Register" in withUnacceptedConnection() { setup ⇒ @@ -268,7 +287,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") selector.send(connectionActor, ChannelConnectable) userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) - assertActorTerminated(connectionActor) + verifyActorTermination(connectionActor) } "close the connection when user handler dies while connecting" in withUnacceptedConnection() { setup ⇒ @@ -277,7 +296,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // simulate death of userHandler test probe userHandler.send(connectionActor, akka.actor.Terminated(userHandler.ref)(false, false)) - assertActorTerminated(connectionActor) + verifyActorTermination(connectionActor) } "close the connection when connection handler dies while connected" in withEstablishedConnection() { setup ⇒ @@ -334,7 +353,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } def assertThisConnectionActorTerminated(): Unit = { - assertActorTerminated(connectionActor) + verifyActorTermination(connectionActor) clientSideChannel must not be ('open) } } @@ -420,9 +439,4 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") channel.socket.setSoLinger(true, 0) channel.close() } - def assertActorTerminated(connectionActor: TestActorRef[TcpOutgoingConnection]): Unit = { - val watcher = TestProbe() - watcher.watch(connectionActor) - watcher.expectMsgType[Terminated].actor must be(connectionActor) - } } diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index 8275170b8f..5d238f28e9 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -5,7 +5,9 @@ package akka.io import java.net.Socket +import scala.annotation.tailrec import scala.concurrent.duration._ +import org.scalatest.exceptions.TestFailedException import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import Tcp._ @@ -14,40 +16,35 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "A TcpListener" must { - "register its ServerSocketChannel with its selector" in { - val setup = ListenerSetup(autoBind = false) - import setup._ - + "register its ServerSocketChannel with its selector" in new TestSetup { selector.expectMsgType[RegisterServerSocketChannel] } - "let the Bind commander know when binding is completed" in { - ListenerSetup() + "let the Bind commander know when binding is completed" in new TestSetup { + listener ! Bound + bindCommander.expectMsg(Bound) } - "accept acceptable connections register them with its parent" in { - val setup = ListenerSetup() - import setup._ + "accept acceptable connections and register them with its parent" in new TestSetup { + bindListener() - new Socket(endpoint.getHostName, endpoint.getPort) - new Socket(endpoint.getHostName, endpoint.getPort) - new Socket(endpoint.getHostName, endpoint.getPort) + attemptConnectionToEndpoint() + attemptConnectionToEndpoint() + attemptConnectionToEndpoint() + // since the batch-accept-limit is 2 we must only receive 2 accepted connections listener ! ChannelAcceptable - - val handlerRef = handler.ref - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } parent.expectNoMsg(100.millis) + // and pick up the last remaining connection on the next ChannelAcceptable listener ! ChannelAcceptable - - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ } + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } } - "react to Unbind commands by replying with Unbound and stopping itself" in { - val setup = ListenerSetup() - import setup._ + "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { + bindListener() val unbindCommander = TestProbe() unbindCommander.send(listener, Unbind) @@ -55,24 +52,44 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { unbindCommander.expectMsg(Unbound) parent.expectMsgType[Terminated].actor must be(listener) } + + "drop an incoming connection if it cannot be registered with a selector" in new TestSetup { + bindListener() + + attemptConnectionToEndpoint() + + listener ! ChannelAcceptable + val channel = parent.expectMsgType[RegisterIncomingConnection].channel + channel.isOpen must be(true) + + listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) + + within(1.second) { + channel.isOpen must be(false) + } + } } val counter = Iterator.from(0) - case class ListenerSetup(autoBind: Boolean = true) { + class TestSetup { val selector = TestProbe() val handler = TestProbe() + val handlerRef = handler.ref val bindCommander = TestProbe() val parent = TestProbe() - val endpoint = TemporaryServerAddress("127.0.0.1") + val endpoint = TemporaryServerAddress() private val parentRef = TestActorRef(new ListenerParent) - if (autoBind) { + def bindListener() { listener ! Bound bindCommander.expectMsg(Bound) } + def attemptConnectionToEndpoint(): Unit = new Socket(endpoint.getHostName, endpoint.getPort) + def listener = parentRef.underlyingActor.listener + private class ListenerParent extends Actor { val listener = context.actorOf( props = Props(new TcpListener(selector.ref, handler.ref, endpoint, 100, bindCommander.ref, diff --git a/akka-io/src/test/scala/akka/io/TestUtils.scala b/akka-io/src/test/scala/akka/io/TestUtils.scala new file mode 100644 index 0000000000..339599d9cb --- /dev/null +++ b/akka-io/src/test/scala/akka/io/TestUtils.scala @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import scala.annotation.tailrec +import scala.concurrent.duration._ +import org.scalatest.exceptions.TestFailedException +import akka.actor.{ ActorSystem, ActorRef, Terminated } +import akka.testkit.TestProbe + +object TestUtils { + + def verifyActorTermination(actor: ActorRef)(implicit system: ActorSystem): Unit = { + val watcher = TestProbe() + watcher.watch(actor) + assert(watcher.expectMsgType[Terminated].actor == actor) + } + + // why doesn't scalatest provide this by default? (specs2 does with its `eventually` matcher modifier!) + // Due to its general utility, should this be moved to the testkit? + @tailrec final def within(timeLeft: Duration, retrySpan: Duration = 10.millis)(test: ⇒ Unit): Unit = { + val start = System.currentTimeMillis + lazy val end = System.currentTimeMillis + val tryAgain = try { + test + false + } catch { + case (_: TestFailedException | _: AssertionError) if timeLeft.toMillis + start - end > 0 ⇒ true + } + if (tryAgain) { + Thread.sleep(retrySpan.toMillis) + within(timeLeft - Duration(end - start, MILLISECONDS) - retrySpan)(test) + } + } +} From 9c9508d76fa9dada5d7c02d92738a53f2194006e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 14:45:19 +0100 Subject: [PATCH 17/66] improve DirectByteBufferPool and some smaller fixes * dispatch Closed events to the sender of the close command as well * DirectByteBufferPool changes: * hide behind BufferPool interface * only return buffers of the configured size * reduce work to be done while locked * use Array-based stack to store free buffers instead of linked lists * keep buffers by soft reference --- .../scala/akka/io/DirectByteBufferPool.scala | 101 ++++++++++-------- akka-io/src/main/scala/akka/io/Tcp.scala | 2 +- .../main/scala/akka/io/TcpConnection.scala | 100 ++++++++++------- .../scala/akka/io/TcpConnectionSpec.scala | 9 +- 4 files changed, 123 insertions(+), 89 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala index 3a1f00b20e..103be7bf6a 100644 --- a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala +++ b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -1,8 +1,9 @@ package akka.io -import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicBoolean import java.nio.ByteBuffer import annotation.tailrec +import java.lang.ref.SoftReference trait WithBufferPool { def tcp: TcpExt @@ -10,62 +11,68 @@ trait WithBufferPool { def acquireBuffer(): ByteBuffer = tcp.bufferPool.acquire() - def acquireBuffer(size: Int): ByteBuffer = - tcp.bufferPool.acquire(size) - def releaseBuffer(buffer: ByteBuffer) = tcp.bufferPool.release(buffer) } -/** - * A buffer pool which keeps direct buffers of a specified default size. - * If a buffer bigger than the default size is requested it is created - * but will not be pooled on release. - * - * This implementation is very loosely based on the one from Netty. - */ -class DirectByteBufferPool(bufferSize: Int, maxPoolSize: Int) { - private val Unlocked = 0 - private val Locked = 1 +trait BufferPool { + def acquire(): ByteBuffer + def release(buf: ByteBuffer): Unit +} - private[this] val state = new AtomicInteger(Unlocked) - @volatile private[this] var pool: List[ByteBuffer] = Nil - @volatile private[this] var poolSize: Int = 0 +/** + * A buffer pool which keeps a free list of direct buffers of a specified default + * size in a simple fixed size stack. + * + * If the stack is full a buffer offered back is not kept but will be let for + * being freed by normal garbage collection. + */ +private[akka] class DirectByteBufferPool(defaultBufferSize: Int, maxPoolEntries: Int) extends BufferPool { + private[this] val locked = new AtomicBoolean(false) + private[this] val pool: Array[SoftReference[ByteBuffer]] = new Array[SoftReference[ByteBuffer]](maxPoolEntries) + private[this] var buffersInPool: Int = 0 + + def acquire(): ByteBuffer = { + val buffer = takeBufferFromPool() + + // allocate new buffer and clear outside the lock + if (buffer == null) + allocate(defaultBufferSize) + else { + buffer.clear() + buffer + } + } + + def release(buf: ByteBuffer): Unit = + offerBufferToPool(buf) private def allocate(size: Int): ByteBuffer = ByteBuffer.allocateDirect(size) - def acquire(size: Int = bufferSize): ByteBuffer = { - if (poolSize == 0 || size > bufferSize) allocate(size) - else takeBufferFromPool() + @tailrec + private[this] final def takeBufferFromPool(): ByteBuffer = { + @tailrec def findBuffer(): ByteBuffer = + if (buffersInPool > 0) { + buffersInPool -= 1 + val buf = pool(buffersInPool).get() + + if (buf != null) buf + else findBuffer() + } else null + + if (locked.compareAndSet(false, true)) + try findBuffer() finally locked.set(false) + else takeBufferFromPool() // spin while locked } - def release(buf: ByteBuffer): Unit = - if (buf.capacity() <= bufferSize && poolSize < maxPoolSize) - addBufferToPool(buf) - - // TODO: check whether limiting the spin count in the following two methods is beneficial - // (e.g. never limit more than 1000 times), since both methods could fall back to not - // using the buffer at all (take fallback: create a new buffer, add fallback: just drop) - @tailrec - private def takeBufferFromPool(): ByteBuffer = - if (state.compareAndSet(Unlocked, Locked)) - try pool match { - case Nil ⇒ allocate(bufferSize) // we have no more buffer available, so create a new one - case buf :: tail ⇒ - pool = tail - poolSize -= 1 - buf - } finally state.set(Unlocked) - else takeBufferFromPool() // spin while locked - - @tailrec - private def addBufferToPool(buf: ByteBuffer): Unit = - if (state.compareAndSet(Unlocked, Locked)) { - buf.clear() // ensure that we never have dirty buffers in the pool - pool = buf :: pool - poolSize += 1 - state.set(Unlocked) - } else addBufferToPool(buf) // spin while locked + private final def offerBufferToPool(buf: ByteBuffer): Unit = + if (locked.compareAndSet(false, true)) + try if (buffersInPool < maxPoolEntries) { + pool(buffersInPool) = new SoftReference(buf) + buffersInPool += 1 + } // else let the buffer be gc'd + finally locked.set(false) + else offerBufferToPool(buf) // spin while locked } diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index ea8892c0c9..7bb13987d3 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -236,5 +236,5 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { name = "IO-TCP") } - val bufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) + val bufferPool: BufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) } diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index e8cc98ce72..b08ba8df48 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -26,8 +26,7 @@ abstract class TcpConnection(val selector: ActorRef, var pendingWrite: PendingWrite = null // Needed to send the ConnectionClosed message in the postStop handler. - // First element is the handler, second the particular close message. - var closedMessage: (ActorRef, ConnectionClosed) = null + var closedMessage: CloseInformation = null def writePending = pendingWrite ne null @@ -45,7 +44,7 @@ abstract class TcpConnection(val selector: ActorRef, context.become(connected(handler)) case cmd: CloseCommand ⇒ - handleClose(commander, closeResponse(cmd)) + handleClose(commander, Some(sender), closeResponse(cmd)) case ReceiveTimeout ⇒ // after sending `Register` user should watch this actor to make sure @@ -58,7 +57,7 @@ abstract class TcpConnection(val selector: ActorRef, def connected(handler: ActorRef): Receive = { case StopReading ⇒ selector ! StopReading case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ doRead(handler) + case ChannelReadable ⇒ doRead(handler, None) case write: Write if writePending ⇒ if (TraceLogging) log.debug("Dropping write because queue is full") @@ -74,29 +73,29 @@ abstract class TcpConnection(val selector: ActorRef, doWrite(handler) case ChannelWritable ⇒ doWrite(handler) - case cmd: CloseCommand ⇒ handleClose(handler, closeResponse(cmd)) + case cmd: CloseCommand ⇒ handleClose(handler, Some(sender), closeResponse(cmd)) } /** connection is closing but a write has to be finished first */ - def closingWithPendingWrite(handler: ActorRef, closedEvent: ConnectionClosed): Receive = { + def closingWithPendingWrite(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Receive = { case StopReading ⇒ selector ! StopReading case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ doRead(handler) + case ChannelReadable ⇒ doRead(handler, closeCommander) case ChannelWritable ⇒ doWrite(handler) if (!writePending) // writing is now finished - handleClose(handler, closedEvent) + handleClose(handler, closeCommander, closedEvent) - case Abort ⇒ handleClose(handler, Aborted) + case Abort ⇒ handleClose(handler, Some(sender), Aborted) } /** connection is closed on our side and we're waiting from confirmation from the other side */ - def closing(handler: ActorRef): Receive = { + def closing(handler: ActorRef, closeCommander: Option[ActorRef]): Receive = { case StopReading ⇒ selector ! StopReading case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ doRead(handler) - case Abort ⇒ handleClose(handler, Aborted) + case ChannelReadable ⇒ doRead(handler, closeCommander) + case Abort ⇒ handleClose(handler, Some(sender), Aborted) } // AUXILIARIES and IMPLEMENTATION @@ -113,7 +112,7 @@ abstract class TcpConnection(val selector: ActorRef, context.become(waitingForRegistration(commander)) } - def doRead(handler: ActorRef): Unit = { + def doRead(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = { val buffer = acquireBuffer() try { @@ -134,7 +133,7 @@ abstract class TcpConnection(val selector: ActorRef, selector ! ReadInterest } else if (readBytes == -1) { if (TraceLogging) log.debug("Read returned end-of-stream") - doCloseConnection(handler, closeReason) + doCloseConnection(handler, closeCommander, closeReason) } else throw new IllegalStateException("Unexpected value returned from read: " + readBytes) } catch { @@ -142,50 +141,58 @@ abstract class TcpConnection(val selector: ActorRef, } } - def doWrite(handler: ActorRef): Unit = { - try { + final def doWrite(handler: ActorRef): Unit = { + @tailrec def innerWrite(): Unit = { + val toWrite = pendingWrite.buffer.remaining() + require(toWrite != 0) val writtenBytes = channel.write(pendingWrite.buffer) if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) - if (pendingWrite.hasData) selector ! WriteInterest // still data to write + pendingWrite = pendingWrite.consume(writtenBytes) + + val wroteCompleteBuffer = writtenBytes == toWrite + if (pendingWrite.hasData) + if (wroteCompleteBuffer) innerWrite() // try again now + else selector ! WriteInterest // try again later else if (pendingWrite.wantsAck) { // everything written pendingWrite.commander ! pendingWrite.ack releaseBuffer(pendingWrite.buffer) pendingWrite = null } - } catch { - case e: IOException ⇒ handleError(handler, e) } + + try innerWrite() + catch { case e: IOException ⇒ handleError(handler, e) } } def closeReason = if (channel.socket.isOutputShutdown) ConfirmedClosed else PeerClosed - def handleClose(handler: ActorRef, closedEvent: ConnectionClosed): Unit = + def handleClose(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Unit = if (closedEvent == Aborted) { // close instantly if (TraceLogging) log.debug("Got Abort command. RESETing connection.") - doCloseConnection(handler, closedEvent) + doCloseConnection(handler, closeCommander, closedEvent) } else if (writePending) { // finish writing first if (TraceLogging) log.debug("Got Close command but write is still pending.") - context.become(closingWithPendingWrite(handler, closedEvent)) + context.become(closingWithPendingWrite(handler, closeCommander, closedEvent)) } else if (closedEvent == ConfirmedClosed) { // shutdown output and wait for confirmation if (TraceLogging) log.debug("Got ConfirmedClose command, sending FIN.") channel.socket.shutdownOutput() - context.become(closing(handler)) + context.become(closing(handler, closeCommander)) } else { // close now if (TraceLogging) log.debug("Got Close command, closing connection.") - doCloseConnection(handler, closedEvent) + doCloseConnection(handler, closeCommander, closedEvent) } - def doCloseConnection(handler: ActorRef, closedEvent: ConnectionClosed): Unit = { + def doCloseConnection(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Unit = { if (closedEvent == Aborted) abort() else channel.close() - closedMessage = (handler, closedEvent) + closedMessage = CloseInformation(Set(handler) ++ closeCommander, closedEvent) context.stop(self) } @@ -198,7 +205,7 @@ abstract class TcpConnection(val selector: ActorRef, } def handleError(handler: ActorRef, exception: IOException): Unit = { - closedMessage = (handler, ErrorClose(extractMsg(exception))) + closedMessage = CloseInformation(Set(handler), ErrorClose(extractMsg(exception))) throw exception } @@ -224,11 +231,11 @@ abstract class TcpConnection(val selector: ActorRef, override def postStop(): Unit = { if (closedMessage != null) { - val msg = closedMessage._2 - closedMessage._1 ! msg + val interestedInClose = + if (writePending) closedMessage.notificationsTo + pendingWrite.commander + else closedMessage.notificationsTo - if (writePending) - pendingWrite.commander ! msg + interestedInClose.foreach(_ ! closedMessage.closedEvent) } if (channel.isOpen) @@ -238,15 +245,36 @@ abstract class TcpConnection(val selector: ActorRef, override def postRestart(reason: Throwable): Unit = throw new IllegalStateException("Restarting not supported for connection actors.") - private[TcpConnection] case class PendingWrite(commander: ActorRef, ack: Any, buffer: ByteBuffer) { - def hasData = buffer.remaining() > 0 + private[TcpConnection] case class PendingWrite( + commander: ActorRef, + ack: Any, + remainingData: ByteString, + buffer: ByteBuffer) { + + def consume(writtenBytes: Int): PendingWrite = + if (buffer.remaining() == 0) { + buffer.clear() + val copied = remainingData.copyToBuffer(buffer) + buffer.flip() + copy(remainingData = remainingData.drop(copied)) + } else this + + def hasData = buffer.remaining() > 0 || remainingData.size > 0 def wantsAck = ack != NoAck } def createWrite(write: Write): PendingWrite = { - val buffer = acquireBuffer(write.data.length) - write.data.copyToBuffer(buffer) + val buffer = acquireBuffer() + val copied = write.data.copyToBuffer(buffer) buffer.flip() - PendingWrite(sender, write.ack, buffer) + PendingWrite(sender, write.ack, write.data.drop(copied), buffer) } + + /** + * Used to transport information to the postStop method to notify + * interested party about a connection close. + */ + private[TcpConnection] case class CloseInformation( + notificationsTo: Set[ActorRef], + closedEvent: ConnectionClosed) } diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index f826be7f17..ff8e1f241d 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -14,7 +14,7 @@ import java.net._ import scala.collection.immutable import scala.concurrent.duration._ import scala.util.control.NonFatal -import akka.actor.{ ActorRef, Terminated } +import akka.actor.{ PoisonPill, ActorRef, Terminated } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import akka.util.ByteString import TestUtils._ @@ -84,7 +84,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") buffer.clear() serverSideChannel.read(buffer) must be(0) writer.send(connectionActor, unackedWrite) - writer.expectNoMsg() + writer.expectNoMsg(500.millis) serverSideChannel.read(buffer) must be(10) buffer.flip() ByteString(buffer).take(10).decodeString("ASCII") must be("morestuff!") @@ -172,7 +172,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.send(connectionActor, Close) connectionHandler.expectMsg(Closed) - connectionHandler.expectNoMsg() + connectionHandler.expectNoMsg(500.millis) } "abort the connection and reply with `Aborted` upong reception of an `Abort` command" in withEstablishedConnection() { setup ⇒ @@ -293,8 +293,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") "close the connection when user handler dies while connecting" in withUnacceptedConnection() { setup ⇒ import setup._ - // simulate death of userHandler test probe - userHandler.send(connectionActor, akka.actor.Terminated(userHandler.ref)(false, false)) + userHandler.ref ! PoisonPill verifyActorTermination(connectionActor) } From 7c1fe8027949f6a3f016c2637febf95f89b7616c Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 14:55:02 +0100 Subject: [PATCH 18/66] remove commented code --- akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index ff8e1f241d..f1682d8077 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -96,7 +96,6 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") object Ack1 object Ack2 - //serverSideChannel.configureBlocking(false) clientSideChannel.socket.setSendBufferSize(1024) val writer = TestProbe() From 91548c7375515b0f16ed3a0b74985d1b9ef86a09 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 15:52:19 +0100 Subject: [PATCH 19/66] Revert "in DirectByteBufferPool keep buffers by soft reference" --- .../scala/akka/io/DirectByteBufferPool.scala | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala index 103be7bf6a..ed9ed4d175 100644 --- a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala +++ b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -3,7 +3,6 @@ package akka.io import java.util.concurrent.atomic.AtomicBoolean import java.nio.ByteBuffer import annotation.tailrec -import java.lang.ref.SoftReference trait WithBufferPool { def tcp: TcpExt @@ -29,20 +28,11 @@ trait BufferPool { */ private[akka] class DirectByteBufferPool(defaultBufferSize: Int, maxPoolEntries: Int) extends BufferPool { private[this] val locked = new AtomicBoolean(false) - private[this] val pool: Array[SoftReference[ByteBuffer]] = new Array[SoftReference[ByteBuffer]](maxPoolEntries) + private[this] val pool: Array[ByteBuffer] = new Array[ByteBuffer](maxPoolEntries) private[this] var buffersInPool: Int = 0 - def acquire(): ByteBuffer = { - val buffer = takeBufferFromPool() - - // allocate new buffer and clear outside the lock - if (buffer == null) - allocate(defaultBufferSize) - else { - buffer.clear() - buffer - } - } + def acquire(): ByteBuffer = + takeBufferFromPool() def release(buf: ByteBuffer): Unit = offerBufferToPool(buf) @@ -51,26 +41,29 @@ private[akka] class DirectByteBufferPool(defaultBufferSize: Int, maxPoolEntries: ByteBuffer.allocateDirect(size) @tailrec - private[this] final def takeBufferFromPool(): ByteBuffer = { - @tailrec def findBuffer(): ByteBuffer = - if (buffersInPool > 0) { - buffersInPool -= 1 - val buf = pool(buffersInPool).get() + private final def takeBufferFromPool(): ByteBuffer = + if (locked.compareAndSet(false, true)) { + val buffer = + try if (buffersInPool > 0) { + buffersInPool -= 1 + pool(buffersInPool) + } else null + finally locked.set(false) - if (buf != null) buf - else findBuffer() - } else null - - if (locked.compareAndSet(false, true)) - try findBuffer() finally locked.set(false) - else takeBufferFromPool() // spin while locked - } + // allocate new and clear outside the lock + if (buffer == null) + allocate(defaultBufferSize) + else { + buffer.clear() + buffer + } + } else takeBufferFromPool() // spin while locked @tailrec private final def offerBufferToPool(buf: ByteBuffer): Unit = if (locked.compareAndSet(false, true)) try if (buffersInPool < maxPoolEntries) { - pool(buffersInPool) = new SoftReference(buf) + pool(buffersInPool) = buf buffersInPool += 1 } // else let the buffer be gc'd finally locked.set(false) From b4baf66442287eb56df4078410fd9758ad8b184d Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 16:22:31 +0100 Subject: [PATCH 20/66] make asserting on received messages more resilient --- .../scala/akka/io/TcpConnectionSpec.scala | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index f1682d8077..4b6b5ec7c7 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -50,13 +50,12 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") import setup._ serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII"))) // emulate selector behavior - selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgType[Received].data.decodeString("ASCII") must be("testdata") + expectReceivedString("testdata") // have two packets in flight before the selector notices serverSideChannel.write(ByteBuffer.wrap("testdata2".getBytes("ASCII"))) serverSideChannel.write(ByteBuffer.wrap("testdata3".getBytes("ASCII"))) - selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgType[Received].data.decodeString("ASCII") must be("testdata2testdata3") + + expectReceivedString("testdata2testdata3") } "write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒ @@ -327,7 +326,6 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") unregisteredSetup: UnacceptedSetup, connectionHandler: TestProbe, serverSideChannel: SocketChannel) { - def userHandler: TestProbe = unregisteredSetup.userHandler def selector: TestProbe = unregisteredSetup.selector def connectionActor: TestActorRef[TcpOutgoingConnection] = unregisteredSetup.connectionActor @@ -350,6 +348,18 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") pullFromServerSide(remaining - read) } + @tailrec final def expectReceivedString(data: String): Unit = { + data.length must be > 0 + + selector.send(connectionActor, ChannelReadable) + + val gotReceived = connectionHandler.expectMsgType[Received] + val receivedString = gotReceived.data.decodeString("ASCII") + data.startsWith(receivedString) must be(true) + if (receivedString.length < data.length) + expectReceivedString(data.drop(receivedString.length)) + } + def assertThisConnectionActorTerminated(): Unit = { verifyActorTermination(connectionActor) clientSideChannel must not be ('open) From 3d34f57c5b0876ca1f62be979e751f4f8a0f1371 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 17:05:44 +0100 Subject: [PATCH 21/66] improve stability of tests, pullFromServerSide now uses real selector to check for conditions --- .../scala/akka/io/TcpConnectionSpec.scala | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 4b6b5ec7c7..55e3675a2b 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -6,7 +6,7 @@ package akka.io import scala.annotation.tailrec -import java.nio.channels.{ SelectionKey, SocketChannel, ServerSocketChannel } +import java.nio.channels.{ Selector, SelectionKey, SocketChannel, ServerSocketChannel } import java.nio.ByteBuffer import java.nio.channels.spi.SelectorProvider import java.io.IOException @@ -49,8 +49,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") "send incoming data to the connection handler" in withEstablishedConnection() { setup ⇒ import setup._ serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII"))) - // emulate selector behavior + expectReceivedString("testdata") + // have two packets in flight before the selector notices serverSideChannel.write(ByteBuffer.wrap("testdata2".getBytes("ASCII"))) serverSideChannel.write(ByteBuffer.wrap("testdata3".getBytes("ASCII"))) @@ -60,7 +61,6 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") "write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒ import setup._ - serverSideChannel.configureBlocking(false) object Ack val writer = TestProbe() @@ -331,21 +331,33 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") def connectionActor: TestActorRef[TcpOutgoingConnection] = unregisteredSetup.connectionActor def clientSideChannel: SocketChannel = unregisteredSetup.clientSideChannel + val (nioSelector, clientSelectionKey) = { + val sel = SelectorProvider.provider().openSelector() + val key = clientSideChannel.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE) + (sel, key) + } + val serverSelectionKey = + serverSideChannel.register(nioSelector, SelectionKey.OP_READ | SelectionKey.OP_WRITE) + val buffer = ByteBuffer.allocate(TestSize) @tailrec final def pullFromServerSide(remaining: Int): Unit = if (remaining > 0) { - if (selector.msgAvailable) { - selector.expectMsg(WriteInterest) + nioSelector.select(100) + if (clientSelectionKey.isValid && clientSelectionKey.isWritable) selector.send(connectionActor, ChannelWritable) - } - buffer.clear() - val read = serverSideChannel.read(buffer) - if (read == 0) - throw new IllegalStateException("Didn't make any progress") - else if (read == -1) - throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) - pullFromServerSide(remaining - read) + if (serverSelectionKey.isValid && serverSelectionKey.isReadable) { + buffer.clear() + val read = serverSideChannel.read(buffer) + if (read == 0) + throw new IllegalStateException("Didn't make any progress") + else if (read == -1) + throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) + + pullFromServerSide(remaining - read) + } else + pullFromServerSide(remaining) + } @tailrec final def expectReceivedString(data: String): Unit = { @@ -391,6 +403,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") localServer.configureBlocking(true) val serverSideChannel = localServer.accept() + serverSideChannel.configureBlocking(false) serverSideChannel must not be (null) selector.send(connectionActor, ChannelConnectable) From 6d5458dfebde084e52b371cc1c77844e4cd6269e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 17:26:52 +0100 Subject: [PATCH 22/66] further improve pullFromServerSide stability --- .../scala/akka/io/TcpConnectionSpec.scala | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 55e3675a2b..01f04fb147 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -340,25 +340,41 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") serverSideChannel.register(nioSelector, SelectionKey.OP_READ | SelectionKey.OP_WRITE) val buffer = ByteBuffer.allocate(TestSize) - @tailrec final def pullFromServerSide(remaining: Int): Unit = - if (remaining > 0) { - nioSelector.select(100) - if (clientSelectionKey.isValid && clientSelectionKey.isWritable) - selector.send(connectionActor, ChannelWritable) - if (serverSelectionKey.isValid && serverSelectionKey.isReadable) { - buffer.clear() - val read = serverSideChannel.read(buffer) - if (read == 0) - throw new IllegalStateException("Didn't make any progress") - else if (read == -1) - throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) + /** + * Tries to simultaneously act on client and server side to read from the server + * all pending data from the client. + */ + @tailrec final def pullFromServerSide(remaining: Int, waitingForWrite: Boolean = true): Unit = + if (remaining > 0) + pullFromServerSide(remaining - tryReading(), checkForWriteInterest(waitingForWrite)) - pullFromServerSide(remaining - read) - } else - pullFromServerSide(remaining) + private def checkForWriteInterest(waitingForWrite: Boolean): Boolean = { + val waitingForWrite0 = + if (selector.msgAvailable) { + selector.expectMsg(WriteInterest) + true + } else waitingForWrite - } + nioSelector.select(1) + + if (waitingForWrite0 && clientSelectionKey.isValid && clientSelectionKey.isWritable) { + selector.send(connectionActor, ChannelWritable) + false + } else waitingForWrite0 + } + private def tryReading(): Int = + if (serverSelectionKey.isValid && serverSelectionKey.isReadable) { + buffer.clear() + val read = serverSideChannel.read(buffer) + if (read == 0) + throw new IllegalStateException("Didn't make any progress") + else if (read == -1) + throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) + + read + } else + 0 @tailrec final def expectReceivedString(data: String): Unit = { data.length must be > 0 From 54c3d77db25adae603d0db777b5c6b87be0f0af2 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 17:41:51 +0100 Subject: [PATCH 23/66] add more integration tests --- .../test/scala/akka/io/IntegrationSpec.scala | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala index f69b8347d0..93c78ecab2 100644 --- a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala @@ -9,6 +9,8 @@ import akka.actor.ActorRef import akka.util.ByteString import Tcp._ import TestUtils._ +import collection.immutable +import annotation.tailrec class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { @@ -29,6 +31,15 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { verifyActorTermination(serverConnection) } + "properly handle connection abort from one side" in new TestSetup { + val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() + clientHandler.send(clientConnection, Abort) + clientHandler.expectMsg(Aborted) + serverHandler.expectMsgType[ErrorClose] + verifyActorTermination(clientConnection) + verifyActorTermination(serverConnection) + } + "properly complete one client/server request/response cycle" in new TestSetup { val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() @@ -48,6 +59,20 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { verifyActorTermination(serverConnection) } + "waiting for writes works with backpressure" in new TestSetup { + val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() + + serverHandler.send(serverConnection, Write(ByteString(Array.fill[Byte](100000)(0)), 4223)) + serverHandler.expectMsg(4223) + + expectReceivedData(clientHandler, 100000) + + override def bindOptions: immutable.Traversable[SocketOption] = + List(SO.SendBufferSize(1024)) + + override def connectOptions: immutable.Traversable[SocketOption] = + List(SO.ReceiveBufferSize(1024)) + } ////////////////////////////////////// ///////// more tests to come ///////// ////////////////////////////////////// @@ -61,13 +86,13 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { def bindServer(): Unit = { val bindCommander = TestProbe() - bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint)) + bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint, options = bindOptions)) bindCommander.expectMsg(Bound) } def establishNewClientConnection(): (TestProbe, ActorRef, TestProbe, ActorRef) = { val connectCommander = TestProbe() - connectCommander.send(IO(Tcp), Connect(endpoint)) + connectCommander.send(IO(Tcp), Connect(endpoint, options = connectOptions)) val Connected(`endpoint`, localAddress) = connectCommander.expectMsgType[Connected] val clientHandler = TestProbe() connectCommander.sender ! Register(clientHandler.ref) @@ -78,6 +103,18 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { (clientHandler, connectCommander.sender, serverHandler, bindHandler.sender) } + + @tailrec final def expectReceivedData(handler: TestProbe, remaining: Int): Unit = + if (remaining > 0) { + val recv = handler.expectMsgType[Received] + expectReceivedData(handler, remaining - recv.data.size) + } + + /** allow overriding socket options for server side channel */ + def bindOptions: collection.immutable.Traversable[SocketOption] = Nil + + /** allow overriding socket options for client side channel */ + def connectOptions: collection.immutable.Traversable[SocketOption] = Nil } -} \ No newline at end of file +} From e853a1d2ae782c51786b1eee4cd6429d538ed8b8 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Mon, 21 Jan 2013 18:26:56 +0100 Subject: [PATCH 24/66] fix race condition when trying to assert closed channel on the server side --- akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 01f04fb147..79dbf8ec0c 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -161,6 +161,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") closeCommander.expectMsg(Closed) assertThisConnectionActorTerminated() + nioSelector.select(2000) + val buffer = ByteBuffer.allocate(1) serverSideChannel.read(buffer) must be(-1) } @@ -207,6 +209,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.expectNoMsg(100.millis) // not yet val buffer = ByteBuffer.allocate(1) + nioSelector.select(2000) serverSideChannel.read(buffer) must be(-1) serverSideChannel.close() From 3687697aedb572d1d979d5eea9c5f216546c7e4e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 11:35:15 +0100 Subject: [PATCH 25/66] use proper selection strategy to try to improve test stability --- .../scala/akka/io/TcpConnectionSpec.scala | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 79dbf8ec0c..30124926b2 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -161,7 +161,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") closeCommander.expectMsg(Closed) assertThisConnectionActorTerminated() - nioSelector.select(2000) + checkFor(serverSelectionKey, SelectionKey.OP_READ, 2000) val buffer = ByteBuffer.allocate(1) serverSideChannel.read(buffer) must be(-1) @@ -209,7 +209,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.expectNoMsg(100.millis) // not yet val buffer = ByteBuffer.allocate(1) - nioSelector.select(2000) + checkFor(serverSelectionKey, SelectionKey.OP_READ, 2000) serverSideChannel.read(buffer) must be(-1) serverSideChannel.close() @@ -334,13 +334,35 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") def connectionActor: TestActorRef[TcpOutgoingConnection] = unregisteredSetup.connectionActor def clientSideChannel: SocketChannel = unregisteredSetup.clientSideChannel - val (nioSelector, clientSelectionKey) = { + val nioSelector = SelectorProvider.provider().openSelector() + + val clientSelectionKey = registerChannel(clientSideChannel, "client") + val serverSelectionKey = registerChannel(serverSideChannel, "server") + + def registerChannel(channel: SocketChannel, name: String): SelectionKey = { + val res = channel.register(nioSelector, 0) + res.attach(name) + res + } + + def checkFor(key: SelectionKey, interest: Int, millis: Int = 100): Boolean = + if (key.isValid) { + if ((key.readyOps() & interest) != 0) true + else { + key.interestOps(interest) + val ret = nioSelector.select(millis) + key.interestOps(0) + + ret > 0 && nioSelector.selectedKeys().contains(key) && key.isValid && + (key.readyOps() & interest) != 0 + } + } else false + + def openSelectorFor(channel: SocketChannel, interests: Int): (Selector, SelectionKey) = { val sel = SelectorProvider.provider().openSelector() - val key = clientSideChannel.register(sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE) + val key = channel.register(sel, interests) (sel, key) } - val serverSelectionKey = - serverSideChannel.register(nioSelector, SelectionKey.OP_READ | SelectionKey.OP_WRITE) val buffer = ByteBuffer.allocate(TestSize) @@ -348,36 +370,37 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") * Tries to simultaneously act on client and server side to read from the server * all pending data from the client. */ - @tailrec final def pullFromServerSide(remaining: Int, waitingForWrite: Boolean = true): Unit = - if (remaining > 0) - pullFromServerSide(remaining - tryReading(), checkForWriteInterest(waitingForWrite)) - - private def checkForWriteInterest(waitingForWrite: Boolean): Boolean = { - val waitingForWrite0 = + @tailrec final def pullFromServerSide(remaining: Int, remainingTries: Int = 1000): Unit = + if (remainingTries <= 0) + throw new AssertionError("Pulling took too many loops") + else if (remaining > 0) { if (selector.msgAvailable) { selector.expectMsg(WriteInterest) - true - } else waitingForWrite + clientSelectionKey.interestOps(SelectionKey.OP_WRITE) + } - nioSelector.select(1) + serverSelectionKey.interestOps(SelectionKey.OP_READ) + nioSelector.select(10) + if (nioSelector.selectedKeys().contains(clientSelectionKey)) { + clientSelectionKey.interestOps(0) + selector.send(connectionActor, ChannelWritable) + } - if (waitingForWrite0 && clientSelectionKey.isValid && clientSelectionKey.isWritable) { - selector.send(connectionActor, ChannelWritable) - false - } else waitingForWrite0 + val read = + if (nioSelector.selectedKeys().contains(serverSelectionKey)) tryReading() + else 0 + + pullFromServerSide(remaining - read, remainingTries - 1) + } + + private def tryReading(): Int = { + buffer.clear() + val read = serverSideChannel.read(buffer) + + if (read == -1) + throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) + else read } - private def tryReading(): Int = - if (serverSelectionKey.isValid && serverSelectionKey.isReadable) { - buffer.clear() - val read = serverSideChannel.read(buffer) - if (read == 0) - throw new IllegalStateException("Didn't make any progress") - else if (read == -1) - throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) - - read - } else - 0 @tailrec final def expectReceivedString(data: String): Unit = { data.length must be > 0 From a53848edfadd55aa517084e43ca97e70c29944f5 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 11:41:40 +0100 Subject: [PATCH 26/66] properly cleanup selector in tests --- akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 30124926b2..659add2f83 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -390,6 +390,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") if (nioSelector.selectedKeys().contains(serverSelectionKey)) tryReading() else 0 + nioSelector.selectedKeys().clear() + pullFromServerSide(remaining - read, remainingTries - 1) } @@ -397,7 +399,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") buffer.clear() val read = serverSideChannel.read(buffer) - if (read == -1) + if (read == 0) + throw new IllegalStateException("Made no progress") + else if (read == -1) throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining) else read } From ba922fa2d75be264de9063326d99fae3f3b57191 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 21 Jan 2013 17:02:20 +0100 Subject: [PATCH 27/66] Change socket options type from `immutable.Traversable` to `Traversable` The problem with `immutable.Traversable` is that it doesn't allow the use of `Seq.apply` for socket options, since `Seq` is aliased to `scala.collection.Seq` and not `scala.collection.immutable.Seq` in `package object scala`. Even though technically nicer `immutable.Traversable` therefore hinders usability of the API, for a benefit that we don't consider worth the cost. Conflicts: akka-io/src/main/scala/akka/io/TcpConnection.scala --- akka-io/src/main/scala/akka/io/Tcp.scala | 6 +++--- akka-io/src/main/scala/akka/io/TcpConnection.scala | 4 ++-- akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala | 2 +- akka-io/src/main/scala/akka/io/TcpListener.scala | 2 +- akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala | 5 +++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 7bb13987d3..7df771caf1 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -132,11 +132,11 @@ object Tcp extends ExtensionKey[TcpExt] { case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None, - options: immutable.Traversable[SocketOption] = Nil) extends Command + options: Traversable[SocketOption] = Nil) extends Command case class Bind(handler: ActorRef, endpoint: InetSocketAddress, backlog: Int = 100, - options: immutable.Traversable[SocketOption] = Nil) extends Command + options: Traversable[SocketOption] = Nil) extends Command case class Register(handler: ActorRef) extends Command case object Unbind extends Command @@ -185,7 +185,7 @@ object Tcp extends ExtensionKey[TcpExt] { case class RegisterOutgoingConnection(channel: SocketChannel) case class RegisterServerSocketChannel(channel: ServerSocketChannel) case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, - options: immutable.Traversable[SocketOption]) extends Command + options: Traversable[SocketOption]) extends Command case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } case object ChannelConnectable case object ChannelAcceptable diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index b08ba8df48..632b28bd59 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -100,8 +100,8 @@ abstract class TcpConnection(val selector: ActorRef, // AUXILIARIES and IMPLEMENTATION - /** use in subclasses to start the common machinery above once a channel is connected */ - def completeConnect(commander: ActorRef, options: immutable.Traversable[SocketOption]): Unit = { + /** used in subclasses to start the common machinery above once a channel is connected */ + def completeConnect(commander: ActorRef, options: Traversable[SocketOption]): Unit = { options.foreach(_.afterConnect(channel.socket)) commander ! Connected( diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala index 902dde0233..5a4a9a5d94 100644 --- a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -17,7 +17,7 @@ class TcpIncomingConnection(_selector: ActorRef, _channel: SocketChannel, _tcp: TcpExt, handler: ActorRef, - options: immutable.Traversable[SocketOption]) + options: Traversable[SocketOption]) extends TcpConnection(_selector, _channel, _tcp) { context.watch(handler) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index 5bf456faf3..fac5b90c9c 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -18,7 +18,7 @@ class TcpListener(selector: ActorRef, backlog: Int, bindCommander: ActorRef, settings: TcpExt#Settings, - options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { + options: Traversable[SocketOption]) extends Actor with ActorLogging { context.watch(handler) // sign death pact val channel = { diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index b33e6605e8..fcaecc0bc4 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -20,7 +20,7 @@ class TcpOutgoingConnection(_selector: ActorRef, commander: ActorRef, remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress], - options: immutable.Traversable[SocketOption]) + options: Traversable[SocketOption]) extends TcpConnection(_selector, TcpOutgoingConnection.newSocketChannel(), _tcp) { context.watch(commander) // sign death pact @@ -34,11 +34,12 @@ class TcpOutgoingConnection(_selector: ActorRef, else { selector ! RegisterOutgoingConnection(channel) context.become(connecting(commander, options)) + Seq(1,2,3) } def receive: Receive = PartialFunction.empty - def connecting(commander: ActorRef, options: immutable.Traversable[SocketOption]): Receive = { + def connecting(commander: ActorRef, options: Traversable[SocketOption]): Receive = { case ChannelConnectable ⇒ try { val connected = channel.finishConnect() From 79accb810e2458698e8b47558dc4f8c6063d5d2e Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 22 Jan 2013 13:48:36 +0100 Subject: [PATCH 28/66] Cleanup, small fixes and more and better tests --- akka-io/src/main/resources/reference.conf | 4 +- .../scala/akka/io/TcpOutgoingConnection.scala | 2 - .../src/main/scala/akka/io/TcpSelector.scala | 14 +++- .../scala/akka/io/CapacityLimitSpec.scala | 33 +++++++++ .../test/scala/akka/io/IntegrationSpec.scala | 74 +++---------------- .../akka/io/IntegrationSpecSupport.scala | 54 ++++++++++++++ .../scala/akka/io/TcpConnectionSpec.scala | 4 +- .../test/scala/akka/io/TcpListenerSpec.scala | 8 +- .../akka/io/TemporaryServerAddress.scala | 19 ----- .../src/test/scala/akka/io/TestUtils.scala | 31 +++----- 10 files changed, 127 insertions(+), 116 deletions(-) create mode 100644 akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala create mode 100644 akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala delete mode 100644 akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index a907706b02..33927a9d45 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -27,8 +27,8 @@ akka { # no intrinsic general limit, this setting is meant to enable DoS # protection by limiting the number of concurrently connected clients. # Also note that this is a "soft" limit; in certain cases the implementation - # will accept a few connections more than the number configured here. - # Set to 0 for "unlimited". + # will accept a few connections more or a few less than the number configured + # here. Set to 0 for "unlimited". max-channels = 256000 # The select loop can be used in two modes: diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index fcaecc0bc4..dfd5e93771 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -7,7 +7,6 @@ package akka.io import java.net.InetSocketAddress import java.io.IOException import java.nio.channels.SocketChannel -import scala.collection.immutable import akka.actor.ActorRef import Tcp._ @@ -34,7 +33,6 @@ class TcpOutgoingConnection(_selector: ActorRef, else { selector ! RegisterOutgoingConnection(channel) context.become(connecting(commander, options)) - Seq(1,2,3) } def receive: Receive = PartialFunction.empty diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 48f1c5ab36..69f5ff89c9 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -28,6 +28,8 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) + case StopReading ⇒ execute(disableInterest(OP_READ, sender)) + case cmd: RegisterIncomingConnection ⇒ handleIncomingConnection(cmd, SelectorAssociationRetries) @@ -148,7 +150,7 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin } } - // TODO: evaluate whether we could run this on the TcpSelector actor itself rather than + // TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap // rather than an unsynchronized one, but since switching interest ops is so frequent // the change might be beneficial, provided the underlying implementation really is thread-safe @@ -161,6 +163,14 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin } } + def disableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps & ~op) + } + } + def unregister(child: ActorRef) = new Task { def tryRun() { @@ -182,6 +192,7 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin while (iterator.hasNext) { val key = iterator.next if (key.isValid) { + key.interestOps(0) // prevent immediate reselection by always clearing val connection = key.attachment.asInstanceOf[ActorRef] key.readyOps match { case OP_READ ⇒ connection ! ChannelReadable @@ -191,7 +202,6 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable case x ⇒ log.warning("Invalid readyOps: {}", x) } - key.interestOps(0) // prevent immediate reselection by always clearing } else log.warning("Invalid selection key: {}", key) } keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected diff --git a/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala b/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala new file mode 100644 index 0000000000..f7dec60449 --- /dev/null +++ b/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import akka.testkit.{ TestProbe, AkkaSpec } +import Tcp._ +import TestUtils._ + +class CapacityLimitSpec extends AkkaSpec("akka.loglevel = ERROR\nakka.io.tcp.max-channels = 4") + with IntegrationSpecSupport { + + "The TCP transport implementation" should { + + "reply with CommandFailed to a Bind or Connect command if max-channels capacity has been reached" in new TestSetup { + establishNewClientConnection() + + // we now have three channels registered: a listener, a server connection and a client connection + // so register one more channel + val bindCommander = TestProbe() + bindCommander.send(IO(Tcp), Bind(bindHandler.ref, temporaryServerAddress())) + bindCommander.expectMsg(Bound) + + // we are now at the configured max-channel capacity of 4 + val bindToFail = Bind(bindHandler.ref, temporaryServerAddress()) + bindCommander.send(IO(Tcp), bindToFail) + bindCommander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (bindToFail) + } + + } + +} diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala index 93c78ecab2..87df405611 100644 --- a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala @@ -4,24 +4,17 @@ package akka.io -import akka.testkit.{ TestProbe, AkkaSpec } -import akka.actor.ActorRef +import akka.testkit.AkkaSpec import akka.util.ByteString import Tcp._ import TestUtils._ -import collection.immutable -import annotation.tailrec -class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { +class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationSpecSupport { "The TCP transport implementation" should { "properly bind a test server" in new TestSetup - "allow connecting to the test server" in new TestSetup { - establishNewClientConnection() - } - "allow connecting to and disconnecting from the test server" in new TestSetup { val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() clientHandler.send(clientConnection, Close) @@ -43,12 +36,12 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { "properly complete one client/server request/response cycle" in new TestSetup { val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() - clientHandler.send(clientConnection, Write(ByteString("Captain on the bridge!"), 42)) - clientHandler.expectMsg(42) + clientHandler.send(clientConnection, Write(ByteString("Captain on the bridge!"), 'Aye)) + clientHandler.expectMsg('Aye) serverHandler.expectMsgType[Received].data.decodeString("ASCII") must be("Captain on the bridge!") - serverHandler.send(serverConnection, Write(ByteString("For the king!"), 4242)) - serverHandler.expectMsg(4242) + serverHandler.send(serverConnection, Write(ByteString("For the king!"), 'Yes)) + serverHandler.expectMsg('Yes) clientHandler.expectMsgType[Received].data.decodeString("ASCII") must be("For the king!") serverHandler.send(serverConnection, Close) @@ -59,62 +52,17 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") { verifyActorTermination(serverConnection) } - "waiting for writes works with backpressure" in new TestSetup { + "support waiting for writes with backpressure" in new TestSetup { val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() - serverHandler.send(serverConnection, Write(ByteString(Array.fill[Byte](100000)(0)), 4223)) - serverHandler.expectMsg(4223) + serverHandler.send(serverConnection, Write(ByteString(Array.fill[Byte](100000)(0)), 'Ack)) + serverHandler.expectMsg('Ack) expectReceivedData(clientHandler, 100000) - override def bindOptions: immutable.Traversable[SocketOption] = - List(SO.SendBufferSize(1024)) - - override def connectOptions: immutable.Traversable[SocketOption] = - List(SO.ReceiveBufferSize(1024)) + override def bindOptions = List(SO.SendBufferSize(1024)) + override def connectOptions = List(SO.ReceiveBufferSize(1024)) } - ////////////////////////////////////// - ///////// more tests to come ///////// - ////////////////////////////////////// - } - - class TestSetup { - val bindHandler = TestProbe() - val endpoint = TemporaryServerAddress() - - bindServer() - - def bindServer(): Unit = { - val bindCommander = TestProbe() - bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint, options = bindOptions)) - bindCommander.expectMsg(Bound) - } - - def establishNewClientConnection(): (TestProbe, ActorRef, TestProbe, ActorRef) = { - val connectCommander = TestProbe() - connectCommander.send(IO(Tcp), Connect(endpoint, options = connectOptions)) - val Connected(`endpoint`, localAddress) = connectCommander.expectMsgType[Connected] - val clientHandler = TestProbe() - connectCommander.sender ! Register(clientHandler.ref) - - val Connected(`localAddress`, `endpoint`) = bindHandler.expectMsgType[Connected] - val serverHandler = TestProbe() - bindHandler.sender ! Register(serverHandler.ref) - - (clientHandler, connectCommander.sender, serverHandler, bindHandler.sender) - } - - @tailrec final def expectReceivedData(handler: TestProbe, remaining: Int): Unit = - if (remaining > 0) { - val recv = handler.expectMsgType[Received] - expectReceivedData(handler, remaining - recv.data.size) - } - - /** allow overriding socket options for server side channel */ - def bindOptions: collection.immutable.Traversable[SocketOption] = Nil - - /** allow overriding socket options for client side channel */ - def connectOptions: collection.immutable.Traversable[SocketOption] = Nil } } diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala b/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala new file mode 100644 index 0000000000..ea93b7809a --- /dev/null +++ b/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.io + +import scala.annotation.tailrec +import akka.testkit.{ AkkaSpec, TestProbe } +import akka.actor.ActorRef +import Tcp._ +import TestUtils._ + +trait IntegrationSpecSupport { _: AkkaSpec ⇒ + + class TestSetup { + val bindHandler = TestProbe() + val endpoint = temporaryServerAddress() + + bindServer() + + def bindServer(): Unit = { + val bindCommander = TestProbe() + bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint, options = bindOptions)) + bindCommander.expectMsg(Bound) + } + + def establishNewClientConnection(): (TestProbe, ActorRef, TestProbe, ActorRef) = { + val connectCommander = TestProbe() + connectCommander.send(IO(Tcp), Connect(endpoint, options = connectOptions)) + val Connected(`endpoint`, localAddress) = connectCommander.expectMsgType[Connected] + val clientHandler = TestProbe() + connectCommander.sender ! Register(clientHandler.ref) + + val Connected(`localAddress`, `endpoint`) = bindHandler.expectMsgType[Connected] + val serverHandler = TestProbe() + bindHandler.sender ! Register(serverHandler.ref) + + (clientHandler, connectCommander.sender, serverHandler, bindHandler.sender) + } + + @tailrec final def expectReceivedData(handler: TestProbe, remaining: Int): Unit = + if (remaining > 0) { + val recv = handler.expectMsgType[Received] + expectReceivedData(handler, remaining - recv.data.size) + } + + /** allow overriding socket options for server side channel */ + def bindOptions: Traversable[SocketOption] = Nil + + /** allow overriding socket options for client side channel */ + def connectOptions: Traversable[SocketOption] = Nil + } + +} diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 659add2f83..7cdcc4f202 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -21,7 +21,7 @@ import TestUtils._ import Tcp._ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { - val serverAddress = TemporaryServerAddress() + val serverAddress = temporaryServerAddress() "An outgoing connection" must { // common behavior @@ -265,7 +265,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") verifyActorTermination(connectionActor) } - val UnboundAddress = TemporaryServerAddress() + val UnboundAddress = temporaryServerAddress() "report failed connection attempt when target is unreachable" in withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ import setup._ diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index 5d238f28e9..8db5cb0d74 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -5,9 +5,7 @@ package akka.io import java.net.Socket -import scala.annotation.tailrec import scala.concurrent.duration._ -import org.scalatest.exceptions.TestFailedException import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import Tcp._ @@ -64,9 +62,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) - within(1.second) { - channel.isOpen must be(false) - } + awaitCond(!channel.isOpen) } } @@ -78,7 +74,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { val handlerRef = handler.ref val bindCommander = TestProbe() val parent = TestProbe() - val endpoint = TemporaryServerAddress() + val endpoint = TestUtils.temporaryServerAddress() private val parentRef = TestActorRef(new ListenerParent) def bindListener() { diff --git a/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala b/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala deleted file mode 100644 index 20b4e3c16b..0000000000 --- a/akka-io/src/test/scala/akka/io/TemporaryServerAddress.scala +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (C) 2009-2013 Typesafe Inc. - */ - -package akka.io - -import java.nio.channels.ServerSocketChannel -import java.net.InetSocketAddress - -object TemporaryServerAddress { - - def apply(address: String = "127.0.0.1"): InetSocketAddress = { - val serverSocket = ServerSocketChannel.open() - serverSocket.socket.bind(new InetSocketAddress(address, 0)) - val port = serverSocket.socket.getLocalPort - serverSocket.close() - new InetSocketAddress(address, port) - } -} diff --git a/akka-io/src/test/scala/akka/io/TestUtils.scala b/akka-io/src/test/scala/akka/io/TestUtils.scala index 339599d9cb..d084060dc1 100644 --- a/akka-io/src/test/scala/akka/io/TestUtils.scala +++ b/akka-io/src/test/scala/akka/io/TestUtils.scala @@ -4,34 +4,25 @@ package akka.io -import scala.annotation.tailrec -import scala.concurrent.duration._ -import org.scalatest.exceptions.TestFailedException -import akka.actor.{ ActorSystem, ActorRef, Terminated } +import java.net.InetSocketAddress +import java.nio.channels.ServerSocketChannel +import akka.actor.{ Terminated, ActorSystem, ActorRef } import akka.testkit.TestProbe object TestUtils { + def temporaryServerAddress(address: String = "127.0.0.1"): InetSocketAddress = { + val serverSocket = ServerSocketChannel.open() + serverSocket.socket.bind(new InetSocketAddress(address, 0)) + val port = serverSocket.socket.getLocalPort + serverSocket.close() + new InetSocketAddress(address, port) + } + def verifyActorTermination(actor: ActorRef)(implicit system: ActorSystem): Unit = { val watcher = TestProbe() watcher.watch(actor) assert(watcher.expectMsgType[Terminated].actor == actor) } - // why doesn't scalatest provide this by default? (specs2 does with its `eventually` matcher modifier!) - // Due to its general utility, should this be moved to the testkit? - @tailrec final def within(timeLeft: Duration, retrySpan: Duration = 10.millis)(test: ⇒ Unit): Unit = { - val start = System.currentTimeMillis - lazy val end = System.currentTimeMillis - val tryAgain = try { - test - false - } catch { - case (_: TestFailedException | _: AssertionError) if timeLeft.toMillis + start - end > 0 ⇒ true - } - if (tryAgain) { - Thread.sleep(retrySpan.toMillis) - within(timeLeft - Duration(end - start, MILLISECONDS) - retrySpan)(test) - } - } } From 570b02f569d7ca62b3feb2f5916e869201c0208c Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 22 Jan 2013 14:10:36 +0100 Subject: [PATCH 29/66] Decrease visibility of internals to `private[io]`, clean up imports --- akka-io/src/main/scala/akka/io/Tcp.scala | 16 +--------------- .../main/scala/akka/io/TcpConnection.scala | 14 +++++++------- .../scala/akka/io/TcpIncomingConnection.scala | 11 +++++------ .../src/main/scala/akka/io/TcpListener.scala | 16 ++++++++-------- .../src/main/scala/akka/io/TcpManager.scala | 2 +- .../scala/akka/io/TcpOutgoingConnection.scala | 13 +++++++------ .../src/main/scala/akka/io/TcpSelector.scala | 19 ++++++++++++++++++- .../scala/akka/io/TcpConnectionSpec.scala | 1 + .../test/scala/akka/io/TcpListenerSpec.scala | 1 + 9 files changed, 49 insertions(+), 44 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 7df771caf1..e9b7a07409 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -128,7 +128,7 @@ object Tcp extends ExtensionKey[TcpExt] { } /// COMMANDS - sealed trait Command + trait Command case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None, @@ -180,20 +180,6 @@ object Tcp extends ExtensionKey[TcpExt] { case object ConfirmedClosed extends ConnectionClosed case object PeerClosed extends ConnectionClosed case class ErrorClose(cause: String) extends ConnectionClosed - - /// INTERNAL - case class RegisterOutgoingConnection(channel: SocketChannel) - case class RegisterServerSocketChannel(channel: ServerSocketChannel) - case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, - options: Traversable[SocketOption]) extends Command - case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } - case object ChannelConnectable - case object ChannelAcceptable - case object ChannelReadable - case object ChannelWritable - case object AcceptInterest - case object ReadInterest - case object WriteInterest } class TcpExt(system: ExtendedActorSystem) extends IO.Extension { diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 632b28bd59..f6f1d099ee 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -7,21 +7,21 @@ package akka.io import java.net.InetSocketAddress import java.io.IOException import java.nio.channels.SocketChannel +import java.nio.ByteBuffer +import scala.annotation.tailrec import scala.util.control.NonFatal -import scala.collection.immutable import scala.concurrent.duration._ import akka.actor._ import akka.util.ByteString import Tcp._ -import annotation.tailrec -import java.nio.ByteBuffer +import TcpSelector._ /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ -abstract class TcpConnection(val selector: ActorRef, - val channel: SocketChannel, - val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { +private[io] abstract class TcpConnection(val selector: ActorRef, + val channel: SocketChannel, + val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { import tcp.Settings._ var pendingWrite: PendingWrite = null @@ -277,4 +277,4 @@ abstract class TcpConnection(val selector: ActorRef, private[TcpConnection] case class CloseInformation( notificationsTo: Set[ActorRef], closedEvent: ConnectionClosed) -} +} \ No newline at end of file diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala index 5a4a9a5d94..503c810f44 100644 --- a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -5,7 +5,6 @@ package akka.io import java.nio.channels.SocketChannel -import scala.collection.immutable import akka.actor.ActorRef import Tcp.SocketOption @@ -13,11 +12,11 @@ import Tcp.SocketOption * An actor handling the connection state machine for an incoming, already connected * SocketChannel. */ -class TcpIncomingConnection(_selector: ActorRef, - _channel: SocketChannel, - _tcp: TcpExt, - handler: ActorRef, - options: Traversable[SocketOption]) +private[io] class TcpIncomingConnection(_selector: ActorRef, + _channel: SocketChannel, + _tcp: TcpExt, + handler: ActorRef, + options: Traversable[SocketOption]) extends TcpConnection(_selector, _channel, _tcp) { context.watch(handler) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index fac5b90c9c..79e37505ad 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -7,18 +7,18 @@ package akka.io import java.net.InetSocketAddress import java.nio.channels.ServerSocketChannel import scala.annotation.tailrec -import scala.collection.immutable import scala.util.control.NonFatal import akka.actor.{ ActorLogging, ActorRef, Actor } +import TcpSelector._ import Tcp._ -class TcpListener(selector: ActorRef, - handler: ActorRef, - endpoint: InetSocketAddress, - backlog: Int, - bindCommander: ActorRef, - settings: TcpExt#Settings, - options: Traversable[SocketOption]) extends Actor with ActorLogging { +private[io] class TcpListener(selector: ActorRef, + handler: ActorRef, + endpoint: InetSocketAddress, + backlog: Int, + bindCommander: ActorRef, + settings: TcpExt#Settings, + options: Traversable[SocketOption]) extends Actor with ActorLogging { context.watch(handler) // sign death pact val channel = { diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-io/src/main/scala/akka/io/TcpManager.scala index 6f2046d75f..0ef1ca3e52 100644 --- a/akka-io/src/main/scala/akka/io/TcpManager.scala +++ b/akka-io/src/main/scala/akka/io/TcpManager.scala @@ -43,7 +43,7 @@ import Tcp._ * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. * */ -class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { +private[io] class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { val selectorPool = context.actorOf( props = Props(new TcpSelector(self, tcp)).withRouter(RandomRouter(tcp.Settings.NrOfSelectors)), diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index dfd5e93771..c3276723b8 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -8,18 +8,19 @@ import java.net.InetSocketAddress import java.io.IOException import java.nio.channels.SocketChannel import akka.actor.ActorRef +import TcpSelector._ import Tcp._ /** * An actor handling the connection state machine for an outgoing connection * to be established. */ -class TcpOutgoingConnection(_selector: ActorRef, - _tcp: TcpExt, - commander: ActorRef, - remoteAddress: InetSocketAddress, - localAddress: Option[InetSocketAddress], - options: Traversable[SocketOption]) +private[io] class TcpOutgoingConnection(_selector: ActorRef, + _tcp: TcpExt, + commander: ActorRef, + remoteAddress: InetSocketAddress, + localAddress: Option[InetSocketAddress], + options: Traversable[SocketOption]) extends TcpConnection(_selector, TcpOutgoingConnection.newSocketChannel(), _tcp) { context.watch(commander) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 69f5ff89c9..ec8e70fe9a 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -14,7 +14,8 @@ import scala.concurrent.duration._ import akka.actor._ import Tcp._ -class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLogging { +private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLogging { + import TcpSelector._ import tcp.Settings._ @volatile var childrenKeys = HashMap.empty[String, SelectionKey] @@ -222,4 +223,20 @@ class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLoggin } } } +} + +private[io] object TcpSelector { + case class RegisterOutgoingConnection(channel: SocketChannel) + case class RegisterServerSocketChannel(channel: ServerSocketChannel) + case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, + options: Traversable[SocketOption]) extends Tcp.Command + case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } + + case object ChannelConnectable + case object ChannelAcceptable + case object ChannelReadable + case object ChannelWritable + case object AcceptInterest + case object ReadInterest + case object WriteInterest } \ No newline at end of file diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 7cdcc4f202..6881a54fb2 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -18,6 +18,7 @@ import akka.actor.{ PoisonPill, ActorRef, Terminated } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import akka.util.ByteString import TestUtils._ +import TcpSelector._ import Tcp._ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index 8db5cb0d74..b972431a5c 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -8,6 +8,7 @@ import java.net.Socket import scala.concurrent.duration._ import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } +import TcpSelector._ import Tcp._ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { From 4e700b661ad0f917118252bfb0f3b7d764d5268e Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 22 Jan 2013 14:15:37 +0100 Subject: [PATCH 30/66] Add missing case to CapacityLimitSpec --- .../test/scala/akka/io/CapacityLimitSpec.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala b/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala index f7dec60449..a61962e223 100644 --- a/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala +++ b/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala @@ -18,14 +18,19 @@ class CapacityLimitSpec extends AkkaSpec("akka.loglevel = ERROR\nakka.io.tcp.max // we now have three channels registered: a listener, a server connection and a client connection // so register one more channel - val bindCommander = TestProbe() - bindCommander.send(IO(Tcp), Bind(bindHandler.ref, temporaryServerAddress())) - bindCommander.expectMsg(Bound) + val commander = TestProbe() + commander.send(IO(Tcp), Bind(bindHandler.ref, temporaryServerAddress())) + commander.expectMsg(Bound) // we are now at the configured max-channel capacity of 4 + val bindToFail = Bind(bindHandler.ref, temporaryServerAddress()) - bindCommander.send(IO(Tcp), bindToFail) - bindCommander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (bindToFail) + commander.send(IO(Tcp), bindToFail) + commander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (bindToFail) + + val connectToFail = Connect(endpoint) + commander.send(IO(Tcp), connectToFail) + commander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (connectToFail) } } From c6265843b3c236274c649cbfa85a7a38a6373707 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 15:51:21 +0100 Subject: [PATCH 31/66] remove explicit `selector` field in TcpConnection + TcpListener --- akka-io/src/main/scala/akka/io/TcpConnection.scala | 7 ++++--- .../main/scala/akka/io/TcpIncomingConnection.scala | 5 ++--- akka-io/src/main/scala/akka/io/TcpListener.scala | 9 +++++---- .../main/scala/akka/io/TcpOutgoingConnection.scala | 5 ++--- akka-io/src/main/scala/akka/io/TcpSelector.scala | 8 ++++---- .../src/test/scala/akka/io/TcpConnectionSpec.scala | 5 +++-- .../src/test/scala/akka/io/TcpListenerSpec.scala | 13 +++++++++---- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index f6f1d099ee..3dc9e6958c 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -19,8 +19,7 @@ import TcpSelector._ /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ -private[io] abstract class TcpConnection(val selector: ActorRef, - val channel: SocketChannel, +private[io] abstract class TcpConnection(val channel: SocketChannel, val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { import tcp.Settings._ var pendingWrite: PendingWrite = null @@ -30,6 +29,8 @@ private[io] abstract class TcpConnection(val selector: ActorRef, def writePending = pendingWrite ne null + def selector = context.parent + // STATES /** connection established, waiting for registration from user handler */ @@ -277,4 +278,4 @@ private[io] abstract class TcpConnection(val selector: ActorRef, private[TcpConnection] case class CloseInformation( notificationsTo: Set[ActorRef], closedEvent: ConnectionClosed) -} \ No newline at end of file +} diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala index 503c810f44..a4dc447a5d 100644 --- a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -12,12 +12,11 @@ import Tcp.SocketOption * An actor handling the connection state machine for an incoming, already connected * SocketChannel. */ -private[io] class TcpIncomingConnection(_selector: ActorRef, - _channel: SocketChannel, +private[io] class TcpIncomingConnection(_channel: SocketChannel, _tcp: TcpExt, handler: ActorRef, options: Traversable[SocketOption]) - extends TcpConnection(_selector, _channel, _tcp) { + extends TcpConnection(_channel, _tcp) { context.watch(handler) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index 79e37505ad..198905f684 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -12,14 +12,15 @@ import akka.actor.{ ActorLogging, ActorRef, Actor } import TcpSelector._ import Tcp._ -private[io] class TcpListener(selector: ActorRef, - handler: ActorRef, +private[io] class TcpListener(handler: ActorRef, endpoint: InetSocketAddress, backlog: Int, bindCommander: ActorRef, settings: TcpExt#Settings, options: Traversable[SocketOption]) extends Actor with ActorLogging { + def selector: ActorRef = context.parent + context.watch(handler) // sign death pact val channel = { val serverSocketChannel = ServerSocketChannel.open @@ -29,7 +30,7 @@ private[io] class TcpListener(selector: ActorRef, socket.bind(endpoint, backlog) // will blow up the actor constructor if the bind fails serverSocketChannel } - selector ! RegisterServerSocketChannel(channel) + context.parent ! RegisterServerSocketChannel(channel) log.debug("Successfully bound to {}", endpoint) def receive: Receive = { @@ -70,7 +71,7 @@ private[io] class TcpListener(selector: ActorRef, context.parent ! RegisterIncomingConnection(socketChannel, handler, options) acceptAllPending(limit - 1) } - } else selector ! AcceptInterest + } else context.parent ! AcceptInterest override def postStop() { try { diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index c3276723b8..e71b734561 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -15,13 +15,12 @@ import Tcp._ * An actor handling the connection state machine for an outgoing connection * to be established. */ -private[io] class TcpOutgoingConnection(_selector: ActorRef, - _tcp: TcpExt, +private[io] class TcpOutgoingConnection(_tcp: TcpExt, commander: ActorRef, remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress], options: Traversable[SocketOption]) - extends TcpConnection(_selector, TcpOutgoingConnection.newSocketChannel(), _tcp) { + extends TcpConnection(TcpOutgoingConnection.newSocketChannel(), _tcp) { context.watch(commander) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index ec8e70fe9a..6e1b2e1c99 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -80,7 +80,7 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with def handleIncomingConnection(cmd: RegisterIncomingConnection, retriesLeft: Int): Unit = withCapacityProtection(cmd, retriesLeft) { import cmd._ - val connection = spawnChild(() ⇒ new TcpIncomingConnection(self, channel, tcp, handler, options)) + val connection = spawnChild(() ⇒ new TcpIncomingConnection(channel, tcp, handler, options)) execute(registerIncomingConnection(channel, connection)) } @@ -88,14 +88,14 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with withCapacityProtection(cmd, retriesLeft) { import cmd._ val commander = sender - spawnChild(() ⇒ new TcpOutgoingConnection(self, tcp, commander, remoteAddress, localAddress, options)) + spawnChild(() ⇒ new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) } def handleBind(cmd: Bind, retriesLeft: Int): Unit = withCapacityProtection(cmd, retriesLeft) { import cmd._ val commander = sender - spawnChild(() ⇒ new TcpListener(self, handler, endpoint, backlog, commander, tcp.Settings, options)) + spawnChild(() ⇒ new TcpListener(handler, endpoint, backlog, commander, tcp.Settings, options)) } def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { @@ -239,4 +239,4 @@ private[io] object TcpSelector { case object AcceptInterest case object ReadInterest case object WriteInterest -} \ No newline at end of file +} diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 6881a54fb2..91d18221cb 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -480,15 +480,16 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") serverAddress: InetSocketAddress = serverAddress, localAddress: Option[InetSocketAddress] = None, options: immutable.Seq[Tcp.SocketOption] = Nil)( - selector: ActorRef, + _selector: ActorRef, commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { TestActorRef( - new TcpOutgoingConnection(selector, Tcp(system), commander, serverAddress, localAddress, options) { + new TcpOutgoingConnection(Tcp(system), commander, serverAddress, localAddress, options) { override def postRestart(reason: Throwable) { // ensure we never restart context.stop(self) } + override def selector = _selector }) } diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index b972431a5c..3fdebba6b5 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -16,7 +16,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "A TcpListener" must { "register its ServerSocketChannel with its selector" in new TestSetup { - selector.expectMsgType[RegisterServerSocketChannel] + parent.expectMsgType[RegisterServerSocketChannel] } "let the Bind commander know when binding is completed" in new TestSetup { @@ -25,6 +25,8 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "accept acceptable connections and register them with its parent" in new TestSetup { + parent.expectMsgType[RegisterServerSocketChannel] + bindListener() attemptConnectionToEndpoint() @@ -33,8 +35,10 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { // since the batch-accept-limit is 2 we must only receive 2 accepted connections listener ! ChannelAcceptable + parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + parent.expectMsg(AcceptInterest) parent.expectNoMsg(100.millis) // and pick up the last remaining connection on the next ChannelAcceptable @@ -43,6 +47,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { + parent.expectMsgType[RegisterServerSocketChannel] bindListener() val unbindCommander = TestProbe() @@ -53,6 +58,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "drop an incoming connection if it cannot be registered with a selector" in new TestSetup { + parent.expectMsgType[RegisterServerSocketChannel] bindListener() attemptConnectionToEndpoint() @@ -69,8 +75,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { val counter = Iterator.from(0) - class TestSetup { - val selector = TestProbe() + class TestSetup { setup ⇒ val handler = TestProbe() val handlerRef = handler.ref val bindCommander = TestProbe() @@ -89,7 +94,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { private class ListenerParent extends Actor { val listener = context.actorOf( - props = Props(new TcpListener(selector.ref, handler.ref, endpoint, 100, bindCommander.ref, + props = Props(new TcpListener(handler.ref, endpoint, 100, bindCommander.ref, Tcp(system).Settings, Nil)), name = "test-listener-" + counter.next()) parent.watch(listener) From fccf596649de6d2040720f0bb710840ce935380e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 16:03:22 +0100 Subject: [PATCH 32/66] Revert "Change socket options type from `immutable.Traversable` to `Traversable`" This reverts commit ba922fa2d75be264de9063326d99fae3f3b57191. Conflicts: akka-io/src/main/scala/akka/io/Tcp.scala akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala akka-io/src/main/scala/akka/io/TcpListener.scala akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala --- akka-io/src/main/scala/akka/io/Tcp.scala | 4 ++-- akka-io/src/main/scala/akka/io/TcpConnection.scala | 3 ++- akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala | 3 ++- akka-io/src/main/scala/akka/io/TcpListener.scala | 3 ++- akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala | 5 +++-- akka-io/src/main/scala/akka/io/TcpSelector.scala | 5 +++-- akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala | 5 +++-- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index e9b7a07409..c1873e91e5 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -132,11 +132,11 @@ object Tcp extends ExtensionKey[TcpExt] { case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None, - options: Traversable[SocketOption] = Nil) extends Command + options: immutable.Traversable[SocketOption] = Nil) extends Command case class Bind(handler: ActorRef, endpoint: InetSocketAddress, backlog: Int = 100, - options: Traversable[SocketOption] = Nil) extends Command + options: immutable.Traversable[SocketOption] = Nil) extends Command case class Register(handler: ActorRef) extends Command case object Unbind extends Command diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 3dc9e6958c..7031021191 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -9,6 +9,7 @@ import java.io.IOException import java.nio.channels.SocketChannel import java.nio.ByteBuffer import scala.annotation.tailrec +import scala.collection.immutable import scala.util.control.NonFatal import scala.concurrent.duration._ import akka.actor._ @@ -102,7 +103,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, // AUXILIARIES and IMPLEMENTATION /** used in subclasses to start the common machinery above once a channel is connected */ - def completeConnect(commander: ActorRef, options: Traversable[SocketOption]): Unit = { + def completeConnect(commander: ActorRef, options: immutable.Traversable[SocketOption]): Unit = { options.foreach(_.afterConnect(channel.socket)) commander ! Connected( diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala index a4dc447a5d..1036509b4e 100644 --- a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -5,6 +5,7 @@ package akka.io import java.nio.channels.SocketChannel +import scala.collection.immutable import akka.actor.ActorRef import Tcp.SocketOption @@ -15,7 +16,7 @@ import Tcp.SocketOption private[io] class TcpIncomingConnection(_channel: SocketChannel, _tcp: TcpExt, handler: ActorRef, - options: Traversable[SocketOption]) + options: immutable.Traversable[SocketOption]) extends TcpConnection(_channel, _tcp) { context.watch(handler) // sign death pact diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index 198905f684..c510f1c186 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -7,6 +7,7 @@ package akka.io import java.net.InetSocketAddress import java.nio.channels.ServerSocketChannel import scala.annotation.tailrec +import scala.collection.immutable import scala.util.control.NonFatal import akka.actor.{ ActorLogging, ActorRef, Actor } import TcpSelector._ @@ -17,7 +18,7 @@ private[io] class TcpListener(handler: ActorRef, backlog: Int, bindCommander: ActorRef, settings: TcpExt#Settings, - options: Traversable[SocketOption]) extends Actor with ActorLogging { + options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { def selector: ActorRef = context.parent diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala index e71b734561..040d866e5b 100644 --- a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -7,6 +7,7 @@ package akka.io import java.net.InetSocketAddress import java.io.IOException import java.nio.channels.SocketChannel +import scala.collection.immutable import akka.actor.ActorRef import TcpSelector._ import Tcp._ @@ -19,7 +20,7 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, commander: ActorRef, remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress], - options: Traversable[SocketOption]) + options: immutable.Traversable[SocketOption]) extends TcpConnection(TcpOutgoingConnection.newSocketChannel(), _tcp) { context.watch(commander) // sign death pact @@ -37,7 +38,7 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, def receive: Receive = PartialFunction.empty - def connecting(commander: ActorRef, options: Traversable[SocketOption]): Receive = { + def connecting(commander: ActorRef, options: immutable.Traversable[SocketOption]): Receive = { case ChannelConnectable ⇒ try { val connected = channel.finishConnect() diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 6e1b2e1c99..580a7c02d0 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -9,7 +9,8 @@ import java.nio.channels.spi.SelectorProvider import java.nio.channels.{ ServerSocketChannel, SelectionKey, SocketChannel } import java.nio.channels.SelectionKey._ import scala.util.control.NonFatal -import scala.collection.immutable.HashMap +import scala.collection.immutable +import immutable.HashMap import scala.concurrent.duration._ import akka.actor._ import Tcp._ @@ -229,7 +230,7 @@ private[io] object TcpSelector { case class RegisterOutgoingConnection(channel: SocketChannel) case class RegisterServerSocketChannel(channel: ServerSocketChannel) case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, - options: Traversable[SocketOption]) extends Tcp.Command + options: immutable.Traversable[SocketOption]) extends Tcp.Command case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } case object ChannelConnectable diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala b/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala index ea93b7809a..0feeb3809f 100644 --- a/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala +++ b/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala @@ -7,6 +7,7 @@ package akka.io import scala.annotation.tailrec import akka.testkit.{ AkkaSpec, TestProbe } import akka.actor.ActorRef +import scala.collection.immutable import Tcp._ import TestUtils._ @@ -45,10 +46,10 @@ trait IntegrationSpecSupport { _: AkkaSpec ⇒ } /** allow overriding socket options for server side channel */ - def bindOptions: Traversable[SocketOption] = Nil + def bindOptions: immutable.Traversable[SocketOption] = Nil /** allow overriding socket options for client side channel */ - def connectOptions: Traversable[SocketOption] = Nil + def connectOptions: immutable.Traversable[SocketOption] = Nil } } From 64d832377bb4a451acd7ea3f3ad3bf1c911edb9c Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 16:05:03 +0100 Subject: [PATCH 33/66] prevent connection leak when temporaryServerAddress throws --- akka-io/src/test/scala/akka/io/TestUtils.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TestUtils.scala b/akka-io/src/test/scala/akka/io/TestUtils.scala index d084060dc1..8c33c2d938 100644 --- a/akka-io/src/test/scala/akka/io/TestUtils.scala +++ b/akka-io/src/test/scala/akka/io/TestUtils.scala @@ -13,10 +13,11 @@ object TestUtils { def temporaryServerAddress(address: String = "127.0.0.1"): InetSocketAddress = { val serverSocket = ServerSocketChannel.open() - serverSocket.socket.bind(new InetSocketAddress(address, 0)) - val port = serverSocket.socket.getLocalPort - serverSocket.close() - new InetSocketAddress(address, port) + try { + serverSocket.socket.bind(new InetSocketAddress(address, 0)) + val port = serverSocket.socket.getLocalPort + new InetSocketAddress(address, port) + } finally serverSocket.close() } def verifyActorTermination(actor: ActorRef)(implicit system: ActorSystem): Unit = { From c61414f28f4ed66a44761ab6555ea8556d850007 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 22 Jan 2013 16:23:11 +0100 Subject: [PATCH 34/66] Fix TcpListener not distributing incoming connections across all selectors --- akka-io/src/main/scala/akka/io/Tcp.scala | 12 +++------- .../src/main/scala/akka/io/TcpListener.scala | 5 ++-- .../src/main/scala/akka/io/TcpSelector.scala | 5 ++-- .../test/scala/akka/io/TcpListenerSpec.scala | 23 ++++++++----------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index c1873e91e5..92191a768d 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -5,19 +5,13 @@ package akka.io import java.net.InetSocketAddress -import java.nio.channels.{ ServerSocketChannel, SocketChannel } -import akka.actor.ActorRef -import akka.util.ByteString -import akka.actor.ExtensionKey -import akka.actor.ExtendedActorSystem -import akka.actor.ActorSystemImpl -import akka.actor.Props import java.net.Socket import java.net.ServerSocket +import com.typesafe.config.Config import scala.concurrent.duration._ import scala.collection.immutable -import akka.actor.ActorSystem -import com.typesafe.config.Config +import akka.util.ByteString +import akka.actor._ object Tcp extends ExtensionKey[TcpExt] { diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-io/src/main/scala/akka/io/TcpListener.scala index c510f1c186..c110b65a39 100644 --- a/akka-io/src/main/scala/akka/io/TcpListener.scala +++ b/akka-io/src/main/scala/akka/io/TcpListener.scala @@ -13,7 +13,8 @@ import akka.actor.{ ActorLogging, ActorRef, Actor } import TcpSelector._ import Tcp._ -private[io] class TcpListener(handler: ActorRef, +private[io] class TcpListener(selectorRouter: ActorRef, + handler: ActorRef, endpoint: InetSocketAddress, backlog: Int, bindCommander: ActorRef, @@ -69,7 +70,7 @@ private[io] class TcpListener(handler: ActorRef, if (socketChannel != null) { log.debug("New connection accepted") socketChannel.configureBlocking(false) - context.parent ! RegisterIncomingConnection(socketChannel, handler, options) + selectorRouter ! RegisterIncomingConnection(socketChannel, handler, options) acceptAllPending(limit - 1) } } else context.parent ! AcceptInterest diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 580a7c02d0..8845f59d3b 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -10,7 +10,6 @@ import java.nio.channels.{ ServerSocketChannel, SelectionKey, SocketChannel } import java.nio.channels.SelectionKey._ import scala.util.control.NonFatal import scala.collection.immutable -import immutable.HashMap import scala.concurrent.duration._ import akka.actor._ import Tcp._ @@ -19,7 +18,7 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with import TcpSelector._ import tcp.Settings._ - @volatile var childrenKeys = HashMap.empty[String, SelectionKey] + @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] val sequenceNumber = Iterator.from(0) val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) val selector = SelectorProvider.provider.openSelector @@ -96,7 +95,7 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with withCapacityProtection(cmd, retriesLeft) { import cmd._ val commander = sender - spawnChild(() ⇒ new TcpListener(handler, endpoint, backlog, commander, tcp.Settings, options)) + spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp.Settings, options)) } def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala index 3fdebba6b5..91902d0e5a 100644 --- a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala @@ -15,9 +15,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "A TcpListener" must { - "register its ServerSocketChannel with its selector" in new TestSetup { - parent.expectMsgType[RegisterServerSocketChannel] - } + "register its ServerSocketChannel with its selector" in new TestSetup "let the Bind commander know when binding is completed" in new TestSetup { listener ! Bound @@ -25,8 +23,6 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "accept acceptable connections and register them with its parent" in new TestSetup { - parent.expectMsgType[RegisterServerSocketChannel] - bindListener() attemptConnectionToEndpoint() @@ -36,18 +32,17 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { // since the batch-accept-limit is 2 we must only receive 2 accepted connections listener ! ChannelAcceptable - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } parent.expectMsg(AcceptInterest) - parent.expectNoMsg(100.millis) + selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + selectorRouter.expectNoMsg(100.millis) // and pick up the last remaining connection on the next ChannelAcceptable listener ! ChannelAcceptable - parent.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } } "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { - parent.expectMsgType[RegisterServerSocketChannel] bindListener() val unbindCommander = TestProbe() @@ -58,13 +53,12 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { } "drop an incoming connection if it cannot be registered with a selector" in new TestSetup { - parent.expectMsgType[RegisterServerSocketChannel] bindListener() attemptConnectionToEndpoint() listener ! ChannelAcceptable - val channel = parent.expectMsgType[RegisterIncomingConnection].channel + val channel = selectorRouter.expectMsgType[RegisterIncomingConnection].channel channel.isOpen must be(true) listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) @@ -80,9 +74,12 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { val handlerRef = handler.ref val bindCommander = TestProbe() val parent = TestProbe() + val selectorRouter = TestProbe() val endpoint = TestUtils.temporaryServerAddress() private val parentRef = TestActorRef(new ListenerParent) + parent.expectMsgType[RegisterServerSocketChannel] + def bindListener() { listener ! Bound bindCommander.expectMsg(Bound) @@ -94,7 +91,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { private class ListenerParent extends Actor { val listener = context.actorOf( - props = Props(new TcpListener(handler.ref, endpoint, 100, bindCommander.ref, + props = Props(new TcpListener(selectorRouter.ref, handler.ref, endpoint, 100, bindCommander.ref, Tcp(system).Settings, Nil)), name = "test-listener-" + counter.next()) parent.watch(listener) From 0a7c793316d4971a9b8f464681546c89cce422b2 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 22 Jan 2013 17:32:46 +0100 Subject: [PATCH 35/66] fix: make sure buffer is released in any case, also after not acked writes and when killed --- .../main/scala/akka/io/TcpConnection.scala | 22 ++++++++++++------- .../scala/akka/io/TcpConnectionSpec.scala | 11 ++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 7031021191..727175c168 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -152,14 +152,17 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, pendingWrite = pendingWrite.consume(writtenBytes) - val wroteCompleteBuffer = writtenBytes == toWrite if (pendingWrite.hasData) - if (wroteCompleteBuffer) innerWrite() // try again now + if (writtenBytes == toWrite) innerWrite() // wrote complete buffer, try again now else selector ! WriteInterest // try again later - else if (pendingWrite.wantsAck) { // everything written - pendingWrite.commander ! pendingWrite.ack - releaseBuffer(pendingWrite.buffer) + else { // everything written + if (pendingWrite.wantsAck) + pendingWrite.commander ! pendingWrite.ack + + val buffer = pendingWrite.buffer pendingWrite = null + + releaseBuffer(buffer) } } @@ -232,6 +235,12 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, } override def postStop(): Unit = { + if (channel.isOpen) + abort() + + if (writePending) + releaseBuffer(pendingWrite.buffer) + if (closedMessage != null) { val interestedInClose = if (writePending) closedMessage.notificationsTo + pendingWrite.commander @@ -239,9 +248,6 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, interestedInClose.foreach(_ ! closedMessage.closedEvent) } - - if (channel.isOpen) - abort() } override def postRestart(reason: Throwable): Unit = diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 91d18221cb..e95bdc9c8b 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -89,6 +89,17 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") buffer.flip() ByteString(buffer).take(10).decodeString("ASCII") must be("morestuff!") } + "write data after not acknowledged data" in withEstablishedConnection() { setup ⇒ + import setup._ + + object Ack + val writer = TestProbe() + writer.send(connectionActor, Write(ByteString(42.toByte))) + writer.expectNoMsg(500.millis) + + writer.send(connectionActor, Write(ByteString.empty, Ack)) + writer.expectMsg(Ack) + } "stop writing in cases of backpressure and resume afterwards" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ From 7761b001228a6c96452f3cb10cc56645327c001b Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Jan 2013 11:25:19 +0100 Subject: [PATCH 36/66] "unseal" Tcp.Event trait --- akka-io/src/main/scala/akka/io/Tcp.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 92191a768d..328f7ac891 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -160,7 +160,7 @@ object Tcp extends ExtensionKey[TcpExt] { case object ResumeReading extends Command /// EVENTS - sealed trait Event + trait Event case class Received(data: ByteString) extends Event case class Connected(remoteAddress: InetSocketAddress, localAddress: InetSocketAddress) extends Event From 385fd322c97eb3b08b71b26221d0c1c5d676d948 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Jan 2013 11:47:12 +0100 Subject: [PATCH 37/66] Rename `ErrorClose` event to `ErrorClosed` --- akka-io/src/main/scala/akka/io/Tcp.scala | 2 +- akka-io/src/main/scala/akka/io/TcpConnection.scala | 2 +- akka-io/src/test/scala/akka/io/IntegrationSpec.scala | 2 +- akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 328f7ac891..2a9a7d7012 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -173,7 +173,7 @@ object Tcp extends ExtensionKey[TcpExt] { case object Aborted extends ConnectionClosed case object ConfirmedClosed extends ConnectionClosed case object PeerClosed extends ConnectionClosed - case class ErrorClose(cause: String) extends ConnectionClosed + case class ErrorClosed(cause: String) extends ConnectionClosed } class TcpExt(system: ExtendedActorSystem) extends IO.Extension { diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 727175c168..32f500b43d 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -210,7 +210,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, } def handleError(handler: ActorRef, exception: IOException): Unit = { - closedMessage = CloseInformation(Set(handler), ErrorClose(extractMsg(exception))) + closedMessage = CloseInformation(Set(handler), ErrorClosed(extractMsg(exception))) throw exception } diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala index 87df405611..0863707936 100644 --- a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-io/src/test/scala/akka/io/IntegrationSpec.scala @@ -28,7 +28,7 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationS val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() clientHandler.send(clientConnection, Abort) clientHandler.expectMsg(Aborted) - serverHandler.expectMsgType[ErrorClose] + serverHandler.expectMsgType[ErrorClosed] verifyActorTermination(clientConnection) verifyActorTermination(serverConnection) } diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index e95bdc9c8b..abf0024664 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -245,7 +245,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") abortClose(serverSideChannel) selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgType[ErrorClose].cause must be("Connection reset by peer") + connectionHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") // wait a while connectionHandler.expectNoMsg(200.millis) @@ -259,8 +259,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") abortClose(serverSideChannel) writer.send(connectionActor, Write(ByteString("testdata"))) // bother writer and handler should get the message - writer.expectMsgType[ErrorClose] - connectionHandler.expectMsgType[ErrorClose] + writer.expectMsgType[ErrorClosed] + connectionHandler.expectMsgType[ErrorClosed] assertThisConnectionActorTerminated() } @@ -272,7 +272,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") localServer.close() selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClose].cause must be("Connection reset by peer") + userHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") verifyActorTermination(connectionActor) } @@ -288,7 +288,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") key.isConnectable must be(true) selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClose].cause must be("Connection refused") + userHandler.expectMsgType[ErrorClosed].cause must be("Connection refused") verifyActorTermination(connectionActor) } From 8282ac9896181bf5602952ae7032b06c96690fb6 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Jan 2013 13:57:22 +0100 Subject: [PATCH 38/66] Add support for 'unlimited' max-channels --- akka-io/src/main/resources/reference.conf | 2 +- akka-io/src/main/scala/akka/io/Tcp.scala | 9 ++++++--- akka-io/src/main/scala/akka/io/TcpSelector.scala | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 33927a9d45..65626a1ea9 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -28,7 +28,7 @@ akka { # protection by limiting the number of concurrently connected clients. # Also note that this is a "soft" limit; in certain cases the implementation # will accept a few connections more or a few less than the number configured - # here. Set to 0 for "unlimited". + # here. Must be an integer > 0 or "unlimited". max-channels = 256000 # The select loop can be used in two modes: diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 2a9a7d7012..26781718e8 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -183,7 +183,10 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { import config._ val NrOfSelectors = getInt("nr-of-selectors") - val MaxChannels = getInt("max-channels") + val MaxChannels = getString("max-channels") match { + case "unlimited" ⇒ -1 + case _ ⇒ getInt("max-channels") + } val SelectTimeout = getString("select-timeout") match { case "infinite" ⇒ Duration.Inf case x ⇒ Duration(x) @@ -202,12 +205,12 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val TraceLogging = getBoolean("trace-logging") require(NrOfSelectors > 0, "nr-of-selectors must be > 0") - require(MaxChannels >= 0, "max-channels must be >= 0") + require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'") require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative") require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0") require(BatchAcceptLimit > 0, "batch-accept-limit must be > 0") - val MaxChannelsPerSelector = MaxChannels / NrOfSelectors + val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) } val manager = { diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-io/src/main/scala/akka/io/TcpSelector.scala index 8845f59d3b..4e0dd6f9a8 100644 --- a/akka-io/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-io/src/main/scala/akka/io/TcpSelector.scala @@ -100,7 +100,7 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { log.debug("Executing {}", cmd) - if (MaxChannelsPerSelector == 0 || childrenKeys.size < MaxChannelsPerSelector) { + if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { body } else { log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) From f138dbd6b4f8e71f71e2f051649bdc83732c89f0 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Jan 2013 15:19:03 +0100 Subject: [PATCH 39/66] Add missing copyright header, smaller cleanups --- akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala | 8 ++++++-- akka-io/src/main/scala/akka/io/TcpConnection.scala | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala index ed9ed4d175..6d9699122f 100644 --- a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala +++ b/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -1,3 +1,7 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + package akka.io import java.util.concurrent.atomic.AtomicBoolean @@ -10,13 +14,13 @@ trait WithBufferPool { def acquireBuffer(): ByteBuffer = tcp.bufferPool.acquire() - def releaseBuffer(buffer: ByteBuffer) = + def releaseBuffer(buffer: ByteBuffer): Unit = tcp.bufferPool.release(buffer) } trait BufferPool { def acquire(): ByteBuffer - def release(buf: ByteBuffer): Unit + def release(buf: ByteBuffer) } /** diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 32f500b43d..e19a91e93b 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -71,8 +71,8 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, case write: Write ⇒ pendingWrite = createWrite(write) - doWrite(handler) + case ChannelWritable ⇒ doWrite(handler) case cmd: CloseCommand ⇒ handleClose(handler, Some(sender), closeResponse(cmd)) From 3e78247cc8e0922826eb9e79976a4c3bbb7bad82 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Wed, 23 Jan 2013 15:28:14 +0100 Subject: [PATCH 40/66] when the connection is established optimistically try reading before registering ReadInterest --- .../src/main/scala/akka/io/TcpConnection.scala | 2 +- .../test/scala/akka/io/TcpConnectionSpec.scala | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index e19a91e93b..25db734d79 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -38,7 +38,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, def waitingForRegistration(commander: ActorRef): Receive = { case Register(handler) ⇒ if (TraceLogging) log.debug("{} registered as connection handler", handler) - selector ! ReadInterest + doRead(handler, None) // immediately try reading context.setReceiveTimeout(Duration.Undefined) context.watch(handler) // sign death pact diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index abf0024664..8ca5d7cc70 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -59,6 +59,24 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") expectReceivedString("testdata2testdata3") } + "receive data directly when the connection is established" in withUnacceptedConnection() { unregisteredSetup ⇒ + import unregisteredSetup._ + + localServer.configureBlocking(true) + val serverSideChannel = localServer.accept() + serverSideChannel must not be (null) + serverSideChannel.write(ByteBuffer.wrap("immediatedata".getBytes("ASCII"))) + serverSideChannel.configureBlocking(false) + + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) + + // we unrealistically register the selector here so that we can observe + // the ordering between Received and ReadInterest + userHandler.send(connectionActor, Register(selector.ref)) + selector.expectMsgType[Received].data.decodeString("ASCII") must be("immediatedata") + selector.expectMsg(ReadInterest) + } "write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒ import setup._ From 5883c40ce87b1d977151813f4f2ea4e2cb94f1b0 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 23 Jan 2013 15:38:47 +0100 Subject: [PATCH 41/66] Add TCP message protocol diagrams --- .../rst/images/tcp-message-protocol.graffle | 7831 +++++++++++++++++ ...essage-protocol_bind-connect-maxed-out.svg | 3 + .../images/tcp-message-protocol_binding.svg | 3 + .../images/tcp-message-protocol_closing.svg | 3 + ...message-protocol_establishing-incoming.svg | 3 + ...message-protocol_establishing-outgoing.svg | 3 + .../tcp-message-protocol_noticing-close.svg | 3 + .../images/tcp-message-protocol_receiving.svg | 3 + .../images/tcp-message-protocol_unbinding.svg | 3 + .../images/tcp-message-protocol_writing.svg | 3 + 10 files changed, 7858 insertions(+) create mode 100644 akka-docs/rst/images/tcp-message-protocol.graffle create mode 100644 akka-docs/rst/images/tcp-message-protocol_bind-connect-maxed-out.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_binding.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_closing.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_establishing-incoming.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_establishing-outgoing.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_noticing-close.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_receiving.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_unbinding.svg create mode 100644 akka-docs/rst/images/tcp-message-protocol_writing.svg diff --git a/akka-docs/rst/images/tcp-message-protocol.graffle b/akka-docs/rst/images/tcp-message-protocol.graffle new file mode 100644 index 0000000000..c69e25376b --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol.graffle @@ -0,0 +1,7831 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 138.33.0.157554 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {559, 3132}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2013-01-14 12:27:36 +0000 + Creator + Mathias + DisplayScale + 1.000 cm = 1.000 cm + GraphDocumentVersion + 8 + GraphicsList + + + Bounds + {{45, 1962}, {270, 27}} + Class + ShapedGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 237 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs16 \cf0 * if retriesLeft > 0\ +** if retriesLeft == 0} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 236 + Points + + {360, 1926} + {72, 1926} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{216, 1881}, {108, 36}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 235 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Retry(command, retriesLeft)*\ +(via Router)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ControlPoints + + {-54, 0} + {-54, 0} + + ID + 234 + Points + + {360, 1890} + {360, 1908} + + Style + + stroke + + Bezier + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{234, 1854}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 233 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 (via Router)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1773}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 229 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Bind/Connect when max Capacity is reached} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1791}, {468, 18}} + Class + ShapedGraphic + ID + 228 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Maximum number of channels registered with TcpSelectors} + + + + Bounds + {{81, 1908}, {117, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 227 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 CommandFailed(command)**} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1944}, {468, 18}} + Class + ShapedGraphic + ID + 226 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Maximum number of channels registered with TcpSelectors} + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ControlPoints + + {54, 0} + {-54, 0} + + ID + 218 + Points + + {198, 1854} + {198, 1872} + + Style + + stroke + + Bezier + + HeadArrow + 0 + LineType + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 217 + Points + + {198, 1872} + {360, 1872} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 216 + Points + + {360, 1836} + {360, 1944} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Class + LineGraphic + ID + 215 + Points + + {198, 1836} + {360, 1836} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + Bounds + {{81, 1836}, {108, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 214 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Bind(\'85) / Connect(\'85)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 213 + Points + + {72, 1854} + {198, 1854} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 212 + Points + + {72, 1836} + {72, 1944} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 1818}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 211 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{324, 1818}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 209 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{153, 1818}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 208 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpManager} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 207 + Points + + {197.5, 1836} + {197.5, 1944} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{135, 1251}, {99, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 206 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 CommandFailed(write)*} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 205 + Points + + {414, 1269} + {126, 1269} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{45, 1359}, {270, 36}} + Class + ShapedGraphic + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 204 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs16 \cf0 * if a preceding write is still uncompleted\ +** if all preceding writes have been completed\ +*** if the write has been completed successfully and write.ack != Tcp.NoAck } + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{234, 522}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 203 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 (via Router)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ControlPoints + + {54, 0} + {-54, 0} + + Head + + ID + 201 + + ID + 202 + Points + + {234, 522} + {234, 540} + + Style + + stroke + + Bezier + + HeadArrow + 0 + LineType + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 201 + Points + + {234, 540} + {306, 540} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 101 + Points + + {261, 873} + {72, 873} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 98 + Points + + {477, 819} + {162, 819} + + Style + + stroke + + HeadArrow + 0 + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{72, 810}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 198 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 (via Router)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 199 + Points + + {171, 783} + {171, 927} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ControlPoints + + {-54, 0} + {-54, 0} + + ID + 197 + Points + + {162, 819} + {171, 837} + + Style + + stroke + + Bezier + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + + + Bounds + {{234, 90}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 195 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 (via Router)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 194 + Points + + {468, 126} + {495, 126} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + Width + 0.25 + + + + + Bounds + {{288, 891}, {54, 36}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 193 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs16 \cf0 death pact\ +with handler} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 192 + Points + + {279, 891} + {279, 927} + + Style + + stroke + + Color + + a + 0.5 + b + 0 + g + 0 + r + 0 + + HeadArrow + StickArrow + TailArrow + 0 + + + + + Bounds + {{495, 630}, {45, 36}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 191 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs16 \cf0 death pact\ +with handler} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 190 + Points + + {486, 630} + {486, 666} + + Style + + stroke + + Color + + a + 0.5 + b + 0 + g + 0 + r + 0 + + HeadArrow + StickArrow + TailArrow + 0 + + + + + Bounds + {{495, 126}, {27, 54}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 189 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\fs16 \cf0 death\ +pact\ +with\ +handler} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 188 + Points + + {486, 126} + {486, 198} + + Style + + stroke + + Color + + a + 0.5 + b + 0 + g + 0 + r + 0 + + HeadArrow + StickArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 187 + Points + + {261, 873} + {288, 873} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + Width + 0.25 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 186 + Points + + {261, 891} + {288, 891} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + Width + 0.25 + + + + + Bounds + {{279, 873}, {45, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 185 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Timeout} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 184 + Points + + {279, 873} + {279, 891} + + Style + + stroke + + Color + + a + 0.5 + b + 0 + g + 0 + r + 0 + + HeadArrow + StickArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 183 + Points + + {468, 612} + {495, 612} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + Width + 0.25 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 182 + Points + + {468, 630} + {495, 630} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + Width + 0.25 + + + + + Bounds + {{486, 612}, {45, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 181 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Timeout} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 180 + Points + + {486, 612} + {486, 630} + + Style + + stroke + + Color + + a + 0.5 + b + 0 + g + 0 + r + 0 + + HeadArrow + StickArrow + TailArrow + 0 + + + + + Bounds + {{36, 1602}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 179 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Noticing that a Connection was closed} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1719}, {468, 18}} + Class + ShapedGraphic + ID + 176 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 No Connection Established} + + + + Bounds + {{135, 1665}, {261, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 175 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Closed / ConfirmedClosed / Aborted / PeerClosed / ErrorClosed(cause)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 174 + Points + + {414, 1683} + {126, 1683} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 173 + Points + + {414, 1665} + {414, 1701} + + Style + + stroke + + HeadArrow + NegativeControls + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 172 + Points + + {126, 1665} + {126, 1719} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{360, 1647}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 171 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{90, 1647}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 170 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Handler} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1620}, {468, 18}} + Class + ShapedGraphic + ID + 169 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{36, 1413}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 168 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Closing a Connection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{252, 1476}, {117, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 167 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Close / ConfirmedClose / Abort} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 166 + Points + + {162, 1494} + {378, 1494} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{36, 1548}, {468, 18}} + Class + ShapedGraphic + ID + 165 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 No Connection Established} + + + + Bounds + {{171, 1494}, {135, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 162 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Closed / ConfirmedClosed / Aborted} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 161 + Points + + {378, 1512} + {162, 1512} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 158 + Points + + {378, 1476} + {378, 1530} + + Style + + stroke + + HeadArrow + NegativeControls + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 156 + Points + + {162, 1476} + {162, 1548} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{324, 1458}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 155 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{126, 1458}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 154 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1431}, {468, 18}} + Class + ShapedGraphic + ID + 152 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{36, 1170}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 144 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Writing to a Connection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 720}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 143 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Establishing an incoming Connection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 981}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 142 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Receiving Data from a Connection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{342, 1233}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 141 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Write(data, ack)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 140 + Points + + {126, 1251} + {414, 1251} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{36, 1341}, {468, 18}} + Class + ShapedGraphic + ID + 139 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{135, 1305}, {36, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 138 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ck***} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 137 + Points + + {414, 1323} + {126, 1323} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{279, 1269}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 136 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 WriteInterest**} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 135 + Points + + {414, 1287} + {270, 1287} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Bounds + {{333, 1287}, {72, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 134 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ChannelWriteable**} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 133 + Points + + {270, 1305} + {414, 1305} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 132 + Points + + {414, 1251} + {414, 1359} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 131 + Points + + {270, 1233} + {270, 1341} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 130 + Points + + {126, 1233} + {126, 1341} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{360, 1215}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 129 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{90, 1215}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 128 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{234, 1215}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 127 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 1188}, {468, 18}} + Class + ShapedGraphic + ID + 126 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{36, 1116}, {468, 18}} + Class + ShapedGraphic + ID + 125 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{135, 1062}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 124 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Received(data)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 123 + Points + + {414, 1080} + {126, 1080} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{396, 819}, {63, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 122 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 AcceptInterest} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 121 + Points + + {477, 837} + {387, 837} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Bounds + {{279, 1080}, {54, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 120 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ReadInterest} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 119 + Points + + {414, 1098} + {270, 1098} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Bounds + {{333, 1044}, {72, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 118 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ChannelReadable} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 117 + Points + + {270, 1062} + {414, 1062} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 116 + Points + + {414, 1044} + {414, 1116} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 115 + Points + + {270, 1044} + {270, 1116} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 114 + Points + + {126, 1044} + {126, 1116} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{360, 1026}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 113 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{90, 1026}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 112 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Handler} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{234, 1026}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 84 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 999}, {468, 18}} + Class + ShapedGraphic + ID + 83 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Connection Established} + + + + Bounds + {{180, 891}, {54, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 82 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ReadInterest} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 111 + Points + + {261, 909} + {171, 909} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 110 + Points + + {72, 891} + {261, 891} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{180, 873}, {72, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 109 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Register(handler)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 108 + Points + + {261, 855} + {261, 927} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{207, 837}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 107 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpIncomingConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{81, 855}, {135, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 106 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Connected(remoteAddr, localAddr)} + VerticalPad + 0 + + Wrap + NO + + + Class + LineGraphic + ID + 105 + Points + + {171, 855} + {261, 855} + + Style + + stroke + + HeadArrow + 0 + HopLines + + HopType + 1 + Pattern + 1 + TailArrow + 0 + + + + + Bounds + {{171, 801}, {216, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 104 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 RegisterIncomingConnection(channel, handler, options)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{387, 783}, {81, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 103 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ChannelAcceptable} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 927}, {468, 18}} + Class + ShapedGraphic + ID + 102 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Incoming Connection Established} + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 97 + Points + + {477, 783} + {477, 927} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 96 + Points + + {386.5, 783} + {386.5, 927} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 95 + Points + + {387, 801} + {477, 801} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 94 + Points + + {72, 783} + {72, 927} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 765}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 93 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Bind-Handler} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{432, 765}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 92 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpListener} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{324, 765}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 91 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector (Listener)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{117, 765}, {126, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 90 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector (Connection)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 738}, {468, 18}} + Class + ShapedGraphic + ID + 88 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Server Bound} + + + + Bounds + {{36, 252}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 87 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Unbinding a Server} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 441}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 86 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Establishing an outgoing Connection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 9}, {468, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica-Bold + Size + 10 + + ID + 85 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 0 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural + +\f0\b\fs20 \cf0 Binding a Server} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{315, 630}, {54, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 81 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ReadInterest} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 80 + Points + + {468, 648} + {306, 648} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 79 + Points + + {72, 630} + {468, 630} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{387, 612}, {72, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 78 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Register(handler)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 459}, {468, 18}} + Class + ShapedGraphic + ID + 77 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 No Connection Established} + + + + Bounds + {{81, 594}, {135, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 76 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Connected(remoteAddr, localAddr)} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 666}, {468, 18}} + Class + ShapedGraphic + ID + 75 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Outgoing Connection Established} + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 74 + Points + + {468, 612} + {72, 612} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{378, 576}, {81, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 73 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 ChannelConnectable} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 72 + Points + + {306, 594} + {468, 594} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Bounds + {{315, 558}, {153, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 71 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 RegisterOutgoingConnection(channel)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 70 + Points + + {468, 576} + {306, 576} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Class + LineGraphic + ID + 69 + Points + + {306, 558} + {468, 558} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 68 + Points + + {468, 558} + {468, 666} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 65 + Points + + {306, 504} + {306, 666} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Class + LineGraphic + ID + 64 + Points + + {234, 504} + {306, 504} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + Bounds + {{45, 504}, {180, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 63 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Connect(remoteAddress, localAddress, options)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 62 + Points + + {72, 522} + {234, 522} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 61 + Points + + {72, 504} + {72, 666} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 486}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 60 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{414, 540}, {108, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 59 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpOutgoingConnection} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{270, 486}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 58 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{198, 486}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 57 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpManager} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 56 + Points + + {234, 504} + {234, 666} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 387}, {468, 18}} + Class + ShapedGraphic + ID + 54 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Server Unbound} + + + + Bounds + {{342, 297}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 53 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpListener} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{36, 270}, {468, 18}} + Class + ShapedGraphic + ID + 52 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Server Bound} + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 51 + Points + + {378, 315} + {378, 369} + + Style + + stroke + + HeadArrow + NegativeControls + TailArrow + 0 + + + + + Bounds + {{36, 27}, {468, 18}} + Class + ShapedGraphic + ID + 50 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Server Unbound} + + + + Bounds + {{81, 162}, {36, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 49 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Bound} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 48 + Points + + {378, 351} + {162, 351} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{333, 315}, {36, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 47 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Unbind} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 46 + Points + + {162, 333} + {378, 333} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 45 + Points + + {162, 315} + {162, 387} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 198}, {468, 18}} + Class + ShapedGraphic + ID + 44 + Shape + Rectangle + Style + + fill + + Color + + b + 0.8 + g + 0.8 + r + 0.8 + + + shadow + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 Server Bound} + + + + Bounds + {{171, 333}, {45, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 43 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Unbound} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 42 + Points + + {468, 180} + {72, 180} + + Style + + stroke + + HeadArrow + FilledArrow + HopLines + + HopType + 1 + TailArrow + 0 + + + + + Bounds + {{423, 144}, {36, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 41 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Bound} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 40 + Points + + {306, 162} + {468, 162} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Bounds + {{315, 126}, {153, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 8 + + ID + 38 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 RegisterServerSocketChannel(channel)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 37 + Points + + {468, 144} + {306, 144} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + Class + LineGraphic + ID + 35 + Points + + {306, 126} + {468, 126} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 34 + Points + + {468, 126} + {468, 198} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ControlPoints + + {54, 0} + {-54, 0} + + Head + + ID + 24 + + ID + 33 + Points + + {234, 90} + {234, 108} + + Style + + stroke + + Bezier + + HeadArrow + 0 + LineType + 1 + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 24 + Points + + {234, 108} + {306, 108} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 23 + Points + + {306, 72} + {306, 216} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Class + LineGraphic + ID + 200 + Points + + {234, 72} + {306, 72} + + Style + + stroke + + HeadArrow + 0 + Pattern + 1 + TailArrow + 0 + + + + + Bounds + {{72, 72}, {153, 27}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 8 + + ID + 21 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs16 \cf0 Bind(handler, endpoint, backlog, options)} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 19 + Points + + {72, 90} + {234, 90} + + Style + + stroke + + HeadArrow + FilledArrow + TailArrow + 0 + + + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 16 + Points + + {72, 72} + {72, 216} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + Bounds + {{36, 54}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 12 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{126, 297}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 11 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 User Actor} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{432, 108}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 10 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpListener} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{270, 54}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 9 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpSelector} + VerticalPad + 0 + + Wrap + NO + + + Bounds + {{198, 54}, {72, 18}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + FontInfo + + Font + Helvetica + Size + 10 + + ID + 7 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs20 \cf0 TcpManager} + VerticalPad + 0 + + Wrap + NO + + + AllowConnections + NO + AllowLabelDrop + + AllowToConnect + + Class + LineGraphic + ID + 5 + Points + + {234, 72} + {234, 216} + + Style + + stroke + + HeadArrow + 0 + TailArrow + 0 + + + + + GridInfo + + ShowsGrid + YES + SnapsToGrid + YES + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2013-01-23 14:28:34 +0000 + Modifier + Mathias + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + int + 0 + + NSLeftMargin + + float + 18 + + NSPaperSize + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgVMCgUoDhg== + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 4 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + + name + Canvas 1 + + + Frame + {{65, 0}, {1476, 1178}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{-10, 1661.4718}, {580.51947, 442.8573}} + Zoom + 2.309999942779541 + ZoomValues + + + Canvas 1 + 2.309999942779541 + 2.2999999523162842 + + + + saveQuickLookFiles + YES + + diff --git a/akka-docs/rst/images/tcp-message-protocol_bind-connect-maxed-out.svg b/akka-docs/rst/images/tcp-message-protocol_bind-connect-maxed-out.svg new file mode 100644 index 0000000000..8c1a568662 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_bind-connect-maxed-out.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1TcpManagerTcpSelectorUser ActorBind(…) / Connect(…)Maximum number of channels registered with TcpSelectorsCommandFailed(command)**Maximum number of channels registered with TcpSelectorsBind/Connect when max Capacity is reached(via Router)Retry(command, retriesLeft)*(via Router)* if retriesLeft > 0** if retriesLeft == 0 diff --git a/akka-docs/rst/images/tcp-message-protocol_binding.svg b/akka-docs/rst/images/tcp-message-protocol_binding.svg new file mode 100644 index 0000000000..7fdd097c17 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_binding.svg @@ -0,0 +1,3 @@ + + +2013-01-22 22:05ZCanvas 1Layer 1TcpManagerTcpSelectorTcpListenerUser ActorBind(handler, endpoint, backlog, options)RegisterServerSocketChannel(channel)BoundServer BoundBoundServer UnboundBinding a Serverdeathpactwithhandler(via Router) diff --git a/akka-docs/rst/images/tcp-message-protocol_closing.svg b/akka-docs/rst/images/tcp-message-protocol_closing.svg new file mode 100644 index 0000000000..28b2f51822 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_closing.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1Connection EstablishedUser ActorTcpConnectionClosed / ConfirmedClosed / AbortedNo Connection EstablishedClose / ConfirmedClose / AbortClosing a Connection diff --git a/akka-docs/rst/images/tcp-message-protocol_establishing-incoming.svg b/akka-docs/rst/images/tcp-message-protocol_establishing-incoming.svg new file mode 100644 index 0000000000..d4f7944383 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_establishing-incoming.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1Server BoundTcpSelector (Connection)TcpSelector (Listener)TcpListenerBind-HandlerIncoming Connection EstablishedChannelAcceptableRegisterIncomingConnection(channel, handler, options)Connected(remoteAddr, localAddr)TcpIncomingConnectionRegister(handler)ReadInterestAcceptInterestEstablishing an incoming ConnectionTimeoutdeath pactwith handler(via Router) diff --git a/akka-docs/rst/images/tcp-message-protocol_establishing-outgoing.svg b/akka-docs/rst/images/tcp-message-protocol_establishing-outgoing.svg new file mode 100644 index 0000000000..b9487a6604 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_establishing-outgoing.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1TcpManagerTcpSelectorTcpOutgoingConnectionUser ActorConnect(remoteAddress, localAddress, options)RegisterOutgoingConnection(channel)ChannelConnectableOutgoing Connection EstablishedConnected(remoteAddr, localAddr)No Connection EstablishedRegister(handler)ReadInterestEstablishing an outgoing ConnectionTimeoutdeath pactwith handler(via Router) diff --git a/akka-docs/rst/images/tcp-message-protocol_noticing-close.svg b/akka-docs/rst/images/tcp-message-protocol_noticing-close.svg new file mode 100644 index 0000000000..0608356d8b --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_noticing-close.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1Connection EstablishedHandlerTcpConnectionClosed / ConfirmedClosed / Aborted / PeerClosed / ErrorClosed(cause)No Connection EstablishedNoticing that a Connection was closed diff --git a/akka-docs/rst/images/tcp-message-protocol_receiving.svg b/akka-docs/rst/images/tcp-message-protocol_receiving.svg new file mode 100644 index 0000000000..1e50b3d078 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_receiving.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1Connection EstablishedTcpSelectorHandlerTcpConnectionChannelReadableReadInterestReceived(data)Connection EstablishedReceiving Data from a Connection diff --git a/akka-docs/rst/images/tcp-message-protocol_unbinding.svg b/akka-docs/rst/images/tcp-message-protocol_unbinding.svg new file mode 100644 index 0000000000..b71b8bc6a4 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_unbinding.svg @@ -0,0 +1,3 @@ + + +2013-01-22 22:05ZCanvas 1Layer 1User ActorUnboundUnbindServer BoundTcpListenerServer UnboundUnbinding a Server diff --git a/akka-docs/rst/images/tcp-message-protocol_writing.svg b/akka-docs/rst/images/tcp-message-protocol_writing.svg new file mode 100644 index 0000000000..72be575290 --- /dev/null +++ b/akka-docs/rst/images/tcp-message-protocol_writing.svg @@ -0,0 +1,3 @@ + + +2013-01-23 14:28ZCanvas 1Layer 1Connection EstablishedTcpSelectorUser ActorTcpConnectionChannelWriteable**WriteInterest**ck***Connection EstablishedWrite(data, ack)Writing to a Connection* if a preceding write is still uncompleted** if all preceding writes have been completed*** if the write has been completed successfully and write.ack != Tcp.NoAck CommandFailed(write)* From e994267bf6dd23768449db10d5f17d4e6422f91f Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Fri, 25 Jan 2013 10:06:49 +0100 Subject: [PATCH 42/66] fix checking for connection close in TcpConnectionSpec The fix was to only rely on actually selecting not also on checking key.readyOps since that isn't necessarily reliable without selecting. --- .../scala/akka/io/TcpConnectionSpec.scala | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 8ca5d7cc70..3232a68caf 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -6,6 +6,8 @@ package akka.io import scala.annotation.tailrec +import org.scalatest.matchers.{ MatchResult, BeMatcher } + import java.nio.channels.{ Selector, SelectionKey, SocketChannel, ServerSocketChannel } import java.nio.ByteBuffer import java.nio.channels.spi.SelectorProvider @@ -191,7 +193,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") closeCommander.expectMsg(Closed) assertThisConnectionActorTerminated() - checkFor(serverSelectionKey, SelectionKey.OP_READ, 2000) + serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds)) val buffer = ByteBuffer.allocate(1) serverSideChannel.read(buffer) must be(-1) @@ -239,7 +241,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.expectNoMsg(100.millis) // not yet val buffer = ByteBuffer.allocate(1) - checkFor(serverSelectionKey, SelectionKey.OP_READ, 2000) + serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds)) serverSideChannel.read(buffer) must be(-1) serverSideChannel.close() @@ -377,15 +379,13 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") def checkFor(key: SelectionKey, interest: Int, millis: Int = 100): Boolean = if (key.isValid) { - if ((key.readyOps() & interest) != 0) true - else { - key.interestOps(interest) - val ret = nioSelector.select(millis) - key.interestOps(0) + key.interestOps(interest) + nioSelector.selectedKeys().clear() + val ret = nioSelector.select(millis) + key.interestOps(0) - ret > 0 && nioSelector.selectedKeys().contains(key) && key.isValid && - (key.readyOps() & interest) != 0 - } + ret > 0 && nioSelector.selectedKeys().contains(key) && key.isValid && + (key.readyOps() & interest) != 0 } else false def openSelectorFor(channel: SocketChannel, interests: Int): (Selector, SelectionKey) = { @@ -452,6 +452,23 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") verifyActorTermination(connectionActor) clientSideChannel must not be ('open) } + + def selectedAs(interest: Int, duration: Duration): BeMatcher[SelectionKey] = + new BeMatcher[SelectionKey] { + def apply(key: SelectionKey) = + MatchResult( + checkFor(key, interest, duration.toMillis.toInt), + "%s key was not selected for %s after %s" format (key.attachment(), interestsDesc(interest), duration), + "%s key was selected for %s after %s" format (key.attachment(), interestsDesc(interest), duration)) + } + + val interestsNames = Seq( + SelectionKey.OP_ACCEPT -> "accepting", + SelectionKey.OP_CONNECT -> "connecting", + SelectionKey.OP_READ -> "reading", + SelectionKey.OP_WRITE -> "writing") + def interestsDesc(interests: Int): String = + interestsNames.filter(i ⇒ (i._1 & interests) != 0).map(_._2).mkString(", ") } def withUnacceptedConnection( setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ (), From f6fb147afcc1a9b4beb336c2eeb81b4e58b23bc6 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Thu, 24 Jan 2013 15:08:42 +0100 Subject: [PATCH 43/66] aggregate received data as long as there's data in kernel buffers or 'received-message-size-limit' is reached, see #2886 --- akka-io/src/main/resources/reference.conf | 7 +- akka-io/src/main/scala/akka/io/Tcp.scala | 4 ++ .../main/scala/akka/io/TcpConnection.scala | 66 +++++++++++++------ .../scala/akka/io/TcpConnectionSpec.scala | 23 ++++++- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 65626a1ea9..54df6977f9 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -52,7 +52,7 @@ akka { # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. - direct-buffer-size = 131072 + direct-buffer-size = 131072 # 128 KiB # The maximal number of direct buffers kept in the direct buffer pool for # reuse. @@ -62,6 +62,11 @@ akka { # its commander before aborting the connection. register-timeout = 5s + # The maximum number of bytes delivered by a `Received` message. Before + # more data is read from the network the connection actor will try to + # do other work. + received-message-size-limit = unlimited + # Enable fine grained logging of what goes on inside the implementation. # Be aware that this may log more than once per message sent to the actors # of the tcp implementation. diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 26781718e8..0df8e0e0a6 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -199,6 +199,10 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { case "infinite" ⇒ Duration.Undefined case x ⇒ Duration(x) } + val ReceivedMessageSizeLimit = getString("received-message-size-limit") match { + case "unlimited" ⇒ Int.MaxValue + case x ⇒ getInt("received-message-size-limit") + } val SelectorDispatcher = getString("selector-dispatcher") val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 25db734d79..00b8ffef24 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -23,6 +23,7 @@ import TcpSelector._ private[io] abstract class TcpConnection(val channel: SocketChannel, val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { import tcp.Settings._ + import TcpConnection._ var pendingWrite: PendingWrite = null // Needed to send the ConnectionClosed message in the postStop handler. @@ -115,32 +116,47 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, } def doRead(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = { + @tailrec def innerRead(buffer: ByteBuffer, receivedData: ByteString, remainingLimit: Int): ReadResult = + if (remainingLimit > 0) { + // never read more than the configured limit + buffer.clear() + buffer.limit(math.min(DirectBufferSize, remainingLimit)) + val readBytes = channel.read(buffer) + buffer.flip() + + val totalData = receivedData ++ ByteString(buffer) + + readBytes match { + case DirectBufferSize ⇒ innerRead(buffer, totalData, remainingLimit - DirectBufferSize) + case x if totalData.length > 0 ⇒ GotCompleteData(totalData) + case 0 ⇒ NoData + case -1 ⇒ EndOfStream + case _ ⇒ + throw new IllegalStateException("Unexpected value returned from read: " + readBytes) + } + } else MoreDataWaiting(receivedData) + val buffer = acquireBuffer() - - try { - val readBytes = channel.read(buffer) - buffer.flip() - - if (readBytes > 0) { - if (TraceLogging) log.debug("Read {} bytes", readBytes) - handler ! Received(ByteString(buffer)) - releaseBuffer(buffer) - - if (readBytes == buffer.capacity()) - // directly try reading more because we exhausted our buffer - self ! ChannelReadable - else selector ! ReadInterest - } else if (readBytes == 0) { - if (TraceLogging) log.debug("Read nothing. Registering read interest with selector") + try innerRead(buffer, ByteString.empty, ReceivedMessageSizeLimit) match { + case NoData ⇒ + if (TraceLogging) log.debug("Read nothing.") selector ! ReadInterest - } else if (readBytes == -1) { + case GotCompleteData(data) ⇒ + if (TraceLogging) log.debug("Read {} bytes.", data.length) + + handler ! Received(data) + selector ! ReadInterest + case MoreDataWaiting(data) ⇒ + if (TraceLogging) log.debug("Read {} bytes. More data waiting.", data.length) + + handler ! Received(data) + self ! ChannelReadable + case EndOfStream ⇒ if (TraceLogging) log.debug("Read returned end-of-stream") doCloseConnection(handler, closeCommander, closeReason) - } else throw new IllegalStateException("Unexpected value returned from read: " + readBytes) - } catch { case e: IOException ⇒ handleError(handler, e) - } + } finally releaseBuffer(buffer) } final def doWrite(handler: ActorRef): Unit = { @@ -277,12 +293,20 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, PendingWrite(sender, write.ack, write.data.drop(copied), buffer) } +} + +private[io] object TcpConnection { + sealed trait ReadResult + object NoData extends ReadResult + object EndOfStream extends ReadResult + case class GotCompleteData(data: ByteString) extends ReadResult + case class MoreDataWaiting(data: ByteString) extends ReadResult /** * Used to transport information to the postStop method to notify * interested party about a connection close. */ - private[TcpConnection] case class CloseInformation( + case class CloseInformation( notificationsTo: Set[ActorRef], closedEvent: ConnectionClosed) } diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala index 3232a68caf..b9b02313af 100644 --- a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -61,6 +61,25 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") expectReceivedString("testdata2testdata3") } + "bundle incoming Received messages as long as more data is available" in withEstablishedConnection( + clientSocketOptions = List(SO.ReceiveBufferSize(1000000)) // to make sure enough data gets through + ) { setup ⇒ + import setup._ + + val DataSize = 1000000 + val bigData = new Array[Byte](DataSize) + val buffer = ByteBuffer.wrap(bigData) + + val wrote = serverSideChannel.write(buffer) + wrote must be > 140000 + + expectNoMsg(1000.millis) // data should have been transferred fully by now + + selector.send(connectionActor, ChannelReadable) + + // 140000 is more than the direct buffer size + connectionHandler.expectMsgType[Received].data.length must be > 140000 + } "receive data directly when the connection is established" in withUnacceptedConnection() { unregisteredSetup ⇒ import unregisteredSetup._ @@ -491,7 +510,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") clientSideChannel) } } - def withEstablishedConnection(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: RegisteredSetup ⇒ Any): Unit = withUnacceptedConnection(setServerSocketOptions) { unregisteredSetup ⇒ + def withEstablishedConnection( + setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ (), + clientSocketOptions: immutable.Seq[SocketOption] = Nil)(body: RegisteredSetup ⇒ Any): Unit = withUnacceptedConnection(setServerSocketOptions, createConnectionActor(options = clientSocketOptions)) { unregisteredSetup ⇒ import unregisteredSetup._ localServer.configureBlocking(true) From 77675383f4bcdb5d985ad66936a9edb632284506 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Sat, 26 Jan 2013 10:44:44 +0100 Subject: [PATCH 44/66] fix: skip selector when read is split up just because of ReceivedMessageSizeLimit --- akka-io/src/main/scala/akka/io/TcpConnection.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-io/src/main/scala/akka/io/TcpConnection.scala index 00b8ffef24..a586881c67 100644 --- a/akka-io/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-io/src/main/scala/akka/io/TcpConnection.scala @@ -120,14 +120,15 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, if (remainingLimit > 0) { // never read more than the configured limit buffer.clear() - buffer.limit(math.min(DirectBufferSize, remainingLimit)) + val maxBufferSpace = math.min(DirectBufferSize, remainingLimit) + buffer.limit(maxBufferSpace) val readBytes = channel.read(buffer) buffer.flip() val totalData = receivedData ++ ByteString(buffer) readBytes match { - case DirectBufferSize ⇒ innerRead(buffer, totalData, remainingLimit - DirectBufferSize) + case `maxBufferSpace` ⇒ innerRead(buffer, totalData, remainingLimit - maxBufferSpace) case x if totalData.length > 0 ⇒ GotCompleteData(totalData) case 0 ⇒ NoData case -1 ⇒ EndOfStream From c8f8b55f010d58c9ff80fcb1b818fca295fa808e Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Sat, 26 Jan 2013 10:55:39 +0100 Subject: [PATCH 45/66] use config.getBytes where appropriate --- akka-io/src/main/resources/reference.conf | 2 +- akka-io/src/main/scala/akka/io/Tcp.scala | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf index 54df6977f9..1c4e47263d 100644 --- a/akka-io/src/main/resources/reference.conf +++ b/akka-io/src/main/resources/reference.conf @@ -52,7 +52,7 @@ akka { # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. - direct-buffer-size = 131072 # 128 KiB + direct-buffer-size = 128 KiB # The maximal number of direct buffers kept in the direct buffer pool for # reuse. diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-io/src/main/scala/akka/io/Tcp.scala index 0df8e0e0a6..df3e3fe320 100644 --- a/akka-io/src/main/scala/akka/io/Tcp.scala +++ b/akka-io/src/main/scala/akka/io/Tcp.scala @@ -193,7 +193,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { } val SelectorAssociationRetries = getInt("selector-association-retries") val BatchAcceptLimit = getInt("batch-accept-limit") - val DirectBufferSize = getInt("direct-buffer-size") + val DirectBufferSize = getIntBytes("direct-buffer-size") val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") val RegisterTimeout = getString("register-timeout") match { case "infinite" ⇒ Duration.Undefined @@ -201,7 +201,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { } val ReceivedMessageSizeLimit = getString("received-message-size-limit") match { case "unlimited" ⇒ Int.MaxValue - case x ⇒ getInt("received-message-size-limit") + case x ⇒ getIntBytes("received-message-size-limit") } val SelectorDispatcher = getString("selector-dispatcher") val WorkerDispatcher = getString("worker-dispatcher") @@ -215,6 +215,12 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { require(BatchAcceptLimit > 0, "batch-accept-limit must be > 0") val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) + + private[this] def getIntBytes(path: String): Int = { + val size = getBytes(path) + require(size < Int.MaxValue, s"$path must be < 2 GiB") + size.toInt + } } val manager = { From a9648438433ef2bc42d6c91ef5eda6f722e3a68c Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 30 Jan 2013 09:30:59 +0100 Subject: [PATCH 46/66] =?UTF-8?q?move=20akka-io=20project=20into=20akka-ac?= =?UTF-8?q?tor=E2=80=99s=20akka.io=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scala/akka/io/CapacityLimitSpec.scala | 0 .../test/scala/akka/io/IntegrationSpec.scala | 0 .../akka/io/IntegrationSpecSupport.scala | 0 .../scala/akka/io/TcpConnectionSpec.scala | 0 .../test/scala/akka/io/TcpListenerSpec.scala | 0 .../src/test/scala/akka/io/TestUtils.scala | 0 akka-actor/src/main/resources/reference.conf | 84 +++++++++++++++++ .../scala/akka/io/DirectByteBufferPool.scala | 0 .../src/main/scala/akka/io/IO.scala | 0 .../src/main/scala/akka/io/Tcp.scala | 0 .../main/scala/akka/io/TcpConnection.scala | 0 .../scala/akka/io/TcpIncomingConnection.scala | 0 .../src/main/scala/akka/io/TcpListener.scala | 0 .../src/main/scala/akka/io/TcpManager.scala | 0 .../scala/akka/io/TcpOutgoingConnection.scala | 0 .../src/main/scala/akka/io/TcpSelector.scala | 0 akka-io/src/main/resources/reference.conf | 90 ------------------- project/AkkaBuild.scala | 13 +-- 18 files changed, 86 insertions(+), 101 deletions(-) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/CapacityLimitSpec.scala (100%) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/IntegrationSpec.scala (100%) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/IntegrationSpecSupport.scala (100%) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/TcpConnectionSpec.scala (100%) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/TcpListenerSpec.scala (100%) rename {akka-io => akka-actor-tests}/src/test/scala/akka/io/TestUtils.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/DirectByteBufferPool.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/IO.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/Tcp.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpConnection.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpIncomingConnection.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpListener.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpManager.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpOutgoingConnection.scala (100%) rename {akka-io => akka-actor}/src/main/scala/akka/io/TcpSelector.scala (100%) delete mode 100644 akka-io/src/main/resources/reference.conf diff --git a/akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala b/akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/CapacityLimitSpec.scala rename to akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/IntegrationSpec.scala rename to akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala diff --git a/akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/IntegrationSpecSupport.scala rename to akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala diff --git a/akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/TcpConnectionSpec.scala rename to akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala diff --git a/akka-io/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/TcpListenerSpec.scala rename to akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala diff --git a/akka-io/src/test/scala/akka/io/TestUtils.scala b/akka-actor-tests/src/test/scala/akka/io/TestUtils.scala similarity index 100% rename from akka-io/src/test/scala/akka/io/TestUtils.scala rename to akka-actor-tests/src/test/scala/akka/io/TestUtils.scala diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index aeee89a65f..9d86ff75b2 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -368,6 +368,90 @@ akka { } io { + + # By default the select loops run on dedicated threads, hence using a + # PinnedDispatcher + pinned-dispatcher { + type = "PinnedDispatcher" + executor = "thread-pool-executor" + thread-pool-executor.allow-core-pool-timeout = off + } + + tcp { + + # The number of selectors to stripe the served channels over; each of + # these will use one select loop on the selector-dispatcher. + nr-of-selectors = 1 + + # Maximum number of open channels supported by this TCP module; there is + # no intrinsic general limit, this setting is meant to enable DoS + # protection by limiting the number of concurrently connected clients. + # Also note that this is a "soft" limit; in certain cases the implementation + # will accept a few connections more or a few less than the number configured + # here. Must be an integer > 0 or "unlimited". + max-channels = 256000 + + # The select loop can be used in two modes: + # - setting "infinite" will select without a timeout, hogging a thread + # - setting a positive timeout will do a bounded select call, + # enabling sharing of a single thread between multiple selectors + # (in this case you will have to use a different configuration for the + # selector-dispatcher, e.g. using "type=Dispatcher" with size 1) + # - setting it to zero means polling, i.e. calling selectNow() + select-timeout = infinite + + # When trying to assign a new connection to a selector and the chosen + # selector is at full capacity, retry selector choosing and assignment + # this many times before giving up + selector-association-retries = 10 + + # The maximum number of connection that are accepted in one go, + # higher numbers decrease latency, lower numbers increase fairness on + # the worker-dispatcher + batch-accept-limit = 10 + + # The number of bytes per direct buffer in the pool used to read or write + # network data from the kernel. + direct-buffer-size = 128 KiB + + # The maximal number of direct buffers kept in the direct buffer pool for + # reuse. + max-direct-buffer-pool-size = 1000 + + # The duration a connection actor waits for a `Register` message from + # its commander before aborting the connection. + register-timeout = 5s + + # The maximum number of bytes delivered by a `Received` message. Before + # more data is read from the network the connection actor will try to + # do other work. + received-message-size-limit = unlimited + + # Enable fine grained logging of what goes on inside the implementation. + # Be aware that this may log more than once per message sent to the actors + # of the tcp implementation. + trace-logging = off + + # Fully qualified config path which holds the dispatcher configuration + # to be used for running the select() calls in the selectors + selector-dispatcher = "akka.io.pinned-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the read/write worker actors + worker-dispatcher = "akka.actor.default-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the selector management actors + management-dispatcher = "akka.actor.default-dispatcher" + } + + # IMPORTANT NOTICE: + # + # The following settings belong to the deprecated akka.actor.IO + # implementation and will be removed once that is removed. They are not + # taken into account by the akka.io.* implementation, which is configured + # above! + # In bytes, the size of the shared read buffer. In the span 0b..2GiB. # read-buffer-size = 8KiB diff --git a/akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/DirectByteBufferPool.scala rename to akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala diff --git a/akka-io/src/main/scala/akka/io/IO.scala b/akka-actor/src/main/scala/akka/io/IO.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/IO.scala rename to akka-actor/src/main/scala/akka/io/IO.scala diff --git a/akka-io/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/Tcp.scala rename to akka-actor/src/main/scala/akka/io/Tcp.scala diff --git a/akka-io/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpConnection.scala rename to akka-actor/src/main/scala/akka/io/TcpConnection.scala diff --git a/akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpIncomingConnection.scala rename to akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala diff --git a/akka-io/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpListener.scala rename to akka-actor/src/main/scala/akka/io/TcpListener.scala diff --git a/akka-io/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpManager.scala rename to akka-actor/src/main/scala/akka/io/TcpManager.scala diff --git a/akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpOutgoingConnection.scala rename to akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala diff --git a/akka-io/src/main/scala/akka/io/TcpSelector.scala b/akka-actor/src/main/scala/akka/io/TcpSelector.scala similarity index 100% rename from akka-io/src/main/scala/akka/io/TcpSelector.scala rename to akka-actor/src/main/scala/akka/io/TcpSelector.scala diff --git a/akka-io/src/main/resources/reference.conf b/akka-io/src/main/resources/reference.conf deleted file mode 100644 index 1c4e47263d..0000000000 --- a/akka-io/src/main/resources/reference.conf +++ /dev/null @@ -1,90 +0,0 @@ -################################# -# Akka IO Reference Config File # -################################# - -# This is the reference config file that contains all the default settings. -# Make your edits/overrides in your application.conf. - -akka { - - io { - - # By default the select loops run on dedicated threads, hence using a - # PinnedDispatcher - pinned-dispatcher { - type = "PinnedDispatcher" - executor = "thread-pool-executor" - thread-pool-executor.allow-core-pool-timeout = off - } - - tcp { - - # The number of selectors to stripe the served channels over; each of - # these will use one select loop on the selector-dispatcher. - nr-of-selectors = 1 - - # Maximum number of open channels supported by this TCP module; there is - # no intrinsic general limit, this setting is meant to enable DoS - # protection by limiting the number of concurrently connected clients. - # Also note that this is a "soft" limit; in certain cases the implementation - # will accept a few connections more or a few less than the number configured - # here. Must be an integer > 0 or "unlimited". - max-channels = 256000 - - # The select loop can be used in two modes: - # - setting "infinite" will select without a timeout, hogging a thread - # - setting a positive timeout will do a bounded select call, - # enabling sharing of a single thread between multiple selectors - # (in this case you will have to use a different configuration for the - # selector-dispatcher, e.g. using "type=Dispatcher" with size 1) - # - setting it to zero means polling, i.e. calling selectNow() - select-timeout = infinite - - # When trying to assign a new connection to a selector and the chosen - # selector is at full capacity, retry selector choosing and assignment - # this many times before giving up - selector-association-retries = 10 - - # The maximum number of connection that are accepted in one go, - # higher numbers decrease latency, lower numbers increase fairness on - # the worker-dispatcher - batch-accept-limit = 10 - - # The number of bytes per direct buffer in the pool used to read or write - # network data from the kernel. - direct-buffer-size = 128 KiB - - # The maximal number of direct buffers kept in the direct buffer pool for - # reuse. - max-direct-buffer-pool-size = 1000 - - # The duration a connection actor waits for a `Register` message from - # its commander before aborting the connection. - register-timeout = 5s - - # The maximum number of bytes delivered by a `Received` message. Before - # more data is read from the network the connection actor will try to - # do other work. - received-message-size-limit = unlimited - - # Enable fine grained logging of what goes on inside the implementation. - # Be aware that this may log more than once per message sent to the actors - # of the tcp implementation. - trace-logging = off - - # Fully qualified config path which holds the dispatcher configuration - # to be used for running the select() calls in the selectors - selector-dispatcher = "akka.io.pinned-dispatcher" - - # Fully qualified config path which holds the dispatcher configuration - # for the read/write worker actors - worker-dispatcher = "akka.actor.default-dispatcher" - - # Fully qualified config path which holds the dispatcher configuration - # for the selector management actors - management-dispatcher = "akka.actor.default-dispatcher" - } - - } - -} diff --git a/project/AkkaBuild.scala b/project/AkkaBuild.scala index d1b1c66aa8..6bca2ff7c0 100644 --- a/project/AkkaBuild.scala +++ b/project/AkkaBuild.scala @@ -73,7 +73,7 @@ object AkkaBuild extends Build { generatedPdf in Sphinx <<= generatedPdf in Sphinx in LocalProject(docs.id) map identity ), - aggregate = Seq(actor, testkit, actorTests, dataflow, io, remote, remoteTests, camel, cluster, slf4j, agent, transactor, mailboxes, zeroMQ, kernel, akkaSbtPlugin, osgi, osgiAries, docs, contrib, samples) + aggregate = Seq(actor, testkit, actorTests, dataflow, remote, remoteTests, camel, cluster, slf4j, agent, transactor, mailboxes, zeroMQ, kernel, akkaSbtPlugin, osgi, osgiAries, docs, contrib, samples) ) lazy val actor = Project( @@ -100,13 +100,6 @@ object AkkaBuild extends Build { settings = defaultSettings ++ OSGi.dataflow ++ cpsPlugin ) - lazy val io = Project( - id = "akka-io", - base = file("akka-io"), - dependencies = Seq(actor, testkit % "test->test"), - settings = defaultSettings ++ OSGi.io - ) - lazy val testkit = Project( id = "akka-testkit", base = file("akka-testkit"), @@ -371,7 +364,7 @@ object AkkaBuild extends Build { lazy val docs = Project( id = "akka-docs", base = file("akka-docs"), - dependencies = Seq(actor, testkit % "test->test", mailboxesCommon % "compile;test->test", io, + dependencies = Seq(actor, testkit % "test->test", mailboxesCommon % "compile;test->test", remote, cluster, slf4j, agent, dataflow, transactor, fileMailbox, zeroMQ, camel, osgi, osgiAries), settings = defaultSettings ++ site.settings ++ site.sphinxSupport() ++ site.publishSite ++ sphinxPreprocessing ++ cpsPlugin ++ Seq( sourceDirectory in Sphinx <<= baseDirectory / "rst", @@ -658,8 +651,6 @@ object AkkaBuild extends Build { val fileMailbox = exports(Seq("akka.actor.mailbox.filebased.*")) - val io = exports(Seq("akka.io.*")) - val mailboxesCommon = exports(Seq("akka.actor.mailbox.*"), imports = Seq(protobufImport())) val osgi = exports(Seq("akka.osgi")) ++ Seq(OsgiKeys.privatePackage := Seq("akka.osgi.impl")) From 389768b488fd390c47e9e4c4ee031d7236f2dead Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 30 Jan 2013 11:28:47 +0100 Subject: [PATCH 47/66] clean up test output and help aggregation to pass on my Mac --- .../test/scala/akka/io/IntegrationSpec.scala | 6 +- .../scala/akka/io/TcpConnectionSpec.scala | 86 ++++++++++++------- .../test/scala/akka/io/TcpListenerSpec.scala | 8 +- 3 files changed, 63 insertions(+), 37 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala index 0863707936..e4d53f5f9b 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala @@ -8,6 +8,8 @@ import akka.testkit.AkkaSpec import akka.util.ByteString import Tcp._ import TestUtils._ +import akka.testkit.EventFilter +import java.io.IOException class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationSpecSupport { @@ -26,7 +28,9 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationS "properly handle connection abort from one side" in new TestSetup { val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection() - clientHandler.send(clientConnection, Abort) + EventFilter[IOException](occurrences = 1) intercept { + clientHandler.send(clientConnection, Abort) + } clientHandler.expectMsg(Aborted) serverHandler.expectMsgType[ErrorClosed] verifyActorTermination(clientConnection) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index b9b02313af..c5d67a50be 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -4,24 +4,24 @@ package akka.io -import scala.annotation.tailrec - -import org.scalatest.matchers.{ MatchResult, BeMatcher } - -import java.nio.channels.{ Selector, SelectionKey, SocketChannel, ServerSocketChannel } -import java.nio.ByteBuffer -import java.nio.channels.spi.SelectorProvider import java.io.IOException -import java.net._ +import java.net.{ ConnectException, InetSocketAddress, SocketException } +import java.nio.ByteBuffer +import java.nio.channels.{ SelectionKey, Selector, ServerSocketChannel, SocketChannel } +import java.nio.channels.spi.SelectorProvider +import scala.annotation.tailrec import scala.collection.immutable import scala.concurrent.duration._ import scala.util.control.NonFatal -import akka.actor.{ PoisonPill, ActorRef, Terminated } -import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } -import akka.util.ByteString -import TestUtils._ -import TcpSelector._ +import org.scalatest.matchers._ import Tcp._ +import TcpSelector._ +import TestUtils._ +import akka.actor.{ ActorRef, PoisonPill, Terminated } +import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe } +import akka.util.ByteString +import akka.actor.DeathPactException +import akka.actor.DeathPactException class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { val serverAddress = temporaryServerAddress() @@ -45,8 +45,10 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") createConnectionActor(options = Vector(SO.KeepAlive(true)))(selector.ref, userHandler.ref) val clientChannel = connectionActor.underlyingActor.channel clientChannel.socket.getKeepAlive must be(false) // only set after connection is established - selector.send(connectionActor, ChannelConnectable) - clientChannel.socket.getKeepAlive must be(true) + EventFilter.warning(pattern = "registration timeout", occurrences = 1) intercept { + selector.send(connectionActor, ChannelConnectable) + clientChannel.socket.getKeepAlive must be(true) + } } "send incoming data to the connection handler" in withEstablishedConnection() { setup ⇒ @@ -61,6 +63,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") expectReceivedString("testdata2testdata3") } + "bundle incoming Received messages as long as more data is available" in withEstablishedConnection( clientSocketOptions = List(SO.ReceiveBufferSize(1000000)) // to make sure enough data gets through ) { setup ⇒ @@ -70,6 +73,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val bigData = new Array[Byte](DataSize) val buffer = ByteBuffer.wrap(bigData) + serverSideChannel.socket.setSendBufferSize(150000) val wrote = serverSideChannel.write(buffer) wrote must be > 140000 @@ -80,6 +84,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // 140000 is more than the direct buffer size connectionHandler.expectMsgType[Received].data.length must be > 140000 } + "receive data directly when the connection is established" in withUnacceptedConnection() { unregisteredSetup ⇒ import unregisteredSetup._ @@ -282,9 +287,11 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") "report when peer aborted the connection" in withEstablishedConnection() { setup ⇒ import setup._ - abortClose(serverSideChannel) - selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + EventFilter[IOException](occurrences = 1) intercept { + abortClose(serverSideChannel) + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + } // wait a while connectionHandler.expectNoMsg(200.millis) @@ -296,9 +303,11 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val writer = TestProbe() abortClose(serverSideChannel) - writer.send(connectionActor, Write(ByteString("testdata"))) - // bother writer and handler should get the message - writer.expectMsgType[ErrorClosed] + EventFilter[IOException](occurrences = 1) intercept { + writer.send(connectionActor, Write(ByteString("testdata"))) + // bother writer and handler should get the message + writer.expectMsgType[ErrorClosed] + } connectionHandler.expectMsgType[ErrorClosed] assertThisConnectionActorTerminated() @@ -310,8 +319,10 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // close instead of accept localServer.close() - selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + EventFilter[SocketException](occurrences = 1) intercept { + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + } verifyActorTermination(connectionActor) } @@ -326,8 +337,10 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") sel.select(200) key.isConnectable must be(true) - selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClosed].cause must be("Connection refused") + EventFilter[ConnectException](occurrences = 1) intercept { + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsgType[ErrorClosed].cause must be("Connection refused") + } verifyActorTermination(connectionActor) } @@ -336,27 +349,34 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") import setup._ localServer.accept() - selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) - verifyActorTermination(connectionActor) + EventFilter.warning(pattern = "registration timeout", occurrences = 1) intercept { + selector.send(connectionActor, ChannelConnectable) + userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])) + + verifyActorTermination(connectionActor) + } } "close the connection when user handler dies while connecting" in withUnacceptedConnection() { setup ⇒ import setup._ - userHandler.ref ! PoisonPill + EventFilter[DeathPactException](occurrences = 1) intercept { + userHandler.ref ! PoisonPill - verifyActorTermination(connectionActor) + verifyActorTermination(connectionActor) + } } "close the connection when connection handler dies while connected" in withEstablishedConnection() { setup ⇒ import setup._ watch(connectionHandler.ref) watch(connectionActor) - system.stop(connectionHandler.ref) - expectMsgType[Terminated].actor must be(connectionHandler.ref) - expectMsgType[Terminated].actor must be(connectionActor) + EventFilter[DeathPactException](occurrences = 1) intercept { + system.stop(connectionHandler.ref) + expectMsgType[Terminated].actor must be(connectionHandler.ref) + expectMsgType[Terminated].actor must be(connectionActor) + } } } diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index 91902d0e5a..2a38c1547b 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -10,6 +10,7 @@ import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } import TcpSelector._ import Tcp._ +import akka.testkit.EventFilter class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { @@ -61,9 +62,10 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { val channel = selectorRouter.expectMsgType[RegisterIncomingConnection].channel channel.isOpen must be(true) - listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) - - awaitCond(!channel.isOpen) + EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept { + listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) + awaitCond(!channel.isOpen) + } } } From 5949d71b939e26c3ca36b064cb8898f13174cec0 Mon Sep 17 00:00:00 2001 From: Roland Date: Wed, 30 Jan 2013 12:08:48 +0100 Subject: [PATCH 48/66] deprecate old akka.actor.IO --- akka-actor/src/main/scala/akka/actor/IO.scala | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/akka-actor/src/main/scala/akka/actor/IO.scala b/akka-actor/src/main/scala/akka/actor/IO.scala index e1dedb3ba2..c3ec0af4e4 100644 --- a/akka-actor/src/main/scala/akka/actor/IO.scala +++ b/akka-actor/src/main/scala/akka/actor/IO.scala @@ -37,6 +37,7 @@ import akka.actor.IO.Chunk * This is still in an experimental state and is subject to change until it * has received more real world testing. */ +@deprecated("use the new implementation in package akka.io instead", "2.2") object IO { final class DivergentIterateeException extends IllegalStateException("Iteratees should not return a continuation when receiving EOF") @@ -833,6 +834,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension { * @param option Seq of [[akka.actor.IO.ServerSocketOptions]] to setup on socket * @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket */ + @deprecated("use the new implementation in package akka.io instead", "2.2") def listen(address: SocketAddress, options: immutable.Seq[IO.ServerSocketOption])(implicit owner: ActorRef): IO.ServerHandle = { val server = IO.ServerHandle(owner, actor) actor ! IO.Listen(server, address, options) @@ -848,6 +850,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension { * @param owner the ActorRef that will receive messages from the IOManagerActor * @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket */ + @deprecated("use the new implementation in package akka.io instead", "2.2") def listen(address: SocketAddress)(implicit owner: ActorRef): IO.ServerHandle = listen(address, Nil) /** @@ -861,6 +864,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension { * @param owner the ActorRef that will receive messages from the IOManagerActor * @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket */ + @deprecated("use the new implementation in package akka.io instead", "2.2") def listen(host: String, port: Int, options: immutable.Seq[IO.ServerSocketOption] = Nil)(implicit owner: ActorRef): IO.ServerHandle = listen(new InetSocketAddress(host, port), options)(owner) @@ -874,6 +878,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension { * @param owner the ActorRef that will receive messages from the IOManagerActor * @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket */ + @deprecated("use the new implementation in package akka.io instead", "2.2") def connect(address: SocketAddress, options: immutable.Seq[IO.SocketOption] = Nil)(implicit owner: ActorRef): IO.SocketHandle = { val socket = IO.SocketHandle(owner, actor) actor ! IO.Connect(socket, address, options) @@ -891,6 +896,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension { * @param owner the ActorRef that will receive messages from the IOManagerActor * @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket */ + @deprecated("use the new implementation in package akka.io instead", "2.2") def connect(host: String, port: Int)(implicit owner: ActorRef): IO.SocketHandle = connect(new InetSocketAddress(host, port))(owner) From 824158a6980edf2a085f1b97d8e58034843be8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Wed, 30 Jan 2013 18:10:45 +0100 Subject: [PATCH 49/66] First iteration of Fire-and-Forget style UDP IO --- .../scala/akka/io/UdpFFIntegrationSpec.scala | 59 ++++++ akka-actor/src/main/resources/reference.conf | 58 ++++++ akka-actor/src/main/scala/akka/io/UdpFF.scala | 177 +++++++++++++++++ .../main/scala/akka/io/UdpFFListener.scala | 89 +++++++++ .../src/main/scala/akka/io/UdpFFManager.scala | 60 ++++++ .../main/scala/akka/io/UdpFFSelector.scala | 186 ++++++++++++++++++ .../src/main/scala/akka/io/UdpFFSender.scala | 36 ++++ .../main/scala/akka/io/WithUdpFFSend.scala | 63 ++++++ 8 files changed, 728 insertions(+) create mode 100644 akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpFF.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpFFListener.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpFFManager.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpFFSelector.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpFFSender.scala create mode 100644 akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala new file mode 100644 index 0000000000..21d61c3e1d --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.testkit.{ TestProbe, ImplicitSender, AkkaSpec } +import akka.io.UdpFF._ +import TestUtils._ +import akka.util.ByteString +import java.net.InetSocketAddress +import akka.actor.ActorRef + +class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with ImplicitSender { + + def bindUdp(handler: ActorRef): (InetSocketAddress, ActorRef) = { + val address = temporaryServerAddress() + val commander = TestProbe() + commander.send(IO(UdpFF), Bind(handler, address)) + commander.expectMsg(Bound) + (address, commander.sender) + } + + val simpleSender: ActorRef = { + val commander = TestProbe() + commander.send(IO(UdpFF), SimpleSender) + commander.expectMsg(SimpleSendReady) + commander.sender + } + + "The UDP Fire-and-Forget implementation" must { + + "be able to send without binding" in { + val (serverAddress, server) = bindUdp(testActor) + val data = ByteString("To infinity and beyond!") + simpleSender ! Send(data, serverAddress) + + expectMsgPF() { + case Received(d, _) ⇒ + d must be === data + } + } + + "be able to send with binding" in { + val (serverAddress, _) = bindUdp(testActor) + val (clientAddress, client) = bindUdp(testActor) + val data = ByteString("Fly little packet!") + + client ! Send(data, serverAddress) + + expectMsgPF() { + case Received(d, a) ⇒ + d must be === data + a must be === clientAddress + } + } + + } + +} diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index 9d86ff75b2..859ae7acbf 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -445,6 +445,62 @@ akka { management-dispatcher = "akka.actor.default-dispatcher" } + udpFF { + + # The number of selectors to stripe the served channels over; each of + # these will use one select loop on the selector-dispatcher. + nr-of-selectors = 1 + + # Maximum number of open channels supported by this UDP module Generally + # UDP does not require a large number of channels, therefore it is + # recommended to keep this setting low. + max-channels = 4096 + + # The select loop can be used in two modes: + # - setting "infinite" will select without a timeout, hogging a thread + # - setting a positive timeout will do a bounded select call, + # enabling sharing of a single thread between multiple selectors + # (in this case you will have to use a different configuration for the + # selector-dispatcher, e.g. using "type=Dispatcher" with size 1) + # - setting it to zero means polling, i.e. calling selectNow() + select-timeout = infinite + + # When trying to assign a new connection to a selector and the chosen + # selector is at full capacity, retry selector choosing and assignment + # this many times before giving up + selector-association-retries = 10 + + # The number of bytes per direct buffer in the pool used to read or write + # network data from the kernel. + direct-buffer-size = 128 KiB + + # The maximal number of direct buffers kept in the direct buffer pool for + # reuse. + max-direct-buffer-pool-size = 1000 + + # The maximum number of bytes delivered by a `Received` message. Before + # more data is read from the network the connection actor will try to + # do other work. + received-message-size-limit = unlimited + + # Enable fine grained logging of what goes on inside the implementation. + # Be aware that this may log more than once per message sent to the actors + # of the tcp implementation. + trace-logging = off + + # Fully qualified config path which holds the dispatcher configuration + # to be used for running the select() calls in the selectors + selector-dispatcher = "akka.io.pinned-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the read/write worker actors + worker-dispatcher = "akka.actor.default-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the selector management actors + management-dispatcher = "akka.actor.default-dispatcher" + } + # IMPORTANT NOTICE: # # The following settings belong to the deprecated akka.actor.IO @@ -463,4 +519,6 @@ akka { # 0 or negative means that the platform default will be used. default-backlog = 1000 } + + } diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala new file mode 100644 index 0000000000..b6d78fc1d1 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -0,0 +1,177 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor._ +import akka.util.ByteString +import java.net.{ DatagramSocket, Socket, InetSocketAddress } +import scala.collection.immutable +import com.typesafe.config.Config +import scala.concurrent.duration.Duration +import java.nio.ByteBuffer + +object UdpFF extends ExtensionKey[UdpFFExt] { + + // Java API + override def get(system: ActorSystem): UdpFFExt = system.extension(this) + + /** + * SocketOption is a package of data (from the user) and associated + * behavior (how to apply that to a socket). + */ + sealed trait SocketOption { + /** + * Action to be taken for this option before calling bind() + */ + def beforeBind(s: DatagramSocket): Unit = () + + } + + object SO { + + /** + * [[akka.io.UdpFF.SocketOption]] to set the SO_BROADCAST option + * + * For more information see [[java.net.DatagramSocket#setBroadcast]] + */ + case class Broadcast(on: Boolean) extends SocketOption { + override def beforeBind(s: DatagramSocket): Unit = s.setBroadcast(on) + } + + /** + * [[akka.io.UdpFF.SocketOption]] to set the SO_RCVBUF option + * + * For more information see [[java.net.Socket#setReceiveBufferSize]] + */ + case class ReceiveBufferSize(size: Int) extends SocketOption { + require(size > 0, "ReceiveBufferSize must be > 0") + override def beforeBind(s: DatagramSocket): Unit = s.setReceiveBufferSize(size) + } + + /** + * [[akka.io.UdpFF.SocketOption]] to enable or disable SO_REUSEADDR + * + * For more information see [[java.net.Socket#setReuseAddress]] + */ + case class ReuseAddress(on: Boolean) extends SocketOption { + override def beforeBind(s: DatagramSocket): Unit = s.setReuseAddress(on) + } + + /** + * [[akka.io.UdpFF.SocketOption]] to set the SO_SNDBUF option. + * + * For more information see [[java.net.Socket#setSendBufferSize]] + */ + case class SendBufferSize(size: Int) extends SocketOption { + require(size > 0, "SendBufferSize must be > 0") + override def beforeBind(s: DatagramSocket): Unit = s.setSendBufferSize(size) + } + + /** + * [[akka.io.UdpFF.SocketOption]] to set the traffic class or + * type-of-service octet in the IP header for packets sent from this + * socket. + * + * For more information see [[java.net.Socket#setTrafficClass]] + */ + case class TrafficClass(tc: Int) extends SocketOption { + require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]") + override def beforeBind(s: DatagramSocket): Unit = s.setTrafficClass(tc) + } + } + + trait Command + + case object NoAck + case class Send(payload: ByteString, target: InetSocketAddress, ack: Any) extends Command { + require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.") + + def wantsAck: Boolean = ack != NoAck + } + object Send { + def apply(data: ByteString, target: InetSocketAddress): Send = Send(data, target, NoAck) + } + + case class Bind(handler: ActorRef, + endpoint: InetSocketAddress, + options: immutable.Traversable[SocketOption] = Nil) extends Command + case object Unbind extends Command + + case object SimpleSender extends Command + + case object StopReading extends Command + case object ResumeReading extends Command + + trait Event + + case class Received(data: ByteString, sender: InetSocketAddress) extends Event + case class CommandFailed(cmd: Command) extends Event + case object Bound extends Event + case object SimpleSendReady extends Event + case object Unbound extends Event + + sealed trait CloseCommand extends Command + case object Close extends CloseCommand + case object Abort extends CloseCommand + + case class SendFailed(cause: Throwable) extends Event + +} + +class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { + + val Settings = new Settings(system.settings.config.getConfig("akka.io.udpFF")) + class Settings private[UdpFFExt] (config: Config) { + import config._ + + val NrOfSelectors = getInt("nr-of-selectors") + val MaxChannels = getString("max-channels") match { + case "unlimited" ⇒ -1 + case _ ⇒ getInt("max-channels") + } + val SelectTimeout = getString("select-timeout") match { + case "infinite" ⇒ Duration.Inf + case x ⇒ Duration(x) + } + val SelectorAssociationRetries = getInt("selector-association-retries") + val DirectBufferSize = getIntBytes("direct-buffer-size") + val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") + + val SelectorDispatcher = getString("selector-dispatcher") + val WorkerDispatcher = getString("worker-dispatcher") + val ManagementDispatcher = getString("management-dispatcher") + val TraceLogging = getBoolean("trace-logging") + + require(NrOfSelectors > 0, "nr-of-selectors must be > 0") + require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'") + require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative") + require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0") + + val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) + + private[this] def getIntBytes(path: String): Int = { + val size = getBytes(path) + require(size < Int.MaxValue, s"$path must be < 2 GiB") + size.toInt + } + } + + val manager = { + system.asInstanceOf[ActorSystemImpl].systemActorOf( + props = Props(new UdpFFManager(this)), + name = "IO-UDP-FF") + } + + val bufferPool: BufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) +} + +trait WithUdpFFBufferPool { + def udpFF: UdpFFExt + + def acquireBuffer(): ByteBuffer = + udpFF.bufferPool.acquire() + + def releaseBuffer(buffer: ByteBuffer): Unit = + udpFF.bufferPool.release(buffer) +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala new file mode 100644 index 0000000000..f8b72e6f6c --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.{ ActorLogging, Actor, ActorRef } +import akka.io.UdpFF._ +import akka.io.UdpFFSelector._ +import akka.util.ByteString +import java.net.InetSocketAddress +import java.nio.channels.DatagramChannel +import java.nio.channels.SelectionKey._ +import scala.collection.immutable +import scala.util.control.NonFatal + +private[io] class UdpFFListener(selectorRouter: ActorRef, + handler: ActorRef, + endpoint: InetSocketAddress, + bindCommander: ActorRef, + val udpFF: UdpFFExt, + options: immutable.Traversable[SocketOption]) + extends Actor with ActorLogging with WithUdpFFBufferPool with WithUdpFFSend { + import udpFF.Settings._ + + def selector: ActorRef = context.parent + + context.watch(handler) // sign death pact + val channel = { + val datagramChannel = DatagramChannel.open + datagramChannel.configureBlocking(false) + val socket = datagramChannel.socket + options.foreach(_.beforeBind(socket)) + socket.bind(endpoint) // will blow up the actor constructor if the bind fails + datagramChannel + } + context.parent ! RegisterDatagramChannel(channel, OP_READ) + bindCommander ! Bound + log.debug("Successfully bound to {}", endpoint) + + def receive: Receive = receiveInternal orElse sendHandlers + + def receiveInternal: Receive = { + case StopReading ⇒ selector ! StopReading + case ResumeReading ⇒ selector ! ReadInterest + case ChannelReadable ⇒ doReceive(handler, None) + + case CommandFailed(RegisterDatagramChannel(datagramChannel, _)) ⇒ + log.warning("Could not bind to UDP port since selector capacity limit is reached, aborting bind") + try datagramChannel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing channel") + } + + case Unbind ⇒ + log.debug("Unbinding endpoint {}", endpoint) + channel.close() + sender ! Unbound + log.debug("Unbound endpoint {}, stopping listener", endpoint) + context.stop(self) + } + + def doReceive(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = { + val buffer = acquireBuffer() + try { + buffer.clear() + buffer.limit(DirectBufferSize) + + channel.receive(buffer) match { + case sender: InetSocketAddress ⇒ + buffer.flip() + handler ! Received(ByteString(buffer), sender) + case _ ⇒ // Ignore + } + + selector ! ReadInterest + } finally releaseBuffer(buffer) + } + + override def postStop() { + try { + if (channel.isOpen) { + log.debug("Closing serverSocketChannel after being stopped") + channel.close() + } + } catch { + case NonFatal(e) ⇒ log.error(e, "Error closing ServerSocketChannel") + } + } +} diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala new file mode 100644 index 0000000000..f4c39fad1a --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.{ ActorRef, Props, Actor } +import akka.io.UdpFF._ +import akka.routing.RandomRouter + +/** + * UdpFFManager is a facade for simple fire-and-forget style UDP operations + * + * UdpFFManager is obtainable by calling {{{ IO(UdpFF) }}} (see [[akka.io.IO]] and [[akka.io.UdpFF]]) + * + * *Warning!* UdpFF uses [[java.nio.channels.DatagramChannel#send]] to deliver datagrams, and as a consequence if a + * security manager has been installed then for each datagram it will verify if the target address and port number are + * permitted. If this performance overhead is undesirable use the connection style Udp extension. + * + * == Bind and send == + * + * To bind and listen to a local address, a [[akka.io.UdpFF..Bind]] command must be sent to this actor. If the binding + * was successful, the sender of the [[akka.io.UdpFF.Bind]] will be notified with a [[akka.io.UdpFF.Bound]] + * message. The sender of the [[akka.io.UdpFF.Bound]] message is the Listener actor (an internal actor responsible for + * listening to server events). To unbind the port an [[akka.io.Tcp.Unbind]] message must be sent to the Listener actor. + * + * If the bind request is rejected because the Udp system is not able to register more channels (see the nr-of-selectors + * and max-channels configuration options in the akka.io.udpFF section of the configuration) the sender will be notified + * with a [[akka.io.UdpFF.CommandFailed]] message. This message contains the original command for reference. + * + * The handler provided in the [[akka.io.UdpFF.Bind]] message will receive inbound datagrams to the bound port + * wrapped in [[akka.io.UdpFF.Received]] messages which contain the payload of the datagram and the sender address. + * + * UDP datagrams can be sent by sending [[akka.io.UdpFF.Send]] messages to the Listener actor. The sender port of the + * outbound datagram will be the port to which the Listener is bound. + * + * == Simple send == + * + * UdpFF provides a simple method of sending UDP datagrams if no reply is expected. To acquire the Sender actor + * a SimpleSend message has to be sent to the manager. The sender of the command will be notified by a SimpleSendReady + * message that the service is available. UDP datagrams can be sent by sending [[akka.io.UdpFF.Send]] messages to the + * sender of SimpleSendReady. All the datagrams will contain an ephemeral local port as sender and answers will be + * discarded. + * + */ +private[io] class UdpFFManager(udpFF: UdpFFExt) extends Actor { + + val selectorPool = context.actorOf( + props = Props(new UdpFFSelector(self, udpFF)).withRouter(RandomRouter(udpFF.Settings.NrOfSelectors)), + name = "selectors") + + lazy val anonymousSender: ActorRef = context.actorOf( + props = Props(new UdpFFSender(udpFF, selectorPool)), + name = "simplesend") + + def receive = { + case c: Bind ⇒ selectorPool forward c + case SimpleSender ⇒ anonymousSender forward SimpleSender + } + +} diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala b/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala new file mode 100644 index 0000000000..86ebbd2a94 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala @@ -0,0 +1,186 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.io.UdpFF._ +import akka.actor._ +import java.lang.Runnable +import java.nio.channels.{ DatagramChannel, SelectionKey } +import java.nio.channels.SelectionKey._ +import java.nio.channels.spi.SelectorProvider +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.util.control.NonFatal + +private[io] object UdpFFSelector { + case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } + case class RegisterDatagramChannel(channel: DatagramChannel, initialOps: Int) extends Command + + case object ChannelReadable + case object ChannelWritable + case object ReadInterest + case object WriteInterest +} + +private[io] class UdpFFSelector(manager: ActorRef, udp: UdpFFExt) extends Actor with ActorLogging { + + import UdpFFSelector._ + import udp.Settings._ + + @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] + val sequenceNumber = Iterator from 0 + val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) + val selector = SelectorProvider.provider.openSelector + val OP_READ_AND_WRITE = OP_READ + OP_WRITE // compile-time constant + + def receive: Receive = { + case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) + case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) + case StopReading ⇒ execute(disableInterest(OP_READ, sender)) + + case cmd: Bind ⇒ + handleBind(cmd, SelectorAssociationRetries) + + case RegisterDatagramChannel(channel, initialOps) ⇒ + execute(registerDatagramChannel(channel, sender, initialOps)) + + case Retry(command, 0) ⇒ + log.warning("Command '{}' failed since all selectors are at capacity", command) + sender ! CommandFailed(command) + + case Retry(cmd: Bind, retriesLeft) ⇒ + handleBind(cmd, retriesLeft) + + case Terminated(child) ⇒ + execute(unregister(child)) + } + + override def postStop() { + try { + try { + val iterator = selector.keys.iterator + while (iterator.hasNext) iterator.next().channel.close() + } finally selector.close() + } catch { + case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") + } + } + + // we can never recover from failures of a connection or listener child + override def supervisorStrategy = SupervisorStrategy.stoppingStrategy + + def handleBind(cmd: Bind, retriesLeft: Int): Unit = + withCapacityProtection(cmd, retriesLeft) { + import cmd._ + val commander = sender + spawnChild(() ⇒ new UdpFFListener(context.parent, handler, endpoint, commander, udp, options)) + } + + def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { + log.debug("Executing {}", cmd) + if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { + body + } else { + log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) + context.parent forward Retry(cmd, retriesLeft - 1) + } + } + + def spawnChild(creator: () ⇒ Actor) = + context.watch { + context.actorOf( + props = Props(creator, dispatcher = WorkerDispatcher), + name = sequenceNumber.next().toString) + } + + //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// + + def execute(task: Task): Unit = { + selectorManagementDispatcher.execute(task) + selector.wakeup() + } + + def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = + childrenKeys = childrenKeys.updated(child.path.name, key) + + def registerDatagramChannel(channel: DatagramChannel, connection: ActorRef, initialOps: Int) = + new Task { + def tryRun() { + val key = channel.register(selector, initialOps, connection) + updateKeyMap(connection, key) + } + } + + // TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than + // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap + // rather than an unsynchronized one, but since switching interest ops is so frequent + // the change might be beneficial, provided the underlying implementation really is thread-safe + // and behaves consistently on all platforms. + def enableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps | op) + } + } + + def disableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps & ~op) + } + } + + def unregister(child: ActorRef) = + new Task { + def tryRun() { + childrenKeys = childrenKeys - child.path.name + } + } + + val select = new Task { + val doSelect: () ⇒ Int = + SelectTimeout match { + case Duration.Zero ⇒ () ⇒ selector.selectNow() + case Duration.Inf ⇒ () ⇒ selector.select() + case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) + } + def tryRun() { + if (doSelect() > 0) { + val keys = selector.selectedKeys + val iterator = keys.iterator() + while (iterator.hasNext) { + val key = iterator.next + if (key.isValid) { + key.interestOps(0) // prevent immediate reselection by always clearing + val connection = key.attachment.asInstanceOf[ActorRef] + key.readyOps match { + case OP_READ ⇒ connection ! ChannelReadable + case OP_WRITE ⇒ connection ! ChannelWritable + case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable + case x ⇒ log.warning("Invalid readyOps: {}", x) + } + } else log.warning("Invalid selection key: {}", key) + } + keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected + } + selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks + } + } + + selectorManagementDispatcher.execute(select) // start selection "loop" + + abstract class Task extends Runnable { + def tryRun() + def run() { + try tryRun() + catch { + case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown + case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) + } + } + } +} + diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala new file mode 100644 index 0000000000..11218531d1 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor._ +import java.nio.channels.DatagramChannel +import akka.io.UdpFF._ +import akka.io.UdpFFSelector.RegisterDatagramChannel + +/** + * Base class for TcpIncomingConnection and TcpOutgoingConnection. + */ +private[io] class UdpFFSender(val udpFF: UdpFFExt, val selector: ActorRef) + extends Actor with ActorLogging with WithUdpFFBufferPool with WithUdpFFSend { + + val channel = { + val datagramChannel = DatagramChannel.open + datagramChannel.configureBlocking(false) + datagramChannel + } + selector ! RegisterDatagramChannel(channel, 0) + + def receive: Receive = internalReceive orElse sendHandlers + + def internalReceive: Receive = { + case SimpleSender ⇒ sender ! SimpleSendReady + } + + override def postStop(): Unit = if (channel.isOpen) channel.close() + + override def postRestart(reason: Throwable): Unit = + throw new IllegalStateException("Restarting not supported for connection actors.") + +} + diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala new file mode 100644 index 0000000000..8e81f584fb --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.{ ActorRef, ActorLogging, Actor } +import akka.io.UdpFF.{ CommandFailed, Send } +import akka.io.UdpFFSelector._ +import java.nio.channels.DatagramChannel + +trait WithUdpFFSend { + me: Actor with ActorLogging with WithUdpFFBufferPool ⇒ + + var pendingSend: (Send, ActorRef) = null + def writePending = pendingSend ne null + + def selector: ActorRef + def channel: DatagramChannel + def udpFF: UdpFFExt + val settings = udpFF.Settings + + import settings._ + + def sendHandlers: Receive = { + + case send: Send if writePending ⇒ + if (TraceLogging) log.debug("Dropping write because queue is full") + sender ! CommandFailed(send) + + case send: Send if send.payload.isEmpty ⇒ + if (send.wantsAck) + sender ! send.ack + + case send: Send ⇒ + pendingSend = (send, sender) + selector ! WriteInterest + + case ChannelWritable ⇒ doSend() + + } + + final def doSend(): Unit = { + + val buffer = acquireBuffer() + try { + val (send, commander) = pendingSend + buffer.clear() + send.payload.copyToBuffer(buffer) + buffer.flip() + val writtenBytes = channel.send(buffer, send.target) + if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) + + // Datagram channel either sends the whole message, or nothing + if (writtenBytes == 0) commander ! CommandFailed(send) + else if (send.wantsAck) commander ! send.ack + + } finally { + releaseBuffer(buffer) + pendingSend = null + } + + } +} From ad60b155c624b8034542186ea21a4c5118ee1721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Fri, 1 Feb 2013 12:38:13 +0100 Subject: [PATCH 50/66] Temporary LIMBO commit, but UDP now uses the unified selector --- .../main/scala/akka/io/SelectionHandler.scala | 288 ++++++++++++++++++ akka-actor/src/main/scala/akka/io/UdpFF.scala | 25 +- .../main/scala/akka/io/UdpFFListener.scala | 18 +- .../src/main/scala/akka/io/UdpFFManager.scala | 9 +- .../main/scala/akka/io/UdpFFSelector.scala | 2 +- .../src/main/scala/akka/io/UdpFFSender.scala | 4 +- .../main/scala/akka/io/WithUdpFFSend.scala | 4 +- 7 files changed, 314 insertions(+), 36 deletions(-) create mode 100644 akka-actor/src/main/scala/akka/io/SelectionHandler.scala diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala new file mode 100644 index 0000000000..e1f48de592 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -0,0 +1,288 @@ +/** + * Copyright (C) 2009-2012 Typesafe Inc. + */ + +package akka.io + +import java.lang.Runnable +import java.nio.channels.spi.SelectorProvider +import java.nio.channels.{ SelectableChannel, SelectionKey } +import java.nio.channels.SelectionKey._ +import scala.util.control.NonFatal +import scala.collection.immutable +import scala.concurrent.duration._ +import akka.actor._ +import com.typesafe.config.Config +import akka.actor.Terminated + +abstract class SelectionHandlerSettings(config: Config) { + import config._ + + val MaxChannels = getString("max-channels") match { + case "unlimited" ⇒ -1 + case _ ⇒ getInt("max-channels") + } + val SelectTimeout = getString("select-timeout") match { + case "infinite" ⇒ Duration.Inf + case x ⇒ Duration(x) + } + val SelectorAssociationRetries = getInt("selector-association-retries") + + val SelectorDispatcher = getString("selector-dispatcher") + val WorkerDispatcher = getString("worker-dispatcher") + val TraceLogging = getBoolean("trace-logging") + + require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'") + require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative") + require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0") + + def MaxChannelsPerSelector: Int + +} + +private[io] object SelectionHandler { + //FIXME: temporary + case class KickStartCommand(childProps: Props) + + case class RegisterChannel(channel: SelectableChannel, initialOps: Int) + case class Retry(command: KickStartCommand, retriesLeft: Int) { require(retriesLeft >= 0) } + + case object ChannelConnectable + case object ChannelAcceptable + case object ChannelReadable + case object ChannelWritable + case object AcceptInterest + case object ReadInterest + case object WriteInterest +} + +private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandlerSettings) extends Actor with ActorLogging { + import SelectionHandler._ + import settings._ + + @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] + val sequenceNumber = Iterator.from(0) + val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) + val selector = SelectorProvider.provider.openSelector + val OP_READ_AND_WRITE = OP_READ | OP_WRITE // compile-time constant + + def receive: Receive = { + case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) + case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) + case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) + + //case StopReading ⇒ execute(disableInterest(OP_READ, sender)) + + // case cmd: RegisterIncomingConnection ⇒ + // handleIncomingConnection(cmd, SelectorAssociationRetries) + // + // case cmd: Connect ⇒ + // handleConnect(cmd, SelectorAssociationRetries) + // + // case cmd: Bind ⇒ + // handleBind(cmd, SelectorAssociationRetries) + + case cmd: KickStartCommand ⇒ + kickStart(cmd, SelectorAssociationRetries) + + // case RegisterOutgoingConnection(channel) ⇒ + // execute(registerOutgoingConnection(channel, sender)) + // + // case RegisterServerSocketChannel(channel) ⇒ + // execute(registerListener(channel, sender)) + + case RegisterChannel(channel, initialOps) ⇒ + execute(registerChannel(channel, sender, initialOps)) + + // case Retry(command, 0) ⇒ + // log.warning("Command '{}' failed since all selectors are at capacity", command) + // sender ! CommandFailed(command) + + case Retry(cmd, retriesLeft) ⇒ + kickStart(cmd, retriesLeft) + + // case Retry(cmd: RegisterIncomingConnection, retriesLeft) ⇒ + // handleIncomingConnection(cmd, retriesLeft) + // + // case Retry(cmd: Connect, retriesLeft) ⇒ + // handleConnect(cmd, retriesLeft) + // + // case Retry(cmd: Bind, retriesLeft) ⇒ + // handleBind(cmd, retriesLeft) + + case Terminated(child) ⇒ + execute(unregister(child)) + } + + override def postStop() { + try { + try { + val iterator = selector.keys.iterator + while (iterator.hasNext) iterator.next().channel.close() + } finally selector.close() + } catch { + case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") + } + } + + // we can never recover from failures of a connection or listener child + override def supervisorStrategy = SupervisorStrategy.stoppingStrategy + + // def handleIncomingConnection(cmd: RegisterIncomingConnection, retriesLeft: Int): Unit = + // withCapacityProtection(cmd, retriesLeft) { + // import cmd._ + // val connection = spawnChild(() ⇒ new TcpIncomingConnection(channel, tcp, handler, options)) + // execute(registerIncomingConnection(channel, connection)) + // } + // + // def handleConnect(cmd: Connect, retriesLeft: Int): Unit = + // withCapacityProtection(cmd, retriesLeft) { + // import cmd._ + // val commander = sender + // spawnChild(() ⇒ new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) + // } + // + // def handleBind(cmd: Bind, retriesLeft: Int): Unit = + // withCapacityProtection(cmd, retriesLeft) { + // import cmd._ + // val commander = sender + // spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp.Settings, options)) + // } + + def kickStart(cmd: KickStartCommand, retriesLeft: Int): Unit = withCapacityProtection(cmd, retriesLeft) { + spawnChild(cmd.childProps) // TODO: inject sender somehow + } + + def withCapacityProtection(cmd: KickStartCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { + log.debug("Executing {}", cmd) + if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { + body + } else { + log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) + context.parent forward Retry(cmd, retriesLeft - 1) + } + } + + def spawnChild(props: Props) = + context.watch { + context.actorOf( + props = props.withDispatcher(WorkerDispatcher), + name = sequenceNumber.next().toString) + } + + //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// + + def execute(task: Task): Unit = { + selectorManagementDispatcher.execute(task) + selector.wakeup() + } + + def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = + childrenKeys = childrenKeys.updated(child.path.name, key) + + // def registerOutgoingConnection(channel: SocketChannel, connection: ActorRef) = + // new Task { + // def tryRun() { + // val key = channel.register(selector, OP_CONNECT, connection) + // updateKeyMap(connection, key) + // } + // } + // + // def registerListener(channel: ServerSocketChannel, listener: ActorRef) = + // new Task { + // def tryRun() { + // val key = channel.register(selector, OP_ACCEPT, listener) + // updateKeyMap(listener, key) + // listener ! Bound + // } + // } + // + // def registerIncomingConnection(channel: SocketChannel, connection: ActorRef) = + // new Task { + // def tryRun() { + // // we only enable reading after the user-level connection handler has registered + // val key = channel.register(selector, 0, connection) + // updateKeyMap(connection, key) + // } + // } + + def registerChannel(channel: SelectableChannel, channelActor: ActorRef, initialOps: Int): Task = + new Task { + def tryRun() { + updateKeyMap(channelActor, channel.register(selector, initialOps, channelActor)) + } + } + + // TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than + // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap + // rather than an unsynchronized one, but since switching interest ops is so frequent + // the change might be beneficial, provided the underlying implementation really is thread-safe + // and behaves consistently on all platforms. + def enableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps | op) + } + } + + def disableInterest(op: Int, connection: ActorRef) = + new Task { + def tryRun() { + val key = childrenKeys(connection.path.name) + key.interestOps(key.interestOps & ~op) + } + } + + def unregister(child: ActorRef) = + new Task { + def tryRun() { + childrenKeys = childrenKeys - child.path.name + } + } + + val select = new Task { + val doSelect: () ⇒ Int = + SelectTimeout match { + case Duration.Zero ⇒ () ⇒ selector.selectNow() + case Duration.Inf ⇒ () ⇒ selector.select() + case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) + } + def tryRun() { + if (doSelect() > 0) { + val keys = selector.selectedKeys + val iterator = keys.iterator() + while (iterator.hasNext) { + val key = iterator.next + if (key.isValid) { + key.interestOps(0) // prevent immediate reselection by always clearing + val connection = key.attachment.asInstanceOf[ActorRef] + key.readyOps match { + case OP_READ ⇒ connection ! ChannelReadable + case OP_WRITE ⇒ connection ! ChannelWritable + case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable + case x if (x & OP_ACCEPT) > 0 ⇒ connection ! ChannelAcceptable + case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable + case x ⇒ log.warning("Invalid readyOps: {}", x) + } + } else log.warning("Invalid selection key: {}", key) + } + keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected + } + selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks + } + } + + selectorManagementDispatcher.execute(select) // start selection "loop" + + abstract class Task extends Runnable { + def tryRun() + def run() { + try tryRun() + catch { + case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown + case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) + } + } + } +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index b6d78fc1d1..a82c01a56a 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -121,34 +121,19 @@ object UdpFF extends ExtensionKey[UdpFFExt] { class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { - val Settings = new Settings(system.settings.config.getConfig("akka.io.udpFF")) - class Settings private[UdpFFExt] (config: Config) { - import config._ + val settings = new Settings(system.settings.config.getConfig("akka.io.udpFF")) + class Settings private[UdpFFExt] (_config: Config) extends SelectionHandlerSettings(_config) { + import _config._ val NrOfSelectors = getInt("nr-of-selectors") - val MaxChannels = getString("max-channels") match { - case "unlimited" ⇒ -1 - case _ ⇒ getInt("max-channels") - } - val SelectTimeout = getString("select-timeout") match { - case "infinite" ⇒ Duration.Inf - case x ⇒ Duration(x) - } - val SelectorAssociationRetries = getInt("selector-association-retries") val DirectBufferSize = getIntBytes("direct-buffer-size") val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") - val SelectorDispatcher = getString("selector-dispatcher") - val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") - val TraceLogging = getBoolean("trace-logging") require(NrOfSelectors > 0, "nr-of-selectors must be > 0") - require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'") - require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative") - require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0") - val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) + override val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) private[this] def getIntBytes(path: String): Int = { val size = getBytes(path) @@ -163,7 +148,7 @@ class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { name = "IO-UDP-FF") } - val bufferPool: BufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize) + val bufferPool: BufferPool = new DirectByteBufferPool(settings.DirectBufferSize, settings.MaxDirectBufferPoolSize) } trait WithUdpFFBufferPool { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index f8b72e6f6c..f356163303 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -5,7 +5,7 @@ package akka.io import akka.actor.{ ActorLogging, Actor, ActorRef } import akka.io.UdpFF._ -import akka.io.UdpFFSelector._ +import akka.io.SelectionHandler._ import akka.util.ByteString import java.net.InetSocketAddress import java.nio.channels.DatagramChannel @@ -20,7 +20,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, val udpFF: UdpFFExt, options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging with WithUdpFFBufferPool with WithUdpFFSend { - import udpFF.Settings._ + import udpFF.settings._ def selector: ActorRef = context.parent @@ -33,7 +33,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, socket.bind(endpoint) // will blow up the actor constructor if the bind fails datagramChannel } - context.parent ! RegisterDatagramChannel(channel, OP_READ) + context.parent ! RegisterChannel(channel, OP_READ) bindCommander ! Bound log.debug("Successfully bound to {}", endpoint) @@ -44,12 +44,12 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doReceive(handler, None) - case CommandFailed(RegisterDatagramChannel(datagramChannel, _)) ⇒ - log.warning("Could not bind to UDP port since selector capacity limit is reached, aborting bind") - try datagramChannel.close() - catch { - case NonFatal(e) ⇒ log.error(e, "Error closing channel") - } + // case CommandFailed(RegisterChannel(channel, _)) ⇒ + // log.warning("Could not bind to UDP port since selector capacity limit is reached, aborting bind") + // try channel.close() + // catch { + // case NonFatal(e) ⇒ log.error(e, "Error closing channel") + // } case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index f4c39fad1a..d699f7f18e 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -6,6 +6,7 @@ package akka.io import akka.actor.{ ActorRef, Props, Actor } import akka.io.UdpFF._ import akka.routing.RandomRouter +import akka.io.SelectionHandler.KickStartCommand /** * UdpFFManager is a facade for simple fire-and-forget style UDP operations @@ -45,15 +46,19 @@ import akka.routing.RandomRouter private[io] class UdpFFManager(udpFF: UdpFFExt) extends Actor { val selectorPool = context.actorOf( - props = Props(new UdpFFSelector(self, udpFF)).withRouter(RandomRouter(udpFF.Settings.NrOfSelectors)), + props = Props(new SelectionHandler(self, udpFF.settings)).withRouter(RandomRouter(udpFF.settings.NrOfSelectors)), name = "selectors") + // FIXME: fix close overs lazy val anonymousSender: ActorRef = context.actorOf( props = Props(new UdpFFSender(udpFF, selectorPool)), name = "simplesend") def receive = { - case c: Bind ⇒ selectorPool forward c + case Bind(handler, endpoint, options) ⇒ + val commander = sender + selectorPool forward KickStartCommand(Props( + new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options))) case SimpleSender ⇒ anonymousSender forward SimpleSender } diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala b/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala index 86ebbd2a94..ec151ade47 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala @@ -26,7 +26,7 @@ private[io] object UdpFFSelector { private[io] class UdpFFSelector(manager: ActorRef, udp: UdpFFExt) extends Actor with ActorLogging { import UdpFFSelector._ - import udp.Settings._ + import udp.settings._ @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] val sequenceNumber = Iterator from 0 diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala index 11218531d1..c1e23c4213 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -6,7 +6,7 @@ package akka.io import akka.actor._ import java.nio.channels.DatagramChannel import akka.io.UdpFF._ -import akka.io.UdpFFSelector.RegisterDatagramChannel +import akka.io.SelectionHandler.RegisterChannel /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. @@ -19,7 +19,7 @@ private[io] class UdpFFSender(val udpFF: UdpFFExt, val selector: ActorRef) datagramChannel.configureBlocking(false) datagramChannel } - selector ! RegisterDatagramChannel(channel, 0) + selector ! RegisterChannel(channel, 0) def receive: Receive = internalReceive orElse sendHandlers diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala index 8e81f584fb..07ad4132e7 100644 --- a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -5,7 +5,7 @@ package akka.io import akka.actor.{ ActorRef, ActorLogging, Actor } import akka.io.UdpFF.{ CommandFailed, Send } -import akka.io.UdpFFSelector._ +import akka.io.SelectionHandler._ import java.nio.channels.DatagramChannel trait WithUdpFFSend { @@ -17,7 +17,7 @@ trait WithUdpFFSend { def selector: ActorRef def channel: DatagramChannel def udpFF: UdpFFExt - val settings = udpFF.Settings + val settings = udpFF.settings import settings._ From 8b4a3b0b9271bdb5b1efdcebb36a998c12ab21fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Fri, 1 Feb 2013 13:11:17 +0100 Subject: [PATCH 51/66] Another LIMBO commit, but TCP now uses the unified selector --- .../test/scala/akka/io/TcpListenerSpec.scala | 2 +- .../main/scala/akka/io/SelectionHandler.scala | 6 +- akka-actor/src/main/scala/akka/io/Tcp.scala | 18 +- .../main/scala/akka/io/TcpConnection.scala | 2 +- .../scala/akka/io/TcpIncomingConnection.scala | 2 + .../src/main/scala/akka/io/TcpListener.scala | 30 +-- .../src/main/scala/akka/io/TcpManager.scala | 12 +- .../scala/akka/io/TcpOutgoingConnection.scala | 6 +- .../src/main/scala/akka/io/TcpSelector.scala | 2 +- .../src/main/scala/akka/io/UdpFFManager.scala | 2 +- .../main/scala/akka/io/UdpFFSelector.scala | 186 ------------------ 11 files changed, 43 insertions(+), 225 deletions(-) delete mode 100644 akka-actor/src/main/scala/akka/io/UdpFFSelector.scala diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index 2a38c1547b..0ec8f792bd 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -94,7 +94,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { private class ListenerParent extends Actor { val listener = context.actorOf( props = Props(new TcpListener(selectorRouter.ref, handler.ref, endpoint, 100, bindCommander.ref, - Tcp(system).Settings, Nil)), + Tcp(system), Nil)), name = "test-listener-" + counter.next()) parent.watch(listener) def receive: Receive = { diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index e1f48de592..1389ea8c1f 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -43,6 +43,8 @@ abstract class SelectionHandlerSettings(config: Config) { private[io] object SelectionHandler { //FIXME: temporary case class KickStartCommand(childProps: Props) + // FIXME: all actors should listen to this + case object KickStartDone case class RegisterChannel(channel: SelectableChannel, initialOps: Int) case class Retry(command: KickStartCommand, retriesLeft: Int) { require(retriesLeft >= 0) } @@ -150,7 +152,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler // } def kickStart(cmd: KickStartCommand, retriesLeft: Int): Unit = withCapacityProtection(cmd, retriesLeft) { - spawnChild(cmd.childProps) // TODO: inject sender somehow + spawnChild(cmd.childProps) ! KickStartDone } def withCapacityProtection(cmd: KickStartCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { @@ -163,7 +165,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler } } - def spawnChild(props: Props) = + def spawnChild(props: Props): ActorRef = context.watch { context.actorOf( props = props.withDispatcher(WorkerDispatcher), diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index df3e3fe320..4b71c81676 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -179,19 +179,12 @@ object Tcp extends ExtensionKey[TcpExt] { class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val Settings = new Settings(system.settings.config.getConfig("akka.io.tcp")) - class Settings private[TcpExt] (config: Config) { - import config._ + // FIXME: get away with subclassess + class Settings private[TcpExt] (_config: Config) extends SelectionHandlerSettings(_config) { + import _config._ val NrOfSelectors = getInt("nr-of-selectors") - val MaxChannels = getString("max-channels") match { - case "unlimited" ⇒ -1 - case _ ⇒ getInt("max-channels") - } - val SelectTimeout = getString("select-timeout") match { - case "infinite" ⇒ Duration.Inf - case x ⇒ Duration(x) - } - val SelectorAssociationRetries = getInt("selector-association-retries") + val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getIntBytes("direct-buffer-size") val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") @@ -203,10 +196,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { case "unlimited" ⇒ Int.MaxValue case x ⇒ getIntBytes("received-message-size-limit") } - val SelectorDispatcher = getString("selector-dispatcher") - val WorkerDispatcher = getString("worker-dispatcher") val ManagementDispatcher = getString("management-dispatcher") - val TraceLogging = getBoolean("trace-logging") require(NrOfSelectors > 0, "nr-of-selectors must be > 0") require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'") diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index a586881c67..60690e4b23 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -15,7 +15,7 @@ import scala.concurrent.duration._ import akka.actor._ import akka.util.ByteString import Tcp._ -import TcpSelector._ +import akka.io.SelectionHandler._ /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. diff --git a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala index 1036509b4e..0c25b7f46b 100644 --- a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -8,6 +8,7 @@ import java.nio.channels.SocketChannel import scala.collection.immutable import akka.actor.ActorRef import Tcp.SocketOption +import akka.io.SelectionHandler.RegisterChannel /** * An actor handling the connection state machine for an incoming, already connected @@ -22,6 +23,7 @@ private[io] class TcpIncomingConnection(_channel: SocketChannel, context.watch(handler) // sign death pact completeConnect(handler, options) + context.parent ! RegisterChannel(channel, 0) def receive = PartialFunction.empty } diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index c110b65a39..2b23787fe9 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -5,12 +5,12 @@ package akka.io import java.net.InetSocketAddress -import java.nio.channels.ServerSocketChannel +import java.nio.channels.{ SelectionKey, ServerSocketChannel } import scala.annotation.tailrec import scala.collection.immutable import scala.util.control.NonFatal -import akka.actor.{ ActorLogging, ActorRef, Actor } -import TcpSelector._ +import akka.actor.{ Props, ActorLogging, ActorRef, Actor } +import akka.io.SelectionHandler._ import Tcp._ private[io] class TcpListener(selectorRouter: ActorRef, @@ -18,10 +18,11 @@ private[io] class TcpListener(selectorRouter: ActorRef, endpoint: InetSocketAddress, backlog: Int, bindCommander: ActorRef, - settings: TcpExt#Settings, + tcp: TcpExt, options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { def selector: ActorRef = context.parent + import tcp.Settings._ context.watch(handler) // sign death pact val channel = { @@ -32,25 +33,25 @@ private[io] class TcpListener(selectorRouter: ActorRef, socket.bind(endpoint, backlog) // will blow up the actor constructor if the bind fails serverSocketChannel } - context.parent ! RegisterServerSocketChannel(channel) + context.parent ! RegisterChannel(channel, SelectionKey.OP_ACCEPT) log.debug("Successfully bound to {}", endpoint) def receive: Receive = { - case Bound ⇒ + case KickStartDone ⇒ bindCommander ! Bound context.become(bound) } def bound: Receive = { case ChannelAcceptable ⇒ - acceptAllPending(settings.BatchAcceptLimit) + acceptAllPending(BatchAcceptLimit) - case CommandFailed(RegisterIncomingConnection(socketChannel, _, _)) ⇒ - log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection") - try socketChannel.close() - catch { - case NonFatal(e) ⇒ log.error(e, "Error closing channel") - } + // case CommandFailed(RegisterIncomingConnection(socketChannel, _, _)) ⇒ + // log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection") + // try socketChannel.close() + // catch { + // case NonFatal(e) ⇒ log.error(e, "Error closing channel") + // } case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) @@ -70,7 +71,8 @@ private[io] class TcpListener(selectorRouter: ActorRef, if (socketChannel != null) { log.debug("New connection accepted") socketChannel.configureBlocking(false) - selectorRouter ! RegisterIncomingConnection(socketChannel, handler, options) + //selectorRouter ! RegisterIncomingConnection(socketChannel, handler, options) + selectorRouter ! KickStartCommand(Props(new TcpIncomingConnection(socketChannel, tcp, handler, options))) acceptAllPending(limit - 1) } } else context.parent ! AcceptInterest diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index 0ef1ca3e52..94e07e015f 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -7,6 +7,7 @@ package akka.io import akka.actor.{ ActorLogging, Actor, Props } import akka.routing.RandomRouter import Tcp._ +import akka.io.SelectionHandler.KickStartCommand /** * TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections. @@ -46,10 +47,17 @@ import Tcp._ private[io] class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { val selectorPool = context.actorOf( - props = Props(new TcpSelector(self, tcp)).withRouter(RandomRouter(tcp.Settings.NrOfSelectors)), + props = Props(new SelectionHandler(self, tcp.Settings)).withRouter(RandomRouter(tcp.Settings.NrOfSelectors)), name = "selectors") def receive = { - case x @ (_: Connect | _: Bind) ⇒ selectorPool forward x + //case x @ (_: Connect | _: Bind) ⇒ selectorPool forward x + case Connect(remoteAddress, localAddress, options) ⇒ + val commander = sender + selectorPool ! KickStartCommand(Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options))) + + case Bind(handler, endpoint, backlog, options) ⇒ + val commander = sender + selectorPool ! KickStartCommand(Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options))) } } diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 040d866e5b..1a04f6bc5c 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -6,10 +6,10 @@ package akka.io import java.net.InetSocketAddress import java.io.IOException -import java.nio.channels.SocketChannel +import java.nio.channels.{ SelectionKey, SocketChannel } import scala.collection.immutable import akka.actor.ActorRef -import TcpSelector._ +import akka.io.SelectionHandler._ import Tcp._ /** @@ -32,7 +32,7 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, if (channel.connect(remoteAddress)) completeConnect(commander, options) else { - selector ! RegisterOutgoingConnection(channel) + selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) context.become(connecting(commander, options)) } diff --git a/akka-actor/src/main/scala/akka/io/TcpSelector.scala b/akka-actor/src/main/scala/akka/io/TcpSelector.scala index 4e0dd6f9a8..f9ec086b76 100644 --- a/akka-actor/src/main/scala/akka/io/TcpSelector.scala +++ b/akka-actor/src/main/scala/akka/io/TcpSelector.scala @@ -95,7 +95,7 @@ private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with withCapacityProtection(cmd, retriesLeft) { import cmd._ val commander = sender - spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp.Settings, options)) + spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp, options)) } def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index d699f7f18e..0c58e78112 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -57,7 +57,7 @@ private[io] class UdpFFManager(udpFF: UdpFFExt) extends Actor { def receive = { case Bind(handler, endpoint, options) ⇒ val commander = sender - selectorPool forward KickStartCommand(Props( + selectorPool ! KickStartCommand(Props( new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options))) case SimpleSender ⇒ anonymousSender forward SimpleSender } diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala b/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala deleted file mode 100644 index ec151ade47..0000000000 --- a/akka-actor/src/main/scala/akka/io/UdpFFSelector.scala +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Copyright (C) 2009-2013 Typesafe Inc. - */ -package akka.io - -import akka.io.UdpFF._ -import akka.actor._ -import java.lang.Runnable -import java.nio.channels.{ DatagramChannel, SelectionKey } -import java.nio.channels.SelectionKey._ -import java.nio.channels.spi.SelectorProvider -import scala.collection.immutable -import scala.concurrent.duration._ -import scala.util.control.NonFatal - -private[io] object UdpFFSelector { - case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } - case class RegisterDatagramChannel(channel: DatagramChannel, initialOps: Int) extends Command - - case object ChannelReadable - case object ChannelWritable - case object ReadInterest - case object WriteInterest -} - -private[io] class UdpFFSelector(manager: ActorRef, udp: UdpFFExt) extends Actor with ActorLogging { - - import UdpFFSelector._ - import udp.settings._ - - @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] - val sequenceNumber = Iterator from 0 - val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) - val selector = SelectorProvider.provider.openSelector - val OP_READ_AND_WRITE = OP_READ + OP_WRITE // compile-time constant - - def receive: Receive = { - case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) - case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) - case StopReading ⇒ execute(disableInterest(OP_READ, sender)) - - case cmd: Bind ⇒ - handleBind(cmd, SelectorAssociationRetries) - - case RegisterDatagramChannel(channel, initialOps) ⇒ - execute(registerDatagramChannel(channel, sender, initialOps)) - - case Retry(command, 0) ⇒ - log.warning("Command '{}' failed since all selectors are at capacity", command) - sender ! CommandFailed(command) - - case Retry(cmd: Bind, retriesLeft) ⇒ - handleBind(cmd, retriesLeft) - - case Terminated(child) ⇒ - execute(unregister(child)) - } - - override def postStop() { - try { - try { - val iterator = selector.keys.iterator - while (iterator.hasNext) iterator.next().channel.close() - } finally selector.close() - } catch { - case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") - } - } - - // we can never recover from failures of a connection or listener child - override def supervisorStrategy = SupervisorStrategy.stoppingStrategy - - def handleBind(cmd: Bind, retriesLeft: Int): Unit = - withCapacityProtection(cmd, retriesLeft) { - import cmd._ - val commander = sender - spawnChild(() ⇒ new UdpFFListener(context.parent, handler, endpoint, commander, udp, options)) - } - - def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { - log.debug("Executing {}", cmd) - if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { - body - } else { - log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) - context.parent forward Retry(cmd, retriesLeft - 1) - } - } - - def spawnChild(creator: () ⇒ Actor) = - context.watch { - context.actorOf( - props = Props(creator, dispatcher = WorkerDispatcher), - name = sequenceNumber.next().toString) - } - - //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// - - def execute(task: Task): Unit = { - selectorManagementDispatcher.execute(task) - selector.wakeup() - } - - def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = - childrenKeys = childrenKeys.updated(child.path.name, key) - - def registerDatagramChannel(channel: DatagramChannel, connection: ActorRef, initialOps: Int) = - new Task { - def tryRun() { - val key = channel.register(selector, initialOps, connection) - updateKeyMap(connection, key) - } - } - - // TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than - // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap - // rather than an unsynchronized one, but since switching interest ops is so frequent - // the change might be beneficial, provided the underlying implementation really is thread-safe - // and behaves consistently on all platforms. - def enableInterest(op: Int, connection: ActorRef) = - new Task { - def tryRun() { - val key = childrenKeys(connection.path.name) - key.interestOps(key.interestOps | op) - } - } - - def disableInterest(op: Int, connection: ActorRef) = - new Task { - def tryRun() { - val key = childrenKeys(connection.path.name) - key.interestOps(key.interestOps & ~op) - } - } - - def unregister(child: ActorRef) = - new Task { - def tryRun() { - childrenKeys = childrenKeys - child.path.name - } - } - - val select = new Task { - val doSelect: () ⇒ Int = - SelectTimeout match { - case Duration.Zero ⇒ () ⇒ selector.selectNow() - case Duration.Inf ⇒ () ⇒ selector.select() - case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) - } - def tryRun() { - if (doSelect() > 0) { - val keys = selector.selectedKeys - val iterator = keys.iterator() - while (iterator.hasNext) { - val key = iterator.next - if (key.isValid) { - key.interestOps(0) // prevent immediate reselection by always clearing - val connection = key.attachment.asInstanceOf[ActorRef] - key.readyOps match { - case OP_READ ⇒ connection ! ChannelReadable - case OP_WRITE ⇒ connection ! ChannelWritable - case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable - case x ⇒ log.warning("Invalid readyOps: {}", x) - } - } else log.warning("Invalid selection key: {}", key) - } - keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected - } - selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks - } - } - - selectorManagementDispatcher.execute(select) // start selection "loop" - - abstract class Task extends Runnable { - def tryRun() - def run() { - try tryRun() - catch { - case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown - case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) - } - } - } -} - From 58ab585844ddc7f282a6e1ccdcafd75bdb3b937b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Mon, 4 Feb 2013 11:21:04 +0100 Subject: [PATCH 52/66] Various fixes to tests --- .../scala/akka/io/TcpConnectionSpec.scala | 9 +- .../test/scala/akka/io/TcpListenerSpec.scala | 30 ++--- .../main/scala/akka/io/SelectionHandler.scala | 105 ++++-------------- .../src/main/scala/akka/io/TcpListener.scala | 3 +- .../src/main/scala/akka/io/TcpManager.scala | 12 +- .../scala/akka/io/TcpOutgoingConnection.scala | 12 +- .../src/main/scala/akka/io/UdpFFManager.scala | 9 +- 7 files changed, 61 insertions(+), 119 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index c5d67a50be..08c7b745f2 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -15,13 +15,13 @@ import scala.concurrent.duration._ import scala.util.control.NonFatal import org.scalatest.matchers._ import Tcp._ -import TcpSelector._ +import akka.io.SelectionHandler._ import TestUtils._ import akka.actor.{ ActorRef, PoisonPill, Terminated } import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe } import akka.util.ByteString import akka.actor.DeathPactException -import akka.actor.DeathPactException +import java.nio.channels.SelectionKey._ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { val serverAddress = temporaryServerAddress() @@ -241,7 +241,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val buffer = ByteBuffer.allocate(1) val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException] - thrown.getMessage must be("Connection reset by peer") + // FIXME: On windows this message is localized + //thrown.getMessage must be("Connection reset by peer") } "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ @@ -519,7 +520,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val connectionActor = connectionActorCons(selector.ref, userHandler.ref) val clientSideChannel = connectionActor.underlyingActor.channel - selector.expectMsg(RegisterOutgoingConnection(clientSideChannel)) + selector.expectMsg(RegisterChannel(clientSideChannel, OP_CONNECT)) body { UnacceptedSetup( diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index 0ec8f792bd..aa66dc160a 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -8,9 +8,10 @@ import java.net.Socket import scala.concurrent.duration._ import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props } import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec } -import TcpSelector._ import Tcp._ import akka.testkit.EventFilter +import akka.io.SelectionHandler._ +import java.nio.channels.SelectionKey._ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { @@ -19,7 +20,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "register its ServerSocketChannel with its selector" in new TestSetup "let the Bind commander know when binding is completed" in new TestSetup { - listener ! Bound + listener ! KickStartDone bindCommander.expectMsg(Bound) } @@ -34,13 +35,14 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { listener ! ChannelAcceptable parent.expectMsg(AcceptInterest) - selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } - selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + // FIXME: ugly stuff here + selectorRouter.expectMsgType[KickStartCommand] + selectorRouter.expectMsgType[KickStartCommand] selectorRouter.expectNoMsg(100.millis) // and pick up the last remaining connection on the next ChannelAcceptable listener ! ChannelAcceptable - selectorRouter.expectMsgPF() { case RegisterIncomingConnection(_, `handlerRef`, Nil) ⇒ /* ok */ } + selectorRouter.expectMsgType[KickStartCommand] } "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { @@ -59,13 +61,15 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { attemptConnectionToEndpoint() listener ! ChannelAcceptable - val channel = selectorRouter.expectMsgType[RegisterIncomingConnection].channel - channel.isOpen must be(true) + val props = selectorRouter.expectMsgType[KickStartCommand].childProps + // FIXME: need to instantiate propss + //selectorRouter.expectMsgType[RegisterChannel].channel.isOpen must be(true) - EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept { - listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) - awaitCond(!channel.isOpen) - } + // FIXME: fix this + // EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept { + // //listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) + // awaitCond(!channel.isOpen) + // } } } @@ -80,10 +84,10 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { val endpoint = TestUtils.temporaryServerAddress() private val parentRef = TestActorRef(new ListenerParent) - parent.expectMsgType[RegisterServerSocketChannel] + parent.expectMsgType[RegisterChannel] def bindListener() { - listener ! Bound + listener ! KickStartDone bindCommander.expectMsg(Bound) } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 1389ea8c1f..64d6b389f2 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -41,10 +41,12 @@ abstract class SelectionHandlerSettings(config: Config) { } private[io] object SelectionHandler { + //FIXME: temporary - case class KickStartCommand(childProps: Props) + case class KickStartCommand(apiCommand: Any, commander: ActorRef, childProps: Props) // FIXME: all actors should listen to this case object KickStartDone + case class KickStartFailed(apiCommand: Any, commander: ActorRef) case class RegisterChannel(channel: SelectableChannel, initialOps: Int) case class Retry(command: KickStartCommand, retriesLeft: Int) { require(retriesLeft >= 0) } @@ -75,42 +77,19 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler //case StopReading ⇒ execute(disableInterest(OP_READ, sender)) - // case cmd: RegisterIncomingConnection ⇒ - // handleIncomingConnection(cmd, SelectorAssociationRetries) - // - // case cmd: Connect ⇒ - // handleConnect(cmd, SelectorAssociationRetries) - // - // case cmd: Bind ⇒ - // handleBind(cmd, SelectorAssociationRetries) - case cmd: KickStartCommand ⇒ - kickStart(cmd, SelectorAssociationRetries) - - // case RegisterOutgoingConnection(channel) ⇒ - // execute(registerOutgoingConnection(channel, sender)) - // - // case RegisterServerSocketChannel(channel) ⇒ - // execute(registerListener(channel, sender)) + // FIXME: factor out to common + withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) ! KickStartDone } case RegisterChannel(channel, initialOps) ⇒ execute(registerChannel(channel, sender, initialOps)) - // case Retry(command, 0) ⇒ - // log.warning("Command '{}' failed since all selectors are at capacity", command) - // sender ! CommandFailed(command) + case Retry(cmd, 0) ⇒ + // FIXME: extractors + manager ! KickStartFailed(cmd.apiCommand, cmd.commander) case Retry(cmd, retriesLeft) ⇒ - kickStart(cmd, retriesLeft) - - // case Retry(cmd: RegisterIncomingConnection, retriesLeft) ⇒ - // handleIncomingConnection(cmd, retriesLeft) - // - // case Retry(cmd: Connect, retriesLeft) ⇒ - // handleConnect(cmd, retriesLeft) - // - // case Retry(cmd: Bind, retriesLeft) ⇒ - // handleBind(cmd, retriesLeft) + withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) ! KickStartDone } case Terminated(child) ⇒ execute(unregister(child)) @@ -118,43 +97,23 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler override def postStop() { try { - try { - val iterator = selector.keys.iterator - while (iterator.hasNext) iterator.next().channel.close() - } finally selector.close() + val iterator = selector.keys.iterator + while (iterator.hasNext) { + val key = iterator.next() + try key.channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing channel") + } + } + selector.close() } catch { - case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") + case NonFatal(e) ⇒ log.error(e, "Error closing selector") } } // we can never recover from failures of a connection or listener child override def supervisorStrategy = SupervisorStrategy.stoppingStrategy - // def handleIncomingConnection(cmd: RegisterIncomingConnection, retriesLeft: Int): Unit = - // withCapacityProtection(cmd, retriesLeft) { - // import cmd._ - // val connection = spawnChild(() ⇒ new TcpIncomingConnection(channel, tcp, handler, options)) - // execute(registerIncomingConnection(channel, connection)) - // } - // - // def handleConnect(cmd: Connect, retriesLeft: Int): Unit = - // withCapacityProtection(cmd, retriesLeft) { - // import cmd._ - // val commander = sender - // spawnChild(() ⇒ new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) - // } - // - // def handleBind(cmd: Bind, retriesLeft: Int): Unit = - // withCapacityProtection(cmd, retriesLeft) { - // import cmd._ - // val commander = sender - // spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp.Settings, options)) - // } - - def kickStart(cmd: KickStartCommand, retriesLeft: Int): Unit = withCapacityProtection(cmd, retriesLeft) { - spawnChild(cmd.childProps) ! KickStartDone - } - def withCapacityProtection(cmd: KickStartCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { log.debug("Executing {}", cmd) if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { @@ -182,32 +141,6 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = childrenKeys = childrenKeys.updated(child.path.name, key) - // def registerOutgoingConnection(channel: SocketChannel, connection: ActorRef) = - // new Task { - // def tryRun() { - // val key = channel.register(selector, OP_CONNECT, connection) - // updateKeyMap(connection, key) - // } - // } - // - // def registerListener(channel: ServerSocketChannel, listener: ActorRef) = - // new Task { - // def tryRun() { - // val key = channel.register(selector, OP_ACCEPT, listener) - // updateKeyMap(listener, key) - // listener ! Bound - // } - // } - // - // def registerIncomingConnection(channel: SocketChannel, connection: ActorRef) = - // new Task { - // def tryRun() { - // // we only enable reading after the user-level connection handler has registered - // val key = channel.register(selector, 0, connection) - // updateKeyMap(connection, key) - // } - // } - def registerChannel(channel: SelectableChannel, channelActor: ActorRef, initialOps: Int): Task = new Task { def tryRun() { diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index 2b23787fe9..f3cc55d4a5 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -72,7 +72,8 @@ private[io] class TcpListener(selectorRouter: ActorRef, log.debug("New connection accepted") socketChannel.configureBlocking(false) //selectorRouter ! RegisterIncomingConnection(socketChannel, handler, options) - selectorRouter ! KickStartCommand(Props(new TcpIncomingConnection(socketChannel, tcp, handler, options))) + // FIXME null is not nice. There is no explicit API command here + selectorRouter ! KickStartCommand(null, context.system.deadLetters, Props(new TcpIncomingConnection(socketChannel, tcp, handler, options))) acceptAllPending(limit - 1) } } else context.parent ! AcceptInterest diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index 94e07e015f..f8dc6373ad 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -7,7 +7,7 @@ package akka.io import akka.actor.{ ActorLogging, Actor, Props } import akka.routing.RandomRouter import Tcp._ -import akka.io.SelectionHandler.KickStartCommand +import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } /** * TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections. @@ -52,12 +52,14 @@ private[io] class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { def receive = { //case x @ (_: Connect | _: Bind) ⇒ selectorPool forward x - case Connect(remoteAddress, localAddress, options) ⇒ + case c @ Connect(remoteAddress, localAddress, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options))) + selectorPool ! KickStartCommand(c, commander, Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options))) - case Bind(handler, endpoint, backlog, options) ⇒ + case b @ Bind(handler, endpoint, backlog, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options))) + selectorPool ! KickStartCommand(b, commander, Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options))) + + case KickStartFailed(cmd: Command, commander) ⇒ commander ! CommandFailed(cmd) } } diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 1a04f6bc5c..2c0c766543 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -29,12 +29,12 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, options.foreach(_.beforeConnect(channel.socket)) log.debug("Attempting connection to {}", remoteAddress) - if (channel.connect(remoteAddress)) - completeConnect(commander, options) - else { - selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) - context.become(connecting(commander, options)) - } + // if (channel.connect(remoteAddress)) + // completeConnect(commander, options) + // else { + selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) + context.become(connecting(commander, options)) + // } def receive: Receive = PartialFunction.empty diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 0c58e78112..1a9311a43b 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -6,7 +6,7 @@ package akka.io import akka.actor.{ ActorRef, Props, Actor } import akka.io.UdpFF._ import akka.routing.RandomRouter -import akka.io.SelectionHandler.KickStartCommand +import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } /** * UdpFFManager is a facade for simple fire-and-forget style UDP operations @@ -55,11 +55,12 @@ private[io] class UdpFFManager(udpFF: UdpFFExt) extends Actor { name = "simplesend") def receive = { - case Bind(handler, endpoint, options) ⇒ + case b @ Bind(handler, endpoint, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(Props( + selectorPool ! KickStartCommand(b, commander, Props( new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options))) - case SimpleSender ⇒ anonymousSender forward SimpleSender + case SimpleSender ⇒ anonymousSender forward SimpleSender + case KickStartFailed(cmd: Command, commander) ⇒ commander ! CommandFailed(cmd) } } From 946fb0eec418ebb7a9399d357fe7969d7adf4c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Mon, 4 Feb 2013 16:24:34 +0100 Subject: [PATCH 53/66] Removed WithByteBuffer and friends --- .../scala/akka/io/DirectByteBufferPool.scala | 10 - .../main/scala/akka/io/TcpConnection.scala | 13 +- .../scala/akka/io/TcpIncomingConnection.scala | 2 +- .../scala/akka/io/TcpOutgoingConnection.scala | 12 +- .../src/main/scala/akka/io/TcpSelector.scala | 242 ------------------ akka-actor/src/main/scala/akka/io/UdpFF.scala | 10 - .../main/scala/akka/io/UdpFFListener.scala | 7 +- .../src/main/scala/akka/io/UdpFFSender.scala | 2 +- .../main/scala/akka/io/WithUdpFFSend.scala | 6 +- 9 files changed, 22 insertions(+), 282 deletions(-) delete mode 100644 akka-actor/src/main/scala/akka/io/TcpSelector.scala diff --git a/akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala b/akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala index 6d9699122f..cf2f573fd3 100644 --- a/akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala +++ b/akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala @@ -8,16 +8,6 @@ import java.util.concurrent.atomic.AtomicBoolean import java.nio.ByteBuffer import annotation.tailrec -trait WithBufferPool { - def tcp: TcpExt - - def acquireBuffer(): ByteBuffer = - tcp.bufferPool.acquire() - - def releaseBuffer(buffer: ByteBuffer): Unit = - tcp.bufferPool.release(buffer) -} - trait BufferPool { def acquire(): ByteBuffer def release(buf: ByteBuffer) diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index 60690e4b23..b2dea0571c 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -21,8 +21,9 @@ import akka.io.SelectionHandler._ * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ private[io] abstract class TcpConnection(val channel: SocketChannel, - val tcp: TcpExt) extends Actor with ActorLogging with WithBufferPool { + val tcp: TcpExt) extends Actor with ActorLogging { import tcp.Settings._ + import tcp.bufferPool import TcpConnection._ var pendingWrite: PendingWrite = null @@ -137,7 +138,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, } } else MoreDataWaiting(receivedData) - val buffer = acquireBuffer() + val buffer = bufferPool.acquire() try innerRead(buffer, ByteString.empty, ReceivedMessageSizeLimit) match { case NoData ⇒ if (TraceLogging) log.debug("Read nothing.") @@ -157,7 +158,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, doCloseConnection(handler, closeCommander, closeReason) } catch { case e: IOException ⇒ handleError(handler, e) - } finally releaseBuffer(buffer) + } finally bufferPool.release(buffer) } final def doWrite(handler: ActorRef): Unit = { @@ -179,7 +180,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, val buffer = pendingWrite.buffer pendingWrite = null - releaseBuffer(buffer) + bufferPool.release(buffer) } } @@ -256,7 +257,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, abort() if (writePending) - releaseBuffer(pendingWrite.buffer) + bufferPool.release(pendingWrite.buffer) if (closedMessage != null) { val interestedInClose = @@ -288,7 +289,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, def wantsAck = ack != NoAck } def createWrite(write: Write): PendingWrite = { - val buffer = acquireBuffer() + val buffer = bufferPool.acquire() val copied = write.data.copyToBuffer(buffer) buffer.flip() diff --git a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala index 0c25b7f46b..5da609c617 100644 --- a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -7,7 +7,7 @@ package akka.io import java.nio.channels.SocketChannel import scala.collection.immutable import akka.actor.ActorRef -import Tcp.SocketOption +import akka.io.Tcp.SocketOption import akka.io.SelectionHandler.RegisterChannel /** diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 2c0c766543..1a04f6bc5c 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -29,12 +29,12 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, options.foreach(_.beforeConnect(channel.socket)) log.debug("Attempting connection to {}", remoteAddress) - // if (channel.connect(remoteAddress)) - // completeConnect(commander, options) - // else { - selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) - context.become(connecting(commander, options)) - // } + if (channel.connect(remoteAddress)) + completeConnect(commander, options) + else { + selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) + context.become(connecting(commander, options)) + } def receive: Receive = PartialFunction.empty diff --git a/akka-actor/src/main/scala/akka/io/TcpSelector.scala b/akka-actor/src/main/scala/akka/io/TcpSelector.scala deleted file mode 100644 index f9ec086b76..0000000000 --- a/akka-actor/src/main/scala/akka/io/TcpSelector.scala +++ /dev/null @@ -1,242 +0,0 @@ -/** - * Copyright (C) 2009-2012 Typesafe Inc. - */ - -package akka.io - -import java.lang.Runnable -import java.nio.channels.spi.SelectorProvider -import java.nio.channels.{ ServerSocketChannel, SelectionKey, SocketChannel } -import java.nio.channels.SelectionKey._ -import scala.util.control.NonFatal -import scala.collection.immutable -import scala.concurrent.duration._ -import akka.actor._ -import Tcp._ - -private[io] class TcpSelector(manager: ActorRef, tcp: TcpExt) extends Actor with ActorLogging { - import TcpSelector._ - import tcp.Settings._ - - @volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey] - val sequenceNumber = Iterator.from(0) - val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher) - val selector = SelectorProvider.provider.openSelector - val OP_READ_AND_WRITE = OP_READ + OP_WRITE // compile-time constant - - def receive: Receive = { - case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) - case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) - case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) - - case StopReading ⇒ execute(disableInterest(OP_READ, sender)) - - case cmd: RegisterIncomingConnection ⇒ - handleIncomingConnection(cmd, SelectorAssociationRetries) - - case cmd: Connect ⇒ - handleConnect(cmd, SelectorAssociationRetries) - - case cmd: Bind ⇒ - handleBind(cmd, SelectorAssociationRetries) - - case RegisterOutgoingConnection(channel) ⇒ - execute(registerOutgoingConnection(channel, sender)) - - case RegisterServerSocketChannel(channel) ⇒ - execute(registerListener(channel, sender)) - - case Retry(command, 0) ⇒ - log.warning("Command '{}' failed since all selectors are at capacity", command) - sender ! CommandFailed(command) - - case Retry(cmd: RegisterIncomingConnection, retriesLeft) ⇒ - handleIncomingConnection(cmd, retriesLeft) - - case Retry(cmd: Connect, retriesLeft) ⇒ - handleConnect(cmd, retriesLeft) - - case Retry(cmd: Bind, retriesLeft) ⇒ - handleBind(cmd, retriesLeft) - - case Terminated(child) ⇒ - execute(unregister(child)) - } - - override def postStop() { - try { - try { - val iterator = selector.keys.iterator - while (iterator.hasNext) iterator.next().channel.close() - } finally selector.close() - } catch { - case NonFatal(e) ⇒ log.error(e, "Error closing selector or key") - } - } - - // we can never recover from failures of a connection or listener child - override def supervisorStrategy = SupervisorStrategy.stoppingStrategy - - def handleIncomingConnection(cmd: RegisterIncomingConnection, retriesLeft: Int): Unit = - withCapacityProtection(cmd, retriesLeft) { - import cmd._ - val connection = spawnChild(() ⇒ new TcpIncomingConnection(channel, tcp, handler, options)) - execute(registerIncomingConnection(channel, connection)) - } - - def handleConnect(cmd: Connect, retriesLeft: Int): Unit = - withCapacityProtection(cmd, retriesLeft) { - import cmd._ - val commander = sender - spawnChild(() ⇒ new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) - } - - def handleBind(cmd: Bind, retriesLeft: Int): Unit = - withCapacityProtection(cmd, retriesLeft) { - import cmd._ - val commander = sender - spawnChild(() ⇒ new TcpListener(context.parent, handler, endpoint, backlog, commander, tcp, options)) - } - - def withCapacityProtection(cmd: Command, retriesLeft: Int)(body: ⇒ Unit): Unit = { - log.debug("Executing {}", cmd) - if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { - body - } else { - log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) - context.parent forward Retry(cmd, retriesLeft - 1) - } - } - - def spawnChild(creator: () ⇒ Actor) = - context.watch { - context.actorOf( - props = Props(creator, dispatcher = WorkerDispatcher), - name = sequenceNumber.next().toString) - } - - //////////////// Management Tasks scheduled via the selectorManagementDispatcher ///////////// - - def execute(task: Task): Unit = { - selectorManagementDispatcher.execute(task) - selector.wakeup() - } - - def updateKeyMap(child: ActorRef, key: SelectionKey): Unit = - childrenKeys = childrenKeys.updated(child.path.name, key) - - def registerOutgoingConnection(channel: SocketChannel, connection: ActorRef) = - new Task { - def tryRun() { - val key = channel.register(selector, OP_CONNECT, connection) - updateKeyMap(connection, key) - } - } - - def registerListener(channel: ServerSocketChannel, listener: ActorRef) = - new Task { - def tryRun() { - val key = channel.register(selector, OP_ACCEPT, listener) - updateKeyMap(listener, key) - listener ! Bound - } - } - - def registerIncomingConnection(channel: SocketChannel, connection: ActorRef) = - new Task { - def tryRun() { - // we only enable reading after the user-level connection handler has registered - val key = channel.register(selector, 0, connection) - updateKeyMap(connection, key) - } - } - - // TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than - // on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap - // rather than an unsynchronized one, but since switching interest ops is so frequent - // the change might be beneficial, provided the underlying implementation really is thread-safe - // and behaves consistently on all platforms. - def enableInterest(op: Int, connection: ActorRef) = - new Task { - def tryRun() { - val key = childrenKeys(connection.path.name) - key.interestOps(key.interestOps | op) - } - } - - def disableInterest(op: Int, connection: ActorRef) = - new Task { - def tryRun() { - val key = childrenKeys(connection.path.name) - key.interestOps(key.interestOps & ~op) - } - } - - def unregister(child: ActorRef) = - new Task { - def tryRun() { - childrenKeys = childrenKeys - child.path.name - } - } - - val select = new Task { - val doSelect: () ⇒ Int = - SelectTimeout match { - case Duration.Zero ⇒ () ⇒ selector.selectNow() - case Duration.Inf ⇒ () ⇒ selector.select() - case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis) - } - def tryRun() { - if (doSelect() > 0) { - val keys = selector.selectedKeys - val iterator = keys.iterator() - while (iterator.hasNext) { - val key = iterator.next - if (key.isValid) { - key.interestOps(0) // prevent immediate reselection by always clearing - val connection = key.attachment.asInstanceOf[ActorRef] - key.readyOps match { - case OP_READ ⇒ connection ! ChannelReadable - case OP_WRITE ⇒ connection ! ChannelWritable - case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable - case x if (x & OP_ACCEPT) > 0 ⇒ connection ! ChannelAcceptable - case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable - case x ⇒ log.warning("Invalid readyOps: {}", x) - } - } else log.warning("Invalid selection key: {}", key) - } - keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected - } - selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks - } - } - - selectorManagementDispatcher.execute(select) // start selection "loop" - - abstract class Task extends Runnable { - def tryRun() - def run() { - try tryRun() - catch { - case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown - case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) - } - } - } -} - -private[io] object TcpSelector { - case class RegisterOutgoingConnection(channel: SocketChannel) - case class RegisterServerSocketChannel(channel: ServerSocketChannel) - case class RegisterIncomingConnection(channel: SocketChannel, handler: ActorRef, - options: immutable.Traversable[SocketOption]) extends Tcp.Command - case class Retry(command: Command, retriesLeft: Int) { require(retriesLeft >= 0) } - - case object ChannelConnectable - case object ChannelAcceptable - case object ChannelReadable - case object ChannelWritable - case object AcceptInterest - case object ReadInterest - case object WriteInterest -} diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index a82c01a56a..6d9274e191 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -149,14 +149,4 @@ class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { } val bufferPool: BufferPool = new DirectByteBufferPool(settings.DirectBufferSize, settings.MaxDirectBufferPoolSize) -} - -trait WithUdpFFBufferPool { - def udpFF: UdpFFExt - - def acquireBuffer(): ByteBuffer = - udpFF.bufferPool.acquire() - - def releaseBuffer(buffer: ByteBuffer): Unit = - udpFF.bufferPool.release(buffer) } \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index f356163303..a481559e07 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -19,8 +19,9 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, bindCommander: ActorRef, val udpFF: UdpFFExt, options: immutable.Traversable[SocketOption]) - extends Actor with ActorLogging with WithUdpFFBufferPool with WithUdpFFSend { + extends Actor with ActorLogging with WithUdpFFSend { import udpFF.settings._ + import udpFF.bufferPool def selector: ActorRef = context.parent @@ -60,7 +61,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, } def doReceive(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = { - val buffer = acquireBuffer() + val buffer = bufferPool.acquire() try { buffer.clear() buffer.limit(DirectBufferSize) @@ -73,7 +74,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, } selector ! ReadInterest - } finally releaseBuffer(buffer) + } finally bufferPool.release(buffer) } override def postStop() { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala index c1e23c4213..4e6245c160 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -12,7 +12,7 @@ import akka.io.SelectionHandler.RegisterChannel * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ private[io] class UdpFFSender(val udpFF: UdpFFExt, val selector: ActorRef) - extends Actor with ActorLogging with WithUdpFFBufferPool with WithUdpFFSend { + extends Actor with ActorLogging with WithUdpFFSend { val channel = { val datagramChannel = DatagramChannel.open diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala index 07ad4132e7..da837c35fd 100644 --- a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -9,7 +9,7 @@ import akka.io.SelectionHandler._ import java.nio.channels.DatagramChannel trait WithUdpFFSend { - me: Actor with ActorLogging with WithUdpFFBufferPool ⇒ + me: Actor with ActorLogging ⇒ var pendingSend: (Send, ActorRef) = null def writePending = pendingSend ne null @@ -41,7 +41,7 @@ trait WithUdpFFSend { final def doSend(): Unit = { - val buffer = acquireBuffer() + val buffer = udpFF.bufferPool.acquire() try { val (send, commander) = pendingSend buffer.clear() @@ -55,7 +55,7 @@ trait WithUdpFFSend { else if (send.wantsAck) commander ! send.ack } finally { - releaseBuffer(buffer) + udpFF.bufferPool.release(buffer) pendingSend = null } From 1ec065b0cd33ab147bfaf7417c27de5cd997cd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Feb 2013 11:48:47 +0100 Subject: [PATCH 54/66] Factored out common manager code and SocketOptions --- .../test/scala/akka/io/IntegrationSpec.scala | 5 +- .../akka/io/IntegrationSpecSupport.scala | 1 + .../scala/akka/io/TcpConnectionSpec.scala | 7 +- akka-actor/src/main/scala/akka/io/IO.scala | 23 +++++- akka-actor/src/main/scala/akka/io/Inet.scala | 82 +++++++++++++++++++ akka-actor/src/main/scala/akka/io/Tcp.scala | 72 +--------------- .../main/scala/akka/io/TcpConnection.scala | 3 +- .../scala/akka/io/TcpIncomingConnection.scala | 2 +- .../src/main/scala/akka/io/TcpListener.scala | 5 +- .../src/main/scala/akka/io/TcpManager.scala | 22 ++--- .../scala/akka/io/TcpOutgoingConnection.scala | 3 +- akka-actor/src/main/scala/akka/io/Udp.scala | 24 ++++++ .../src/main/scala/akka/io/UdpConn.scala | 21 +++++ akka-actor/src/main/scala/akka/io/UdpFF.scala | 70 +--------------- .../main/scala/akka/io/UdpFFListener.scala | 3 +- .../src/main/scala/akka/io/UdpFFManager.scala | 18 ++-- 16 files changed, 188 insertions(+), 173 deletions(-) create mode 100644 akka-actor/src/main/scala/akka/io/Inet.scala create mode 100644 akka-actor/src/main/scala/akka/io/Udp.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpConn.scala diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala index e4d53f5f9b..1e7a40eb90 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala @@ -6,6 +6,7 @@ package akka.io import akka.testkit.AkkaSpec import akka.util.ByteString +import akka.io.Inet import Tcp._ import TestUtils._ import akka.testkit.EventFilter @@ -64,8 +65,8 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationS expectReceivedData(clientHandler, 100000) - override def bindOptions = List(SO.SendBufferSize(1024)) - override def connectOptions = List(SO.ReceiveBufferSize(1024)) + override def bindOptions = List(Inet.SO.SendBufferSize(1024)) + override def connectOptions = List(Inet.SO.ReceiveBufferSize(1024)) } } diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala index 0feeb3809f..692815b96a 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala +++ b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala @@ -8,6 +8,7 @@ import scala.annotation.tailrec import akka.testkit.{ AkkaSpec, TestProbe } import akka.actor.ActorRef import scala.collection.immutable +import akka.io.Inet.SocketOption import Tcp._ import TestUtils._ diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index 08c7b745f2..a59f2ccad4 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -22,6 +22,7 @@ import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe } import akka.util.ByteString import akka.actor.DeathPactException import java.nio.channels.SelectionKey._ +import akka.io.Inet.SocketOption class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { val serverAddress = temporaryServerAddress() @@ -33,7 +34,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val userHandler = TestProbe() val selector = TestProbe() val connectionActor = - createConnectionActor(options = Vector(SO.ReuseAddress(true)))(selector.ref, userHandler.ref) + createConnectionActor(options = Vector(Inet.SO.ReuseAddress(true)))(selector.ref, userHandler.ref) val clientChannel = connectionActor.underlyingActor.channel clientChannel.socket.getReuseAddress must be(true) } @@ -65,7 +66,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } "bundle incoming Received messages as long as more data is available" in withEstablishedConnection( - clientSocketOptions = List(SO.ReceiveBufferSize(1000000)) // to make sure enough data gets through + clientSocketOptions = List(Inet.SO.ReceiveBufferSize(1000000)) // to make sure enough data gets through ) { setup ⇒ import setup._ @@ -567,7 +568,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") def createConnectionActor( serverAddress: InetSocketAddress = serverAddress, localAddress: Option[InetSocketAddress] = None, - options: immutable.Seq[Tcp.SocketOption] = Nil)( + options: immutable.Seq[SocketOption] = Nil)( _selector: ActorRef, commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { diff --git a/akka-actor/src/main/scala/akka/io/IO.scala b/akka-actor/src/main/scala/akka/io/IO.scala index e787f1295b..2b6cbd9f57 100644 --- a/akka-actor/src/main/scala/akka/io/IO.scala +++ b/akka-actor/src/main/scala/akka/io/IO.scala @@ -4,7 +4,9 @@ package akka.io -import akka.actor.{ ActorRef, ActorSystem, ExtensionKey } +import akka.actor._ +import akka.routing.RandomRouter +import akka.io.SelectionHandler.KickStartCommand object IO { @@ -14,4 +16,23 @@ object IO { def apply[T <: Extension](key: ExtensionKey[T])(implicit system: ActorSystem): ActorRef = key(system).manager + abstract class SelectorBasedManager(selectorSettings: SelectionHandlerSettings, nrOfSelectors: Int) extends Actor { + + val selectorPool = context.actorOf( + props = Props(new SelectionHandler(self, selectorSettings)).withRouter(RandomRouter(nrOfSelectors)), + name = "selectors") + + def createKickStart(pf: PartialFunction[Any, Props], cmd: Any): PartialFunction[Any, KickStartCommand] = { + pf.andThen { props ⇒ + val commander = sender + KickStartCommand(cmd, commander, props) + } + } + + def kickStartReceive(pf: PartialFunction[Any, Props]): Receive = { + //case KickStartFailed = + case cmd if pf.isDefinedAt(cmd) ⇒ selectorPool ! createKickStart(pf, cmd)(cmd) + } + } + } \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/io/Inet.scala b/akka-actor/src/main/scala/akka/io/Inet.scala new file mode 100644 index 0000000000..9e53507284 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/Inet.scala @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import java.net.{ DatagramSocket, Socket, ServerSocket } + +object Inet { + + /** + * SocketOption is a package of data (from the user) and associated + * behavior (how to apply that to a socket). + */ + trait SocketOption { + + def beforeDatagramBind(ds: DatagramSocket): Unit = () + + def beforeServerSocketBind(ss: ServerSocket): Unit = () + + /** + * Action to be taken for this option before calling connect() + */ + def beforeConnect(s: Socket): Unit = () + /** + * Action to be taken for this option after connect returned (i.e. on + * the slave socket for servers). + */ + def afterConnect(s: Socket): Unit = () + } + + object SO { + + /** + * [[akka.io.Tcp.SocketOption]] to set the SO_RCVBUF option + * + * For more information see [[java.net.Socket.setReceiveBufferSize]] + */ + case class ReceiveBufferSize(size: Int) extends SocketOption { + require(size > 0, "ReceiveBufferSize must be > 0") + override def beforeServerSocketBind(s: ServerSocket): Unit = s.setReceiveBufferSize(size) + override def beforeDatagramBind(s: DatagramSocket): Unit = s.setReceiveBufferSize(size) + override def beforeConnect(s: Socket): Unit = s.setReceiveBufferSize(size) + } + + // server socket options + + /** + * [[akka.io.Tcp.SocketOption]] to enable or disable SO_REUSEADDR + * + * For more information see [[java.net.Socket.setReuseAddress]] + */ + case class ReuseAddress(on: Boolean) extends SocketOption { + override def beforeServerSocketBind(s: ServerSocket): Unit = s.setReuseAddress(on) + override def beforeDatagramBind(s: DatagramSocket): Unit = s.setReuseAddress(on) + override def beforeConnect(s: Socket): Unit = s.setReuseAddress(on) + } + + /** + * [[akka.io.Tcp.SocketOption]] to set the SO_SNDBUF option. + * + * For more information see [[java.net.Socket.setSendBufferSize]] + */ + case class SendBufferSize(size: Int) extends SocketOption { + require(size > 0, "SendBufferSize must be > 0") + override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) + } + + /** + * [[akka.io.Tcp.SocketOption]] to set the traffic class or + * type-of-service octet in the IP header for packets sent from this + * socket. + * + * For more information see [[java.net.Socket.setTrafficClass]] + */ + case class TrafficClass(tc: Int) extends SocketOption { + require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]") + override def afterConnect(s: Socket): Unit = s.setTrafficClass(tc) + } + + } + +} diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index 4b71c81676..f7a31a76fe 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -6,7 +6,7 @@ package akka.io import java.net.InetSocketAddress import java.net.Socket -import java.net.ServerSocket +import akka.io.Inet.SocketOption import com.typesafe.config.Config import scala.concurrent.duration._ import scala.collection.immutable @@ -18,56 +18,13 @@ object Tcp extends ExtensionKey[TcpExt] { // Java API override def get(system: ActorSystem): TcpExt = system.extension(this) - /** - * SocketOption is a package of data (from the user) and associated - * behavior (how to apply that to a socket). - */ - sealed trait SocketOption { - /** - * Action to be taken for this option before calling bind() - */ - def beforeBind(s: ServerSocket): Unit = () - /** - * Action to be taken for this option before calling connect() - */ - def beforeConnect(s: Socket): Unit = () - /** - * Action to be taken for this option after connect returned (i.e. on - * the slave socket for servers). - */ - def afterConnect(s: Socket): Unit = () - } - // shared socket options object SO { - /** - * [[akka.io.Tcp.SocketOption]] to set the SO_RCVBUF option - * - * For more information see [[java.net.Socket.setReceiveBufferSize]] - */ - case class ReceiveBufferSize(size: Int) extends SocketOption { - require(size > 0, "ReceiveBufferSize must be > 0") - override def beforeBind(s: ServerSocket): Unit = s.setReceiveBufferSize(size) - override def beforeConnect(s: Socket): Unit = s.setReceiveBufferSize(size) - } - - // server socket options - - /** - * [[akka.io.Tcp.SocketOption]] to enable or disable SO_REUSEADDR - * - * For more information see [[java.net.Socket.setReuseAddress]] - */ - case class ReuseAddress(on: Boolean) extends SocketOption { - override def beforeBind(s: ServerSocket): Unit = s.setReuseAddress(on) - override def beforeConnect(s: Socket): Unit = s.setReuseAddress(on) - } - // general socket options /** - * [[akka.io.Tcp.SocketOption]] to enable or disable SO_KEEPALIVE + * [[akka.io.Inet.SocketOption]] to enable or disable SO_KEEPALIVE * * For more information see [[java.net.Socket.setKeepAlive]] */ @@ -76,7 +33,7 @@ object Tcp extends ExtensionKey[TcpExt] { } /** - * [[akka.io.Tcp.SocketOption]] to enable or disable OOBINLINE (receipt + * [[akka.io.Inet.SocketOption]] to enable or disable OOBINLINE (receipt * of TCP urgent data) By default, this option is disabled and TCP urgent * data is silently discarded. * @@ -86,20 +43,10 @@ object Tcp extends ExtensionKey[TcpExt] { override def afterConnect(s: Socket): Unit = s.setOOBInline(on) } - /** - * [[akka.io.Tcp.SocketOption]] to set the SO_SNDBUF option. - * - * For more information see [[java.net.Socket.setSendBufferSize]] - */ - case class SendBufferSize(size: Int) extends SocketOption { - require(size > 0, "SendBufferSize must be > 0") - override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size) - } - // SO_LINGER is handled by the Close code /** - * [[akka.io.Tcp.SocketOption]] to enable or disable TCP_NODELAY + * [[akka.io.Inet.SocketOption]] to enable or disable TCP_NODELAY * (disable or enable Nagle's algorithm) * * For more information see [[java.net.Socket.setTcpNoDelay]] @@ -108,17 +55,6 @@ object Tcp extends ExtensionKey[TcpExt] { override def afterConnect(s: Socket): Unit = s.setTcpNoDelay(on) } - /** - * [[akka.io.Tcp.SocketOption]] to set the traffic class or - * type-of-service octet in the IP header for packets sent from this - * socket. - * - * For more information see [[java.net.Socket.setTrafficClass]] - */ - case class TrafficClass(tc: Int) extends SocketOption { - require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]") - override def afterConnect(s: Socket): Unit = s.setTrafficClass(tc) - } } /// COMMANDS diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index b2dea0571c..b331ef0622 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -14,7 +14,8 @@ import scala.util.control.NonFatal import scala.concurrent.duration._ import akka.actor._ import akka.util.ByteString -import Tcp._ +import akka.io.Inet.SocketOption +import akka.io.Tcp._ import akka.io.SelectionHandler._ /** diff --git a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala index 5da609c617..ea2703c40e 100644 --- a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -7,7 +7,7 @@ package akka.io import java.nio.channels.SocketChannel import scala.collection.immutable import akka.actor.ActorRef -import akka.io.Tcp.SocketOption +import akka.io.Inet.SocketOption import akka.io.SelectionHandler.RegisterChannel /** diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index f3cc55d4a5..541d101327 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -11,7 +11,8 @@ import scala.collection.immutable import scala.util.control.NonFatal import akka.actor.{ Props, ActorLogging, ActorRef, Actor } import akka.io.SelectionHandler._ -import Tcp._ +import akka.io.Inet.SocketOption +import akka.io.Tcp._ private[io] class TcpListener(selectorRouter: ActorRef, handler: ActorRef, @@ -29,7 +30,7 @@ private[io] class TcpListener(selectorRouter: ActorRef, val serverSocketChannel = ServerSocketChannel.open serverSocketChannel.configureBlocking(false) val socket = serverSocketChannel.socket - options.foreach(_.beforeBind(socket)) + options.foreach(_.beforeServerSocketBind(socket)) socket.bind(endpoint, backlog) // will blow up the actor constructor if the bind fails serverSocketChannel } diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index f8dc6373ad..a6d3cc95ab 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -8,6 +8,7 @@ import akka.actor.{ ActorLogging, Actor, Props } import akka.routing.RandomRouter import Tcp._ import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } +import akka.io.IO.SelectorBasedManager /** * TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections. @@ -44,22 +45,15 @@ import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } * with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference. * */ -private[io] class TcpManager(tcp: TcpExt) extends Actor with ActorLogging { +private[io] class TcpManager(tcp: TcpExt) extends SelectorBasedManager(tcp.Settings, tcp.Settings.NrOfSelectors) with ActorLogging { - val selectorPool = context.actorOf( - props = Props(new SelectionHandler(self, tcp.Settings)).withRouter(RandomRouter(tcp.Settings.NrOfSelectors)), - name = "selectors") - - def receive = { - //case x @ (_: Connect | _: Bind) ⇒ selectorPool forward x - case c @ Connect(remoteAddress, localAddress, options) ⇒ + def receive = kickStartReceive { + case Connect(remoteAddress, localAddress, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(c, commander, Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options))) - - case b @ Bind(handler, endpoint, backlog, options) ⇒ + Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) + case Bind(handler, endpoint, backlog, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(b, commander, Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options))) - - case KickStartFailed(cmd: Command, commander) ⇒ commander ! CommandFailed(cmd) + Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options)) } + } diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 1a04f6bc5c..2c28bbada2 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -10,7 +10,8 @@ import java.nio.channels.{ SelectionKey, SocketChannel } import scala.collection.immutable import akka.actor.ActorRef import akka.io.SelectionHandler._ -import Tcp._ +import akka.io.Inet.SocketOption +import akka.io.Tcp._ /** * An actor handling the connection state machine for an outgoing connection diff --git a/akka-actor/src/main/scala/akka/io/Udp.scala b/akka-actor/src/main/scala/akka/io/Udp.scala new file mode 100644 index 0000000000..c543cf4927 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/Udp.scala @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import java.net.DatagramSocket +import akka.io.Inet.SocketOption + +object Udp { + + object SO { + + /** + * [[akka.io.Inet.SocketOption]] to set the SO_BROADCAST option + * + * For more information see [[java.net.DatagramSocket#setBroadcast]] + */ + case class Broadcast(on: Boolean) extends SocketOption { + override def beforeDatagramBind(s: DatagramSocket): Unit = s.setBroadcast(on) + } + + } + +} diff --git a/akka-actor/src/main/scala/akka/io/UdpConn.scala b/akka-actor/src/main/scala/akka/io/UdpConn.scala new file mode 100644 index 0000000000..651953e65e --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpConn.scala @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.{ ExtendedActorSystem, Props, ActorSystemImpl, ExtensionKey } + +object UdpConn extends ExtensionKey[UdpConnExt] { + +} + +class UdpConnExt(system: ExtendedActorSystem) extends IO.Extension { + + val manager = { + system.asInstanceOf[ActorSystemImpl].systemActorOf( + props = Props.empty, + name = "IO-UDP-CONN") + } + +} + diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index 6d9274e191..31913dce3d 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -5,82 +5,16 @@ package akka.io import akka.actor._ import akka.util.ByteString -import java.net.{ DatagramSocket, Socket, InetSocketAddress } +import java.net.{ DatagramSocket, InetSocketAddress } import scala.collection.immutable import com.typesafe.config.Config -import scala.concurrent.duration.Duration -import java.nio.ByteBuffer +import akka.io.Inet.SocketOption object UdpFF extends ExtensionKey[UdpFFExt] { // Java API override def get(system: ActorSystem): UdpFFExt = system.extension(this) - /** - * SocketOption is a package of data (from the user) and associated - * behavior (how to apply that to a socket). - */ - sealed trait SocketOption { - /** - * Action to be taken for this option before calling bind() - */ - def beforeBind(s: DatagramSocket): Unit = () - - } - - object SO { - - /** - * [[akka.io.UdpFF.SocketOption]] to set the SO_BROADCAST option - * - * For more information see [[java.net.DatagramSocket#setBroadcast]] - */ - case class Broadcast(on: Boolean) extends SocketOption { - override def beforeBind(s: DatagramSocket): Unit = s.setBroadcast(on) - } - - /** - * [[akka.io.UdpFF.SocketOption]] to set the SO_RCVBUF option - * - * For more information see [[java.net.Socket#setReceiveBufferSize]] - */ - case class ReceiveBufferSize(size: Int) extends SocketOption { - require(size > 0, "ReceiveBufferSize must be > 0") - override def beforeBind(s: DatagramSocket): Unit = s.setReceiveBufferSize(size) - } - - /** - * [[akka.io.UdpFF.SocketOption]] to enable or disable SO_REUSEADDR - * - * For more information see [[java.net.Socket#setReuseAddress]] - */ - case class ReuseAddress(on: Boolean) extends SocketOption { - override def beforeBind(s: DatagramSocket): Unit = s.setReuseAddress(on) - } - - /** - * [[akka.io.UdpFF.SocketOption]] to set the SO_SNDBUF option. - * - * For more information see [[java.net.Socket#setSendBufferSize]] - */ - case class SendBufferSize(size: Int) extends SocketOption { - require(size > 0, "SendBufferSize must be > 0") - override def beforeBind(s: DatagramSocket): Unit = s.setSendBufferSize(size) - } - - /** - * [[akka.io.UdpFF.SocketOption]] to set the traffic class or - * type-of-service octet in the IP header for packets sent from this - * socket. - * - * For more information see [[java.net.Socket#setTrafficClass]] - */ - case class TrafficClass(tc: Int) extends SocketOption { - require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]") - override def beforeBind(s: DatagramSocket): Unit = s.setTrafficClass(tc) - } - } - trait Command case object NoAck diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index a481559e07..c3600ff121 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -5,6 +5,7 @@ package akka.io import akka.actor.{ ActorLogging, Actor, ActorRef } import akka.io.UdpFF._ +import akka.io.Inet.SocketOption import akka.io.SelectionHandler._ import akka.util.ByteString import java.net.InetSocketAddress @@ -30,7 +31,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, val datagramChannel = DatagramChannel.open datagramChannel.configureBlocking(false) val socket = datagramChannel.socket - options.foreach(_.beforeBind(socket)) + options.foreach(_.beforeDatagramBind(socket)) socket.bind(endpoint) // will blow up the actor constructor if the bind fails datagramChannel } diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 1a9311a43b..be417166d0 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -7,6 +7,7 @@ import akka.actor.{ ActorRef, Props, Actor } import akka.io.UdpFF._ import akka.routing.RandomRouter import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } +import akka.io.IO.SelectorBasedManager /** * UdpFFManager is a facade for simple fire-and-forget style UDP operations @@ -43,24 +44,19 @@ import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } * discarded. * */ -private[io] class UdpFFManager(udpFF: UdpFFExt) extends Actor { - - val selectorPool = context.actorOf( - props = Props(new SelectionHandler(self, udpFF.settings)).withRouter(RandomRouter(udpFF.settings.NrOfSelectors)), - name = "selectors") +private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udpFF.settings, udpFF.settings.NrOfSelectors) { // FIXME: fix close overs lazy val anonymousSender: ActorRef = context.actorOf( props = Props(new UdpFFSender(udpFF, selectorPool)), name = "simplesend") - def receive = { - case b @ Bind(handler, endpoint, options) ⇒ + def receive = kickStartReceive { + case Bind(handler, endpoint, options) ⇒ val commander = sender - selectorPool ! KickStartCommand(b, commander, Props( - new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options))) - case SimpleSender ⇒ anonymousSender forward SimpleSender - case KickStartFailed(cmd: Command, commander) ⇒ commander ! CommandFailed(cmd) + Props(new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options)) + } orElse { + case SimpleSender ⇒ anonymousSender forward SimpleSender } } From 3505a7e76becce98553782866b51eec944011dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Feb 2013 12:17:26 +0100 Subject: [PATCH 55/66] Failed commands in SelectionHandler are now notified in a nicer way. --- .../test/scala/akka/io/TcpListenerSpec.scala | 12 +++---- akka-actor/src/main/scala/akka/io/IO.scala | 17 ++++++---- .../main/scala/akka/io/SelectionHandler.scala | 23 +++++++------ akka-actor/src/main/scala/akka/io/Tcp.scala | 6 ++-- .../src/main/scala/akka/io/TcpListener.scala | 32 ++++++++++++------- .../src/main/scala/akka/io/TcpManager.scala | 4 +-- akka-actor/src/main/scala/akka/io/UdpFF.scala | 4 ++- .../src/main/scala/akka/io/UdpFFManager.scala | 6 ++-- 8 files changed, 58 insertions(+), 46 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index aa66dc160a..da60c7880a 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -20,7 +20,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "register its ServerSocketChannel with its selector" in new TestSetup "let the Bind commander know when binding is completed" in new TestSetup { - listener ! KickStartDone + listener ! WorkerForCommandDone bindCommander.expectMsg(Bound) } @@ -36,13 +36,13 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { parent.expectMsg(AcceptInterest) // FIXME: ugly stuff here - selectorRouter.expectMsgType[KickStartCommand] - selectorRouter.expectMsgType[KickStartCommand] + selectorRouter.expectMsgType[WorkerForCommand] + selectorRouter.expectMsgType[WorkerForCommand] selectorRouter.expectNoMsg(100.millis) // and pick up the last remaining connection on the next ChannelAcceptable listener ! ChannelAcceptable - selectorRouter.expectMsgType[KickStartCommand] + selectorRouter.expectMsgType[WorkerForCommand] } "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { @@ -61,7 +61,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { attemptConnectionToEndpoint() listener ! ChannelAcceptable - val props = selectorRouter.expectMsgType[KickStartCommand].childProps + val props = selectorRouter.expectMsgType[WorkerForCommand].childProps // FIXME: need to instantiate propss //selectorRouter.expectMsgType[RegisterChannel].channel.isOpen must be(true) @@ -87,7 +87,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { parent.expectMsgType[RegisterChannel] def bindListener() { - listener ! KickStartDone + listener ! WorkerForCommandDone bindCommander.expectMsg(Bound) } diff --git a/akka-actor/src/main/scala/akka/io/IO.scala b/akka-actor/src/main/scala/akka/io/IO.scala index 2b6cbd9f57..8f152fe303 100644 --- a/akka-actor/src/main/scala/akka/io/IO.scala +++ b/akka-actor/src/main/scala/akka/io/IO.scala @@ -6,7 +6,7 @@ package akka.io import akka.actor._ import akka.routing.RandomRouter -import akka.io.SelectionHandler.KickStartCommand +import akka.io.SelectionHandler.WorkerForCommand object IO { @@ -16,22 +16,25 @@ object IO { def apply[T <: Extension](key: ExtensionKey[T])(implicit system: ActorSystem): ActorRef = key(system).manager + trait HasFailureMessage { + def failureMessage: Any + } + abstract class SelectorBasedManager(selectorSettings: SelectionHandlerSettings, nrOfSelectors: Int) extends Actor { val selectorPool = context.actorOf( props = Props(new SelectionHandler(self, selectorSettings)).withRouter(RandomRouter(nrOfSelectors)), name = "selectors") - def createKickStart(pf: PartialFunction[Any, Props], cmd: Any): PartialFunction[Any, KickStartCommand] = { - pf.andThen { props ⇒ + private def createKickStart(pf: PartialFunction[HasFailureMessage, Props]): PartialFunction[HasFailureMessage, WorkerForCommand] = { + case cmd ⇒ + val props = pf(cmd) val commander = sender - KickStartCommand(cmd, commander, props) - } + WorkerForCommand(cmd, commander, props) } def kickStartReceive(pf: PartialFunction[Any, Props]): Receive = { - //case KickStartFailed = - case cmd if pf.isDefinedAt(cmd) ⇒ selectorPool ! createKickStart(pf, cmd)(cmd) + case cmd: HasFailureMessage if pf.isDefinedAt(cmd) ⇒ selectorPool ! createKickStart(pf)(cmd) } } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 64d6b389f2..daadb0c8b5 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -14,6 +14,7 @@ import scala.concurrent.duration._ import akka.actor._ import com.typesafe.config.Config import akka.actor.Terminated +import akka.io.IO.HasFailureMessage abstract class SelectionHandlerSettings(config: Config) { import config._ @@ -42,14 +43,12 @@ abstract class SelectionHandlerSettings(config: Config) { private[io] object SelectionHandler { - //FIXME: temporary - case class KickStartCommand(apiCommand: Any, commander: ActorRef, childProps: Props) + case class WorkerForCommand(apiCommand: HasFailureMessage, commander: ActorRef, childProps: Props) // FIXME: all actors should listen to this - case object KickStartDone - case class KickStartFailed(apiCommand: Any, commander: ActorRef) + case object WorkerForCommandDone case class RegisterChannel(channel: SelectableChannel, initialOps: Int) - case class Retry(command: KickStartCommand, retriesLeft: Int) { require(retriesLeft >= 0) } + case class Retry(command: WorkerForCommand, retriesLeft: Int) { require(retriesLeft >= 0) } case object ChannelConnectable case object ChannelAcceptable @@ -75,21 +74,21 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) + // FIXME: provide StopReading functionality //case StopReading ⇒ execute(disableInterest(OP_READ, sender)) - case cmd: KickStartCommand ⇒ + case cmd: WorkerForCommand ⇒ // FIXME: factor out to common - withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) ! KickStartDone } + withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) ! WorkerForCommandDone } case RegisterChannel(channel, initialOps) ⇒ execute(registerChannel(channel, sender, initialOps)) - case Retry(cmd, 0) ⇒ - // FIXME: extractors - manager ! KickStartFailed(cmd.apiCommand, cmd.commander) + case Retry(WorkerForCommand(cmd, commander, _), 0) ⇒ + commander ! cmd.failureMessage case Retry(cmd, retriesLeft) ⇒ - withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) ! KickStartDone } + withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) ! WorkerForCommandDone } case Terminated(child) ⇒ execute(unregister(child)) @@ -114,7 +113,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler // we can never recover from failures of a connection or listener child override def supervisorStrategy = SupervisorStrategy.stoppingStrategy - def withCapacityProtection(cmd: KickStartCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { + def withCapacityProtection(cmd: WorkerForCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { log.debug("Executing {}", cmd) if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { body diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index f7a31a76fe..625cbd1e7a 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -6,7 +6,7 @@ package akka.io import java.net.InetSocketAddress import java.net.Socket -import akka.io.Inet.SocketOption +import akka.io.Inet._ import com.typesafe.config.Config import scala.concurrent.duration._ import scala.collection.immutable @@ -58,7 +58,9 @@ object Tcp extends ExtensionKey[TcpExt] { } /// COMMANDS - trait Command + trait Command extends IO.HasFailureMessage { + def failureMessage = CommandFailed(this) + } case class Connect(remoteAddress: InetSocketAddress, localAddress: Option[InetSocketAddress] = None, diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index 541d101327..e883310ae3 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -5,7 +5,7 @@ package akka.io import java.net.InetSocketAddress -import java.nio.channels.{ SelectionKey, ServerSocketChannel } +import java.nio.channels.{ SocketChannel, SelectionKey, ServerSocketChannel } import scala.annotation.tailrec import scala.collection.immutable import scala.util.control.NonFatal @@ -13,6 +13,17 @@ import akka.actor.{ Props, ActorLogging, ActorRef, Actor } import akka.io.SelectionHandler._ import akka.io.Inet.SocketOption import akka.io.Tcp._ +import akka.io.IO.HasFailureMessage + +private[io] object TcpListener { + + case class RegisterIncoming(channel: SocketChannel) extends HasFailureMessage { + def failureMessage = FailedRegisterIncoming(channel) + } + + case class FailedRegisterIncoming(channel: SocketChannel) + +} private[io] class TcpListener(selectorRouter: ActorRef, handler: ActorRef, @@ -23,6 +34,7 @@ private[io] class TcpListener(selectorRouter: ActorRef, options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { def selector: ActorRef = context.parent + import TcpListener._ import tcp.Settings._ context.watch(handler) // sign death pact @@ -38,7 +50,7 @@ private[io] class TcpListener(selectorRouter: ActorRef, log.debug("Successfully bound to {}", endpoint) def receive: Receive = { - case KickStartDone ⇒ + case WorkerForCommandDone ⇒ bindCommander ! Bound context.become(bound) } @@ -47,12 +59,12 @@ private[io] class TcpListener(selectorRouter: ActorRef, case ChannelAcceptable ⇒ acceptAllPending(BatchAcceptLimit) - // case CommandFailed(RegisterIncomingConnection(socketChannel, _, _)) ⇒ - // log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection") - // try socketChannel.close() - // catch { - // case NonFatal(e) ⇒ log.error(e, "Error closing channel") - // } + case FailedRegisterIncoming(socketChannel) ⇒ + log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection") + try socketChannel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing channel") + } case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) @@ -72,9 +84,7 @@ private[io] class TcpListener(selectorRouter: ActorRef, if (socketChannel != null) { log.debug("New connection accepted") socketChannel.configureBlocking(false) - //selectorRouter ! RegisterIncomingConnection(socketChannel, handler, options) - // FIXME null is not nice. There is no explicit API command here - selectorRouter ! KickStartCommand(null, context.system.deadLetters, Props(new TcpIncomingConnection(socketChannel, tcp, handler, options))) + selectorRouter ! WorkerForCommand(RegisterIncoming(socketChannel), self, Props(new TcpIncomingConnection(socketChannel, tcp, handler, options))) acceptAllPending(limit - 1) } } else context.parent ! AcceptInterest diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index a6d3cc95ab..81d3f91fa2 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -4,10 +4,8 @@ package akka.io -import akka.actor.{ ActorLogging, Actor, Props } -import akka.routing.RandomRouter import Tcp._ -import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } +import akka.actor.{ ActorLogging, Props } import akka.io.IO.SelectorBasedManager /** diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index 31913dce3d..0d6c49b83e 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -15,7 +15,9 @@ object UdpFF extends ExtensionKey[UdpFFExt] { // Java API override def get(system: ActorSystem): UdpFFExt = system.extension(this) - trait Command + trait Command extends IO.HasFailureMessage { + def failureMessage = CommandFailed(this) + } case object NoAck case class Send(payload: ByteString, target: InetSocketAddress, ack: Any) extends Command { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index be417166d0..4499cb8115 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -3,11 +3,9 @@ */ package akka.io -import akka.actor.{ ActorRef, Props, Actor } -import akka.io.UdpFF._ -import akka.routing.RandomRouter -import akka.io.SelectionHandler.{ KickStartFailed, KickStartCommand } +import akka.actor.{ ActorRef, Props } import akka.io.IO.SelectorBasedManager +import akka.io.UdpFF._ /** * UdpFFManager is a facade for simple fire-and-forget style UDP operations From 78a9e81a6b6ea7dc10cbd49b43b46dfa33b0e290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Feb 2013 13:26:27 +0100 Subject: [PATCH 56/66] Eliminated KickStartDone and using ChannelRegistered --- .../test/scala/akka/io/TcpListenerSpec.scala | 4 ++-- akka-actor/src/main/scala/akka/io/IO.scala | 6 +++--- .../main/scala/akka/io/SelectionHandler.scala | 8 ++++---- .../scala/akka/io/TcpIncomingConnection.scala | 7 ++++--- .../src/main/scala/akka/io/TcpListener.scala | 2 +- .../src/main/scala/akka/io/TcpManager.scala | 2 +- .../scala/akka/io/TcpOutgoingConnection.scala | 17 +++++++++-------- .../src/main/scala/akka/io/UdpFFListener.scala | 15 ++++++--------- .../src/main/scala/akka/io/UdpFFManager.scala | 2 +- .../src/main/scala/akka/io/UdpFFSender.scala | 9 ++++++--- 10 files changed, 37 insertions(+), 35 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index da60c7880a..e68cc6536e 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -20,7 +20,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { "register its ServerSocketChannel with its selector" in new TestSetup "let the Bind commander know when binding is completed" in new TestSetup { - listener ! WorkerForCommandDone + listener ! ChannelRegistered bindCommander.expectMsg(Bound) } @@ -87,7 +87,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { parent.expectMsgType[RegisterChannel] def bindListener() { - listener ! WorkerForCommandDone + listener ! ChannelRegistered bindCommander.expectMsg(Bound) } diff --git a/akka-actor/src/main/scala/akka/io/IO.scala b/akka-actor/src/main/scala/akka/io/IO.scala index 8f152fe303..5b10023990 100644 --- a/akka-actor/src/main/scala/akka/io/IO.scala +++ b/akka-actor/src/main/scala/akka/io/IO.scala @@ -26,15 +26,15 @@ object IO { props = Props(new SelectionHandler(self, selectorSettings)).withRouter(RandomRouter(nrOfSelectors)), name = "selectors") - private def createKickStart(pf: PartialFunction[HasFailureMessage, Props]): PartialFunction[HasFailureMessage, WorkerForCommand] = { + private def createWorkerMessage(pf: PartialFunction[HasFailureMessage, Props]): PartialFunction[HasFailureMessage, WorkerForCommand] = { case cmd ⇒ val props = pf(cmd) val commander = sender WorkerForCommand(cmd, commander, props) } - def kickStartReceive(pf: PartialFunction[Any, Props]): Receive = { - case cmd: HasFailureMessage if pf.isDefinedAt(cmd) ⇒ selectorPool ! createKickStart(pf)(cmd) + def workerForCommand(pf: PartialFunction[Any, Props]): Receive = { + case cmd: HasFailureMessage if pf.isDefinedAt(cmd) ⇒ selectorPool ! createWorkerMessage(pf)(cmd) } } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index daadb0c8b5..59b0894543 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -44,10 +44,9 @@ abstract class SelectionHandlerSettings(config: Config) { private[io] object SelectionHandler { case class WorkerForCommand(apiCommand: HasFailureMessage, commander: ActorRef, childProps: Props) - // FIXME: all actors should listen to this - case object WorkerForCommandDone case class RegisterChannel(channel: SelectableChannel, initialOps: Int) + case object ChannelRegistered case class Retry(command: WorkerForCommand, retriesLeft: Int) { require(retriesLeft >= 0) } case object ChannelConnectable @@ -79,7 +78,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler case cmd: WorkerForCommand ⇒ // FIXME: factor out to common - withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) ! WorkerForCommandDone } + withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) } case RegisterChannel(channel, initialOps) ⇒ execute(registerChannel(channel, sender, initialOps)) @@ -88,7 +87,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler commander ! cmd.failureMessage case Retry(cmd, retriesLeft) ⇒ - withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) ! WorkerForCommandDone } + withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) } case Terminated(child) ⇒ execute(unregister(child)) @@ -144,6 +143,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler new Task { def tryRun() { updateKeyMap(channelActor, channel.register(selector, initialOps, channelActor)) + channelActor ! ChannelRegistered } } diff --git a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala index ea2703c40e..2f7cf9c5fa 100644 --- a/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpIncomingConnection.scala @@ -8,7 +8,7 @@ import java.nio.channels.SocketChannel import scala.collection.immutable import akka.actor.ActorRef import akka.io.Inet.SocketOption -import akka.io.SelectionHandler.RegisterChannel +import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel } /** * An actor handling the connection state machine for an incoming, already connected @@ -22,8 +22,9 @@ private[io] class TcpIncomingConnection(_channel: SocketChannel, context.watch(handler) // sign death pact - completeConnect(handler, options) context.parent ! RegisterChannel(channel, 0) - def receive = PartialFunction.empty + def receive = { + case ChannelRegistered ⇒ completeConnect(handler, options) + } } diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index e883310ae3..356319286f 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -50,7 +50,7 @@ private[io] class TcpListener(selectorRouter: ActorRef, log.debug("Successfully bound to {}", endpoint) def receive: Receive = { - case WorkerForCommandDone ⇒ + case ChannelRegistered ⇒ bindCommander ! Bound context.become(bound) } diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index 81d3f91fa2..8761104ba5 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -45,7 +45,7 @@ import akka.io.IO.SelectorBasedManager */ private[io] class TcpManager(tcp: TcpExt) extends SelectorBasedManager(tcp.Settings, tcp.Settings.NrOfSelectors) with ActorLogging { - def receive = kickStartReceive { + def receive = workerForCommand { case Connect(remoteAddress, localAddress, options) ⇒ val commander = sender Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 2c28bbada2..6c9231fa46 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -28,17 +28,18 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, localAddress.foreach(channel.socket.bind) options.foreach(_.beforeConnect(channel.socket)) + selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) - log.debug("Attempting connection to {}", remoteAddress) - if (channel.connect(remoteAddress)) - completeConnect(commander, options) - else { - selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT) - context.become(connecting(commander, options)) + def receive: Receive = { + case ChannelRegistered ⇒ + log.debug("Attempting connection to {}", remoteAddress) + if (channel.connect(remoteAddress)) + completeConnect(commander, options) + else { + context.become(connecting(commander, options)) + } } - def receive: Receive = PartialFunction.empty - def connecting(commander: ActorRef, options: immutable.Traversable[SocketOption]): Receive = { case ChannelConnectable ⇒ try { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index c3600ff121..cfafb8bc52 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -13,6 +13,8 @@ import java.nio.channels.DatagramChannel import java.nio.channels.SelectionKey._ import scala.collection.immutable import scala.util.control.NonFatal +import akka.io.UdpFF.Received +import akka.io.SelectionHandler.RegisterChannel private[io] class UdpFFListener(selectorRouter: ActorRef, handler: ActorRef, @@ -39,20 +41,15 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, bindCommander ! Bound log.debug("Successfully bound to {}", endpoint) - def receive: Receive = receiveInternal orElse sendHandlers + def receive: Receive = { + case ChannelRegistered ⇒ context.become(readHandlers orElse sendHandlers, discardOld = true) + } - def receiveInternal: Receive = { + def readHandlers: Receive = { case StopReading ⇒ selector ! StopReading case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doReceive(handler, None) - // case CommandFailed(RegisterChannel(channel, _)) ⇒ - // log.warning("Could not bind to UDP port since selector capacity limit is reached, aborting bind") - // try channel.close() - // catch { - // case NonFatal(e) ⇒ log.error(e, "Error closing channel") - // } - case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) channel.close() diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 4499cb8115..3f5c677991 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -49,7 +49,7 @@ private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udp props = Props(new UdpFFSender(udpFF, selectorPool)), name = "simplesend") - def receive = kickStartReceive { + def receive = workerForCommand { case Bind(handler, endpoint, options) ⇒ val commander = sender Props(new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options)) diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala index 4e6245c160..24e222e71c 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -6,7 +6,7 @@ package akka.io import akka.actor._ import java.nio.channels.DatagramChannel import akka.io.UdpFF._ -import akka.io.SelectionHandler.RegisterChannel +import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel } /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. @@ -21,9 +21,12 @@ private[io] class UdpFFSender(val udpFF: UdpFFExt, val selector: ActorRef) } selector ! RegisterChannel(channel, 0) - def receive: Receive = internalReceive orElse sendHandlers + def receive: Receive = { + case ChannelRegistered ⇒ context.become(simpleSendHandlers orElse sendHandlers, discardOld = true) + case _ ⇒ sender ! SimpleSendReady // FIXME: queueing here? + } - def internalReceive: Receive = { + def simpleSendHandlers: Receive = { case SimpleSender ⇒ sender ! SimpleSendReady } From 98a707bd57ee848f5f1cdb2405e96bb0d9743799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Feb 2013 13:38:27 +0100 Subject: [PATCH 57/66] Fixed temporarily disabled StopReading operation --- akka-actor/src/main/scala/akka/io/SelectionHandler.scala | 5 ++--- akka-actor/src/main/scala/akka/io/Tcp.scala | 1 - akka-actor/src/main/scala/akka/io/TcpConnection.scala | 6 +++--- akka-actor/src/main/scala/akka/io/UdpFFListener.scala | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 59b0894543..5c3d7c6251 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -55,6 +55,7 @@ private[io] object SelectionHandler { case object ChannelWritable case object AcceptInterest case object ReadInterest + case object DisableReadInterest case object WriteInterest } @@ -73,11 +74,9 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) - // FIXME: provide StopReading functionality - //case StopReading ⇒ execute(disableInterest(OP_READ, sender)) + case DisableReadInterest ⇒ execute(disableInterest(OP_READ, sender)) case cmd: WorkerForCommand ⇒ - // FIXME: factor out to common withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) } case RegisterChannel(channel, initialOps) ⇒ diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index 625cbd1e7a..38c8051714 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -117,7 +117,6 @@ object Tcp extends ExtensionKey[TcpExt] { class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val Settings = new Settings(system.settings.config.getConfig("akka.io.tcp")) - // FIXME: get away with subclassess class Settings private[TcpExt] (_config: Config) extends SelectionHandlerSettings(_config) { import _config._ diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index b331ef0622..dffec75307 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -60,7 +60,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, /** normal connected state */ def connected(handler: ActorRef): Receive = { - case StopReading ⇒ selector ! StopReading + case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doRead(handler, None) @@ -83,7 +83,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, /** connection is closing but a write has to be finished first */ def closingWithPendingWrite(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Receive = { - case StopReading ⇒ selector ! StopReading + case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doRead(handler, closeCommander) @@ -97,7 +97,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, /** connection is closed on our side and we're waiting from confirmation from the other side */ def closing(handler: ActorRef, closeCommander: Option[ActorRef]): Receive = { - case StopReading ⇒ selector ! StopReading + case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doRead(handler, closeCommander) case Abort ⇒ handleClose(handler, Some(sender), Aborted) diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index cfafb8bc52..0d79f64e83 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -46,7 +46,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, } def readHandlers: Receive = { - case StopReading ⇒ selector ! StopReading + case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest case ChannelReadable ⇒ doReceive(handler, None) From e2ce4644f1ebf9914593759036fc12030db8d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 5 Feb 2013 15:48:29 +0100 Subject: [PATCH 58/66] Added connection style UDP (not working fully yet) --- .../akka/io/UdpConnIntegrationSpec.scala | 76 +++++++++++ akka-actor/src/main/resources/reference.conf | 59 ++++++++- .../main/scala/akka/io/SelectionHandler.scala | 8 +- akka-actor/src/main/scala/akka/io/Udp.scala | 22 +++ .../src/main/scala/akka/io/UdpConn.scala | 49 ++++++- .../main/scala/akka/io/UdpConnManager.scala | 18 +++ .../main/scala/akka/io/UdpConnection.scala | 125 ++++++++++++++++++ akka-actor/src/main/scala/akka/io/UdpFF.scala | 33 +---- .../main/scala/akka/io/UdpFFListener.scala | 20 +-- .../main/scala/akka/io/WithUdpFFSend.scala | 2 +- 10 files changed, 365 insertions(+), 47 deletions(-) create mode 100644 akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpConnManager.scala create mode 100644 akka-actor/src/main/scala/akka/io/UdpConnection.scala diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala new file mode 100644 index 0000000000..90bfdb222e --- /dev/null +++ b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala @@ -0,0 +1,76 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.testkit.{ TestProbe, ImplicitSender, AkkaSpec } +import TestUtils._ +import akka.util.ByteString +import java.net.InetSocketAddress +import akka.actor.ActorRef + +class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with ImplicitSender { + + def bindUdp(handler: ActorRef): (InetSocketAddress, ActorRef) = { + val address = temporaryServerAddress() + val commander = TestProbe() + commander.send(IO(UdpFF), UdpFF.Bind(handler, address)) + commander.expectMsg(UdpFF.Bound) + (address, commander.sender) + } + + def connectUdp(localAddress: Option[InetSocketAddress], remoteAddress: InetSocketAddress): ActorRef = { + val commander = TestProbe() + commander.send(IO(UdpConn), UdpConn.Connect(testActor, localAddress, remoteAddress, Nil)) + commander.expectMsg(UdpConn.Connected) + commander.sender + } + + "The UDP connection oriented implementation" must { + + "be able to send and receive without binding" in { + val (serverAddress, server) = bindUdp(testActor) + val data1 = ByteString("To infinity and beyond!") + val data2 = ByteString("All your datagram belong to us") + connectUdp(localAddress = None, serverAddress) ! UdpConn.Send(data1) + + val clientAddress = expectMsgPF() { + case UdpFF.Received(d, a) ⇒ + d must be === data1 + a + } + + println(clientAddress) + + server ! UdpFF.Send(data2, clientAddress) + + // FIXME: Currently this line fails + expectMsgPF() { + case UdpConn.Received(d) ⇒ d must be === data2 + } + } + + "be able to send and receive with binding" in { + val clientAddress = temporaryServerAddress() + val (serverAddress, server) = bindUdp(testActor) + val data1 = ByteString("To infinity and beyond!") + val data2 = ByteString("All your datagram belong to us") + connectUdp(Some(clientAddress), serverAddress) ! UdpConn.Send(data1) + + expectMsgPF() { + case UdpFF.Received(d, a) ⇒ + d must be === data1 + a must be === clientAddress + } + + server ! UdpFF.Send(data2, clientAddress) + + // FIXME: Currently this line fails + expectMsgPF() { + case UdpConn.Received(d) ⇒ d must be === data2 + } + } + + } + +} diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index 859ae7acbf..b755af0e09 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -445,7 +445,7 @@ akka { management-dispatcher = "akka.actor.default-dispatcher" } - udpFF { + udp-fire-and-forget { # The number of selectors to stripe the served channels over; each of # these will use one select loop on the selector-dispatcher. @@ -501,6 +501,63 @@ akka { management-dispatcher = "akka.actor.default-dispatcher" } + udp-connection { + + # The number of selectors to stripe the served channels over; each of + # these will use one select loop on the selector-dispatcher. + nr-of-selectors = 1 + + # Maximum number of open channels supported by this UDP module Generally + # UDP does not require a large number of channels, therefore it is + # recommended to keep this setting low. + max-channels = 4096 + + # The select loop can be used in two modes: + # - setting "infinite" will select without a timeout, hogging a thread + # - setting a positive timeout will do a bounded select call, + # enabling sharing of a single thread between multiple selectors + # (in this case you will have to use a different configuration for the + # selector-dispatcher, e.g. using "type=Dispatcher" with size 1) + # - setting it to zero means polling, i.e. calling selectNow() + select-timeout = infinite + + # When trying to assign a new connection to a selector and the chosen + # selector is at full capacity, retry selector choosing and assignment + # this many times before giving up + selector-association-retries = 10 + + # The number of bytes per direct buffer in the pool used to read or write + # network data from the kernel. + direct-buffer-size = 128 KiB + + # The maximal number of direct buffers kept in the direct buffer pool for + # reuse. + max-direct-buffer-pool-size = 1000 + + # The maximum number of bytes delivered by a `Received` message. Before + # more data is read from the network the connection actor will try to + # do other work. + received-message-size-limit = unlimited + + # Enable fine grained logging of what goes on inside the implementation. + # Be aware that this may log more than once per message sent to the actors + # of the tcp implementation. + trace-logging = off + + # Fully qualified config path which holds the dispatcher configuration + # to be used for running the select() calls in the selectors + selector-dispatcher = "akka.io.pinned-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the read/write worker actors + worker-dispatcher = "akka.actor.default-dispatcher" + + # Fully qualified config path which holds the dispatcher configuration + # for the selector management actors + management-dispatcher = "akka.actor.default-dispatcher" + } + + # IMPORTANT NOTICE: # # The following settings belong to the deprecated akka.actor.IO diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 5c3d7c6251..0f0c068017 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -70,11 +70,11 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler val OP_READ_AND_WRITE = OP_READ | OP_WRITE // compile-time constant def receive: Receive = { - case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) - case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) - case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) + case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender)) + case ReadInterest ⇒ execute(enableInterest(OP_READ, sender)) + case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender)) - case DisableReadInterest ⇒ execute(disableInterest(OP_READ, sender)) + case DisableReadInterest ⇒ execute(disableInterest(OP_READ, sender)) case cmd: WorkerForCommand ⇒ withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) } diff --git a/akka-actor/src/main/scala/akka/io/Udp.scala b/akka-actor/src/main/scala/akka/io/Udp.scala index c543cf4927..18d874baa4 100644 --- a/akka-actor/src/main/scala/akka/io/Udp.scala +++ b/akka-actor/src/main/scala/akka/io/Udp.scala @@ -5,6 +5,8 @@ package akka.io import java.net.DatagramSocket import akka.io.Inet.SocketOption +import com.typesafe.config.Config +import akka.actor.{ Props, ActorSystemImpl } object Udp { @@ -21,4 +23,24 @@ object Udp { } + private[io] class UdpSettings(_config: Config) extends SelectionHandlerSettings(_config) { + import _config._ + + val NrOfSelectors = getInt("nr-of-selectors") + val DirectBufferSize = getIntBytes("direct-buffer-size") + val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") + + val ManagementDispatcher = getString("management-dispatcher") + + require(NrOfSelectors > 0, "nr-of-selectors must be > 0") + + override val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) + + private[this] def getIntBytes(path: String): Int = { + val size = getBytes(path) + require(size < Int.MaxValue, s"$path must be < 2 GiB") + size.toInt + } + } + } diff --git a/akka-actor/src/main/scala/akka/io/UdpConn.scala b/akka-actor/src/main/scala/akka/io/UdpConn.scala index 651953e65e..df6715d2c5 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConn.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConn.scala @@ -3,19 +3,62 @@ */ package akka.io -import akka.actor.{ ExtendedActorSystem, Props, ActorSystemImpl, ExtensionKey } +import akka.actor._ +import akka.io.Inet.SocketOption +import akka.io.Udp.UdpSettings +import akka.util.ByteString +import java.net.InetSocketAddress +import scala.collection.immutable object UdpConn extends ExtensionKey[UdpConnExt] { + // Java API + override def get(system: ActorSystem): UdpConnExt = system.extension(this) + + trait Command extends IO.HasFailureMessage { + def failureMessage = CommandFailed(this) + } + + case object NoAck + case class Send(payload: ByteString, ack: Any) extends Command { + require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.") + + def wantsAck: Boolean = ack != NoAck + } + object Send { + def apply(data: ByteString): Send = Send(data, NoAck) + } + + case class Connect(handler: ActorRef, + localAddress: Option[InetSocketAddress], + remoteAddress: InetSocketAddress, + options: immutable.Traversable[SocketOption] = Nil) extends Command + + case object StopReading extends Command + case object ResumeReading extends Command + + trait Event + + case class Received(data: ByteString) extends Event + case class CommandFailed(cmd: Command) extends Event + case object Connected extends Event + case object Disconnected extends Event + + case object Close extends Command + + case class SendFailed(cause: Throwable) extends Event } class UdpConnExt(system: ExtendedActorSystem) extends IO.Extension { + val settings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) + val manager = { system.asInstanceOf[ActorSystemImpl].systemActorOf( - props = Props.empty, + props = Props(new UdpConnManager(this)), name = "IO-UDP-CONN") } -} + val bufferPool: BufferPool = new DirectByteBufferPool(settings.DirectBufferSize, settings.MaxDirectBufferPoolSize) +} \ No newline at end of file diff --git a/akka-actor/src/main/scala/akka/io/UdpConnManager.scala b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala new file mode 100644 index 0000000000..8dbe806c7d --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.Props +import akka.io.IO.SelectorBasedManager +import akka.io.UdpConn.Connect + +class UdpConnManager(udpConn: UdpConnExt) extends SelectorBasedManager(udpConn.settings, udpConn.settings.NrOfSelectors) { + + def receive = workerForCommand { + case Connect(handler, localAddress, remoteAddress, options) ⇒ + val commander = sender + Props(new UdpConnection(selectorPool, handler, localAddress, remoteAddress, commander, udpConn, options)) + } + +} diff --git a/akka-actor/src/main/scala/akka/io/UdpConnection.scala b/akka-actor/src/main/scala/akka/io/UdpConnection.scala new file mode 100644 index 0000000000..8f8fd2fb35 --- /dev/null +++ b/akka-actor/src/main/scala/akka/io/UdpConnection.scala @@ -0,0 +1,125 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ +package akka.io + +import akka.actor.{ Actor, ActorLogging, ActorRef } +import akka.io.Inet.SocketOption +import akka.io.SelectionHandler._ +import akka.io.UdpConn._ +import akka.util.ByteString +import java.net.InetSocketAddress +import java.nio.channels.DatagramChannel +import java.nio.channels.SelectionKey._ +import scala.collection.immutable +import scala.util.control.NonFatal + +private[io] class UdpConnection(selectorRouter: ActorRef, + handler: ActorRef, + localAddress: Option[InetSocketAddress], + remoteAddress: InetSocketAddress, + bindCommander: ActorRef, + val udpConn: UdpConnExt, + options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { + + def selector: ActorRef = context.parent + + import udpConn._ + import udpConn.settings._ + + var pendingSend: (Send, ActorRef) = null + def writePending = pendingSend ne null + + context.watch(handler) // sign death pact + val channel = { + val datagramChannel = DatagramChannel.open + datagramChannel.configureBlocking(false) + val socket = datagramChannel.socket + options.foreach(_.beforeDatagramBind(socket)) + localAddress foreach { socket.bind } // will blow up the actor constructor if the bind fails + datagramChannel.connect(remoteAddress) + datagramChannel + } + selector ! RegisterChannel(channel, OP_READ) + log.debug("Successfully connected to {}", remoteAddress) + + def receive = { + case ChannelRegistered ⇒ + bindCommander ! Connected + context.become(connected, discardOld = true) + } + + def connected: Receive = { + case StopReading ⇒ selector ! DisableReadInterest + case ResumeReading ⇒ selector ! ReadInterest + case ChannelReadable ⇒ doRead(handler) + + case Close ⇒ + log.debug("Closing UDP connection to {}", remoteAddress) + channel.close() + sender ! Disconnected + log.debug("Connection closed to {}, stopping listener", remoteAddress) + context.stop(self) + + case send: Send if writePending ⇒ + if (TraceLogging) log.debug("Dropping write because queue is full") + sender ! CommandFailed(send) + + case send: Send if send.payload.isEmpty ⇒ + if (send.wantsAck) + sender ! send.ack + + case send: Send ⇒ + pendingSend = (send, sender) + selector ! WriteInterest + + case ChannelWritable ⇒ doWrite() + } + + def doRead(handler: ActorRef): Unit = { + val buffer = bufferPool.acquire() + try { + buffer.clear() + buffer.limit(DirectBufferSize) + + if (channel.read(buffer) > 0) handler ! Received(ByteString(buffer)) + + } finally { + selector ! ReadInterest + bufferPool.release(buffer) + } + } + + final def doWrite(): Unit = { + + val buffer = udpConn.bufferPool.acquire() + try { + val (send, commander) = pendingSend + buffer.clear() + send.payload.copyToBuffer(buffer) + buffer.flip() + val writtenBytes = channel.write(buffer) + if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) + + // Datagram channel either sends the whole message, or nothing + if (writtenBytes == 0) commander ! CommandFailed(send) + else if (send.wantsAck) commander ! send.ack + + } finally { + udpConn.bufferPool.release(buffer) + pendingSend = null + } + + } + + override def postStop() { + if (channel.isOpen) { + log.debug("Closing DatagramChannel after being stopped") + try channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel") + } + } + } + +} diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index 0d6c49b83e..51b1b7429c 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -4,11 +4,11 @@ package akka.io import akka.actor._ -import akka.util.ByteString -import java.net.{ DatagramSocket, InetSocketAddress } -import scala.collection.immutable -import com.typesafe.config.Config import akka.io.Inet.SocketOption +import akka.io.Udp.UdpSettings +import akka.util.ByteString +import java.net.InetSocketAddress +import scala.collection.immutable object UdpFF extends ExtensionKey[UdpFFExt] { @@ -47,36 +47,13 @@ object UdpFF extends ExtensionKey[UdpFFExt] { case object SimpleSendReady extends Event case object Unbound extends Event - sealed trait CloseCommand extends Command - case object Close extends CloseCommand - case object Abort extends CloseCommand - case class SendFailed(cause: Throwable) extends Event } class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { - val settings = new Settings(system.settings.config.getConfig("akka.io.udpFF")) - class Settings private[UdpFFExt] (_config: Config) extends SelectionHandlerSettings(_config) { - import _config._ - - val NrOfSelectors = getInt("nr-of-selectors") - val DirectBufferSize = getIntBytes("direct-buffer-size") - val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") - - val ManagementDispatcher = getString("management-dispatcher") - - require(NrOfSelectors > 0, "nr-of-selectors must be > 0") - - override val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) - - private[this] def getIntBytes(path: String): Int = { - val size = getBytes(path) - require(size < Int.MaxValue, s"$path must be < 2 GiB") - size.toInt - } - } + val settings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) val manager = { system.asInstanceOf[ActorSystemImpl].systemActorOf( diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index 0d79f64e83..3f464bccc5 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -38,17 +38,18 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, datagramChannel } context.parent ! RegisterChannel(channel, OP_READ) - bindCommander ! Bound log.debug("Successfully bound to {}", endpoint) def receive: Receive = { - case ChannelRegistered ⇒ context.become(readHandlers orElse sendHandlers, discardOld = true) + case ChannelRegistered ⇒ + bindCommander ! Bound + context.become(readHandlers orElse sendHandlers, discardOld = true) } def readHandlers: Receive = { case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ doReceive(handler, None) + case ChannelReadable ⇒ doReceive(handler) case Unbind ⇒ log.debug("Unbinding endpoint {}", endpoint) @@ -58,7 +59,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, context.stop(self) } - def doReceive(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = { + def doReceive(handler: ActorRef): Unit = { val buffer = bufferPool.acquire() try { buffer.clear() @@ -76,13 +77,12 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, } override def postStop() { - try { - if (channel.isOpen) { - log.debug("Closing serverSocketChannel after being stopped") - channel.close() + if (channel.isOpen) { + log.debug("Closing DatagramChannel after being stopped") + try channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel") } - } catch { - case NonFatal(e) ⇒ log.error(e, "Error closing ServerSocketChannel") } } } diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala index da837c35fd..e04c4ca6f5 100644 --- a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -8,7 +8,7 @@ import akka.io.UdpFF.{ CommandFailed, Send } import akka.io.SelectionHandler._ import java.nio.channels.DatagramChannel -trait WithUdpFFSend { +private[io] trait WithUdpFFSend { me: Actor with ActorLogging ⇒ var pendingSend: (Send, ActorRef) = null From 116dcc0e544e71a26e0ec084e76823743dc7f16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Wed, 6 Feb 2013 11:38:42 +0100 Subject: [PATCH 59/66] Various minor changes: - added forwarders to SO objects to Inet - added batch read support for UDP - changed write for UDP - SimpleSender is now registered as everone else --- .../test/scala/akka/io/IntegrationSpec.scala | 5 ++--- .../akka/io/UdpConnIntegrationSpec.scala | 8 +++---- .../scala/akka/io/UdpFFIntegrationSpec.scala | 2 +- akka-actor/src/main/resources/reference.conf | 10 +++++++++ akka-actor/src/main/scala/akka/io/Inet.scala | 7 +++++++ .../main/scala/akka/io/SelectionHandler.scala | 1 + akka-actor/src/main/scala/akka/io/Tcp.scala | 2 +- akka-actor/src/main/scala/akka/io/Udp.scala | 4 +++- .../main/scala/akka/io/UdpConnection.scala | 20 +++++++++++------- akka-actor/src/main/scala/akka/io/UdpFF.scala | 2 +- .../main/scala/akka/io/UdpFFListener.scala | 15 +++++++++---- .../src/main/scala/akka/io/UdpFFManager.scala | 10 +++------ .../src/main/scala/akka/io/UdpFFSender.scala | 21 ++++++++++++------- .../main/scala/akka/io/WithUdpFFSend.scala | 18 ++++++++++++---- 14 files changed, 85 insertions(+), 40 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala index 1e7a40eb90..e4d53f5f9b 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala @@ -6,7 +6,6 @@ package akka.io import akka.testkit.AkkaSpec import akka.util.ByteString -import akka.io.Inet import Tcp._ import TestUtils._ import akka.testkit.EventFilter @@ -65,8 +64,8 @@ class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationS expectReceivedData(clientHandler, 100000) - override def bindOptions = List(Inet.SO.SendBufferSize(1024)) - override def connectOptions = List(Inet.SO.ReceiveBufferSize(1024)) + override def bindOptions = List(SO.SendBufferSize(1024)) + override def connectOptions = List(SO.ReceiveBufferSize(1024)) } } diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala index 90bfdb222e..ece503de33 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala @@ -19,9 +19,9 @@ class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Impli (address, commander.sender) } - def connectUdp(localAddress: Option[InetSocketAddress], remoteAddress: InetSocketAddress): ActorRef = { + def connectUdp(localAddress: Option[InetSocketAddress], remoteAddress: InetSocketAddress, handler: ActorRef): ActorRef = { val commander = TestProbe() - commander.send(IO(UdpConn), UdpConn.Connect(testActor, localAddress, remoteAddress, Nil)) + commander.send(IO(UdpConn), UdpConn.Connect(handler, localAddress, remoteAddress, Nil)) commander.expectMsg(UdpConn.Connected) commander.sender } @@ -32,7 +32,7 @@ class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Impli val (serverAddress, server) = bindUdp(testActor) val data1 = ByteString("To infinity and beyond!") val data2 = ByteString("All your datagram belong to us") - connectUdp(localAddress = None, serverAddress) ! UdpConn.Send(data1) + connectUdp(localAddress = None, serverAddress, testActor) ! UdpConn.Send(data1) val clientAddress = expectMsgPF() { case UdpFF.Received(d, a) ⇒ @@ -55,7 +55,7 @@ class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Impli val (serverAddress, server) = bindUdp(testActor) val data1 = ByteString("To infinity and beyond!") val data2 = ByteString("All your datagram belong to us") - connectUdp(Some(clientAddress), serverAddress) ! UdpConn.Send(data1) + connectUdp(Some(clientAddress), serverAddress, testActor) ! UdpConn.Send(data1) expectMsgPF() { case UdpFF.Received(d, a) ⇒ diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala index 21d61c3e1d..a0f138f041 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala @@ -22,7 +22,7 @@ class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Implici val simpleSender: ActorRef = { val commander = TestProbe() - commander.send(IO(UdpFF), SimpleSender) + commander.send(IO(UdpFF), SimpleSender(Nil)) commander.expectMsg(SimpleSendReady) commander.sender } diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index b755af0e09..c17594a307 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -470,6 +470,11 @@ akka { # this many times before giving up selector-association-retries = 10 + # The maximum number of datagrams that are read in one go, + # higher numbers decrease latency, lower numbers increase fairness on + # the worker-dispatcher + batch-receive-limit = 3 + # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. direct-buffer-size = 128 KiB @@ -526,6 +531,11 @@ akka { # this many times before giving up selector-association-retries = 10 + # The maximum number of datagrams that are read in one go, + # higher numbers decrease latency, lower numbers increase fairness on + # the worker-dispatcher + batch-receive-limit = 3 + # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. direct-buffer-size = 128 KiB diff --git a/akka-actor/src/main/scala/akka/io/Inet.scala b/akka-actor/src/main/scala/akka/io/Inet.scala index 9e53507284..0b9fb4ca0c 100644 --- a/akka-actor/src/main/scala/akka/io/Inet.scala +++ b/akka-actor/src/main/scala/akka/io/Inet.scala @@ -79,4 +79,11 @@ object Inet { } + trait SoForwarders { + val ReceiveBufferSize = SO.ReceiveBufferSize + val ReuseAddress = SO.ReuseAddress + val SendBufferSize = SO.SendBufferSize + val TrafficClass = SO.TrafficClass + } + } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 0f0c068017..e7046d8b34 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -208,6 +208,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler selectorManagementDispatcher.execute(select) // start selection "loop" + // FIXME: Add possibility to signal failure of task to someone abstract class Task extends Runnable { def tryRun() def run() { diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index 38c8051714..95218f16cf 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -19,7 +19,7 @@ object Tcp extends ExtensionKey[TcpExt] { override def get(system: ActorSystem): TcpExt = system.extension(this) // shared socket options - object SO { + object SO extends Inet.SoForwarders { // general socket options diff --git a/akka-actor/src/main/scala/akka/io/Udp.scala b/akka-actor/src/main/scala/akka/io/Udp.scala index 18d874baa4..83e8b0d5f1 100644 --- a/akka-actor/src/main/scala/akka/io/Udp.scala +++ b/akka-actor/src/main/scala/akka/io/Udp.scala @@ -10,7 +10,7 @@ import akka.actor.{ Props, ActorSystemImpl } object Udp { - object SO { + object SO extends Inet.SoForwarders { /** * [[akka.io.Inet.SocketOption]] to set the SO_BROADCAST option @@ -29,9 +29,11 @@ object Udp { val NrOfSelectors = getInt("nr-of-selectors") val DirectBufferSize = getIntBytes("direct-buffer-size") val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") + val BatchReceiveLimit = getInt("batch-receive-limit") val ManagementDispatcher = getString("management-dispatcher") + // FIXME: Use new requiring require(NrOfSelectors > 0, "nr-of-selectors must be > 0") override val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1) diff --git a/akka-actor/src/main/scala/akka/io/UdpConnection.scala b/akka-actor/src/main/scala/akka/io/UdpConnection.scala index 8f8fd2fb35..11a5a17f71 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnection.scala @@ -13,6 +13,8 @@ import java.nio.channels.DatagramChannel import java.nio.channels.SelectionKey._ import scala.collection.immutable import scala.util.control.NonFatal +import java.nio.ByteBuffer +import scala.annotation.tailrec private[io] class UdpConnection(selectorRouter: ActorRef, handler: ActorRef, @@ -36,6 +38,7 @@ private[io] class UdpConnection(selectorRouter: ActorRef, datagramChannel.configureBlocking(false) val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) + // FIXME: All bind failures have to be reported to the commander in TCP as well localAddress foreach { socket.bind } // will blow up the actor constructor if the bind fails datagramChannel.connect(remoteAddress) datagramChannel @@ -46,13 +49,14 @@ private[io] class UdpConnection(selectorRouter: ActorRef, def receive = { case ChannelRegistered ⇒ bindCommander ! Connected + selector ! ReadInterest context.become(connected, discardOld = true) } def connected: Receive = { case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ doRead(handler) + case ChannelReadable ⇒ println("read"); doRead(handler) case Close ⇒ log.debug("Closing UDP connection to {}", remoteAddress) @@ -77,14 +81,17 @@ private[io] class UdpConnection(selectorRouter: ActorRef, } def doRead(handler: ActorRef): Unit = { - val buffer = bufferPool.acquire() - try { + @tailrec def innerRead(readsLeft: Int, buffer: ByteBuffer): Unit = { buffer.clear() buffer.limit(DirectBufferSize) - if (channel.read(buffer) > 0) handler ! Received(ByteString(buffer)) - - } finally { + if (channel.read(buffer) > 0) { + handler ! Received(ByteString(buffer)) + innerRead(readsLeft - 1, buffer) + } + } + val buffer = bufferPool.acquire() + try innerRead(BatchReceiveLimit, buffer) finally { selector ! ReadInterest bufferPool.release(buffer) } @@ -104,7 +111,6 @@ private[io] class UdpConnection(selectorRouter: ActorRef, // Datagram channel either sends the whole message, or nothing if (writtenBytes == 0) commander ! CommandFailed(send) else if (send.wantsAck) commander ! send.ack - } finally { udpConn.bufferPool.release(buffer) pendingSend = null diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index 51b1b7429c..f6d86f7053 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -34,7 +34,7 @@ object UdpFF extends ExtensionKey[UdpFFExt] { options: immutable.Traversable[SocketOption] = Nil) extends Command case object Unbind extends Command - case object SimpleSender extends Command + case class SimpleSender(options: immutable.Traversable[SocketOption] = Nil) extends Command case object StopReading extends Command case object ResumeReading extends Command diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index 3f464bccc5..aa8000eb0b 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -15,6 +15,8 @@ import scala.collection.immutable import scala.util.control.NonFatal import akka.io.UdpFF.Received import akka.io.SelectionHandler.RegisterChannel +import scala.annotation.tailrec +import java.nio.ByteBuffer private[io] class UdpFFListener(selectorRouter: ActorRef, handler: ActorRef, @@ -34,6 +36,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, datagramChannel.configureBlocking(false) val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) + // FIXME: signal bind failures socket.bind(endpoint) // will blow up the actor constructor if the bind fails datagramChannel } @@ -60,8 +63,7 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, } def doReceive(handler: ActorRef): Unit = { - val buffer = bufferPool.acquire() - try { + @tailrec def innerReceive(readsLeft: Int, buffer: ByteBuffer) { buffer.clear() buffer.limit(DirectBufferSize) @@ -69,11 +71,16 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, case sender: InetSocketAddress ⇒ buffer.flip() handler ! Received(ByteString(buffer), sender) - case _ ⇒ // Ignore + if (readsLeft > 0) innerReceive(readsLeft - 1, buffer) + case null ⇒ // null means no data was available } + } + val buffer = bufferPool.acquire() + try innerReceive(BatchReceiveLimit, buffer) finally { + bufferPool.release(buffer) selector ! ReadInterest - } finally bufferPool.release(buffer) + } } override def postStop() { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 3f5c677991..28c47ef995 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -44,17 +44,13 @@ import akka.io.UdpFF._ */ private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udpFF.settings, udpFF.settings.NrOfSelectors) { - // FIXME: fix close overs - lazy val anonymousSender: ActorRef = context.actorOf( - props = Props(new UdpFFSender(udpFF, selectorPool)), - name = "simplesend") - def receive = workerForCommand { case Bind(handler, endpoint, options) ⇒ val commander = sender Props(new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options)) - } orElse { - case SimpleSender ⇒ anonymousSender forward SimpleSender + case SimpleSender(options) ⇒ + val commander = sender + Props(new UdpFFSender(udpFF, options, commander)) } } diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala index 24e222e71c..5303aa79d9 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -7,27 +7,34 @@ import akka.actor._ import java.nio.channels.DatagramChannel import akka.io.UdpFF._ import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel } +import scala.collection.immutable +import akka.io.Inet.SocketOption /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. */ -private[io] class UdpFFSender(val udpFF: UdpFFExt, val selector: ActorRef) +private[io] class UdpFFSender(val udpFF: UdpFFExt, options: immutable.Traversable[SocketOption], val commander: ActorRef) extends Actor with ActorLogging with WithUdpFFSend { + def selector: ActorRef = context.parent + val channel = { val datagramChannel = DatagramChannel.open datagramChannel.configureBlocking(false) + val socket = datagramChannel.socket + + options foreach { o ⇒ + o.beforeDatagramBind(socket) + } + datagramChannel } selector ! RegisterChannel(channel, 0) def receive: Receive = { - case ChannelRegistered ⇒ context.become(simpleSendHandlers orElse sendHandlers, discardOld = true) - case _ ⇒ sender ! SimpleSendReady // FIXME: queueing here? - } - - def simpleSendHandlers: Receive = { - case SimpleSender ⇒ sender ! SimpleSendReady + case ChannelRegistered ⇒ + context.become(sendHandlers, discardOld = true) + commander ! SimpleSendReady } override def postStop(): Unit = if (channel.isOpen) channel.close() diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala index e04c4ca6f5..09dd37666c 100644 --- a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -12,6 +12,9 @@ private[io] trait WithUdpFFSend { me: Actor with ActorLogging ⇒ var pendingSend: (Send, ActorRef) = null + // If send fails first, we allow a second go after selected writable, but no more. This flag signals that + // pending send was already tried once. + var retriedSend = false def writePending = pendingSend ne null def selector: ActorRef @@ -33,7 +36,7 @@ private[io] trait WithUdpFFSend { case send: Send ⇒ pendingSend = (send, sender) - selector ! WriteInterest + doSend() case ChannelWritable ⇒ doSend() @@ -51,12 +54,19 @@ private[io] trait WithUdpFFSend { if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) // Datagram channel either sends the whole message, or nothing - if (writtenBytes == 0) commander ! CommandFailed(send) - else if (send.wantsAck) commander ! send.ack + if (writtenBytes == 0) { + if (retriedSend) { + commander ! CommandFailed(send) + retriedSend = false + pendingSend = null + } else { + selector ! WriteInterest + retriedSend = true + } + } else if (send.wantsAck) commander ! send.ack } finally { udpFF.bufferPool.release(buffer) - pendingSend = null } } From 6f0d0911a92d8fe2cc575a4fb201a50b1708f5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Wed, 6 Feb 2013 12:17:52 +0100 Subject: [PATCH 60/66] Bind failures are now explicitly reported instead of swallowed --- .../scala/akka/io/CapacityLimitSpec.scala | 2 +- .../scala/akka/io/TcpConnectionSpec.scala | 2 +- ...ionSpec.scala => TcpIntegrationSpec.scala} | 2 +- ....scala => TcpIntegrationSpecSupport.scala} | 2 +- .../test/scala/akka/io/TcpListenerSpec.scala | 3 +- .../src/main/scala/akka/io/TcpListener.scala | 20 +++++++----- .../src/main/scala/akka/io/TcpManager.scala | 8 ++--- .../scala/akka/io/TcpOutgoingConnection.scala | 15 ++++----- .../main/scala/akka/io/UdpConnManager.scala | 4 +-- .../main/scala/akka/io/UdpConnection.scala | 25 ++++++++------- .../main/scala/akka/io/UdpFFListener.scala | 32 +++++++++---------- .../src/main/scala/akka/io/UdpFFManager.scala | 6 ++-- 12 files changed, 63 insertions(+), 58 deletions(-) rename akka-actor-tests/src/test/scala/akka/io/{IntegrationSpec.scala => TcpIntegrationSpec.scala} (96%) rename akka-actor-tests/src/test/scala/akka/io/{IntegrationSpecSupport.scala => TcpIntegrationSpecSupport.scala} (97%) diff --git a/akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala b/akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala index a61962e223..600aed6114 100644 --- a/akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/CapacityLimitSpec.scala @@ -9,7 +9,7 @@ import Tcp._ import TestUtils._ class CapacityLimitSpec extends AkkaSpec("akka.loglevel = ERROR\nakka.io.tcp.max-channels = 4") - with IntegrationSpecSupport { + with TcpIntegrationSpecSupport { "The TCP transport implementation" should { diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index a59f2ccad4..a09dd68cdd 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -573,7 +573,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { TestActorRef( - new TcpOutgoingConnection(Tcp(system), commander, serverAddress, localAddress, options) { + new TcpOutgoingConnection(Tcp(system), commander, Connect(serverAddress, localAddress, options)) { override def postRestart(reason: Throwable) { // ensure we never restart context.stop(self) diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpec.scala similarity index 96% rename from akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala rename to akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpec.scala index e4d53f5f9b..9f35951ad9 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpec.scala @@ -11,7 +11,7 @@ import TestUtils._ import akka.testkit.EventFilter import java.io.IOException -class IntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with IntegrationSpecSupport { +class TcpIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with TcpIntegrationSpecSupport { "The TCP transport implementation" should { diff --git a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala b/akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpecSupport.scala similarity index 97% rename from akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala rename to akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpecSupport.scala index 692815b96a..4ed3bd9950 100644 --- a/akka-actor-tests/src/test/scala/akka/io/IntegrationSpecSupport.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpIntegrationSpecSupport.scala @@ -12,7 +12,7 @@ import akka.io.Inet.SocketOption import Tcp._ import TestUtils._ -trait IntegrationSpecSupport { _: AkkaSpec ⇒ +trait TcpIntegrationSpecSupport { _: AkkaSpec ⇒ class TestSetup { val bindHandler = TestProbe() diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index e68cc6536e..b04d07d7d8 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -97,8 +97,7 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { private class ListenerParent extends Actor { val listener = context.actorOf( - props = Props(new TcpListener(selectorRouter.ref, handler.ref, endpoint, 100, bindCommander.ref, - Tcp(system), Nil)), + props = Props(new TcpListener(selectorRouter.ref, Tcp(system), bindCommander.ref, Bind(handler.ref, endpoint, 100, Nil))), name = "test-listener-" + counter.next()) parent.watch(listener) def receive: Receive = { diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index 356319286f..51419fa0a5 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -25,17 +25,15 @@ private[io] object TcpListener { } -private[io] class TcpListener(selectorRouter: ActorRef, - handler: ActorRef, - endpoint: InetSocketAddress, - backlog: Int, - bindCommander: ActorRef, - tcp: TcpExt, - options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { +private[io] class TcpListener(val selectorRouter: ActorRef, + val tcp: TcpExt, + val bindCommander: ActorRef, + val bind: Bind) extends Actor with ActorLogging { def selector: ActorRef = context.parent import TcpListener._ import tcp.Settings._ + import bind._ context.watch(handler) // sign death pact val channel = { @@ -43,7 +41,13 @@ private[io] class TcpListener(selectorRouter: ActorRef, serverSocketChannel.configureBlocking(false) val socket = serverSocketChannel.socket options.foreach(_.beforeServerSocketBind(socket)) - socket.bind(endpoint, backlog) // will blow up the actor constructor if the bind fails + try socket.bind(endpoint, backlog) + catch { + case NonFatal(e) ⇒ + bindCommander ! CommandFailed(bind) + log.error(e, "Bind failed for TCP channel") + context.stop(self) + } serverSocketChannel } context.parent ! RegisterChannel(channel, SelectionKey.OP_ACCEPT) diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index 8761104ba5..032bcfd6bc 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -46,12 +46,12 @@ import akka.io.IO.SelectorBasedManager private[io] class TcpManager(tcp: TcpExt) extends SelectorBasedManager(tcp.Settings, tcp.Settings.NrOfSelectors) with ActorLogging { def receive = workerForCommand { - case Connect(remoteAddress, localAddress, options) ⇒ + case c: Connect ⇒ val commander = sender - Props(new TcpOutgoingConnection(tcp, commander, remoteAddress, localAddress, options)) - case Bind(handler, endpoint, backlog, options) ⇒ + Props(new TcpOutgoingConnection(tcp, commander, c)) + case b: Bind ⇒ val commander = sender - Props(new TcpListener(selectorPool, handler, endpoint, backlog, commander, tcp, options)) + Props(new TcpListener(selectorPool, tcp, commander, b)) } } diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 6c9231fa46..39817efe99 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -4,14 +4,13 @@ package akka.io -import java.net.InetSocketAddress +import akka.actor.ActorRef +import akka.io.Inet.SocketOption +import akka.io.SelectionHandler._ +import akka.io.Tcp._ import java.io.IOException import java.nio.channels.{ SelectionKey, SocketChannel } import scala.collection.immutable -import akka.actor.ActorRef -import akka.io.SelectionHandler._ -import akka.io.Inet.SocketOption -import akka.io.Tcp._ /** * An actor handling the connection state machine for an outgoing connection @@ -19,11 +18,11 @@ import akka.io.Tcp._ */ private[io] class TcpOutgoingConnection(_tcp: TcpExt, commander: ActorRef, - remoteAddress: InetSocketAddress, - localAddress: Option[InetSocketAddress], - options: immutable.Traversable[SocketOption]) + connect: Connect) extends TcpConnection(TcpOutgoingConnection.newSocketChannel(), _tcp) { + import connect._ + context.watch(commander) // sign death pact localAddress.foreach(channel.socket.bind) diff --git a/akka-actor/src/main/scala/akka/io/UdpConnManager.scala b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala index 8dbe806c7d..a93a21259d 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala @@ -10,9 +10,9 @@ import akka.io.UdpConn.Connect class UdpConnManager(udpConn: UdpConnExt) extends SelectorBasedManager(udpConn.settings, udpConn.settings.NrOfSelectors) { def receive = workerForCommand { - case Connect(handler, localAddress, remoteAddress, options) ⇒ + case c: Connect ⇒ val commander = sender - Props(new UdpConnection(selectorPool, handler, localAddress, remoteAddress, commander, udpConn, options)) + Props(new UdpConnection(udpConn, commander, c)) } } diff --git a/akka-actor/src/main/scala/akka/io/UdpConnection.scala b/akka-actor/src/main/scala/akka/io/UdpConnection.scala index 11a5a17f71..32e6d09bb6 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnection.scala @@ -16,17 +16,14 @@ import scala.util.control.NonFatal import java.nio.ByteBuffer import scala.annotation.tailrec -private[io] class UdpConnection(selectorRouter: ActorRef, - handler: ActorRef, - localAddress: Option[InetSocketAddress], - remoteAddress: InetSocketAddress, - bindCommander: ActorRef, - val udpConn: UdpConnExt, - options: immutable.Traversable[SocketOption]) extends Actor with ActorLogging { +private[io] class UdpConnection(val udpConn: UdpConnExt, + val commander: ActorRef, + val connect: Connect) extends Actor with ActorLogging { def selector: ActorRef = context.parent import udpConn._ + import connect._ import udpConn.settings._ var pendingSend: (Send, ActorRef) = null @@ -38,9 +35,15 @@ private[io] class UdpConnection(selectorRouter: ActorRef, datagramChannel.configureBlocking(false) val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) - // FIXME: All bind failures have to be reported to the commander in TCP as well - localAddress foreach { socket.bind } // will blow up the actor constructor if the bind fails - datagramChannel.connect(remoteAddress) + try { + localAddress foreach { socket.bind } // will blow up the actor constructor if the bind fails + datagramChannel.connect(remoteAddress) + } catch { + case NonFatal(e) ⇒ + log.error(e, "Failure while connecting UDP channel") + commander ! CommandFailed(connect) + context.stop(self) + } datagramChannel } selector ! RegisterChannel(channel, OP_READ) @@ -48,7 +51,7 @@ private[io] class UdpConnection(selectorRouter: ActorRef, def receive = { case ChannelRegistered ⇒ - bindCommander ! Connected + commander ! Connected selector ! ReadInterest context.become(connected, discardOld = true) } diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index aa8000eb0b..0edccefab3 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -4,29 +4,24 @@ package akka.io import akka.actor.{ ActorLogging, Actor, ActorRef } -import akka.io.UdpFF._ -import akka.io.Inet.SocketOption import akka.io.SelectionHandler._ +import akka.io.UdpFF._ import akka.util.ByteString import java.net.InetSocketAddress +import java.nio.ByteBuffer import java.nio.channels.DatagramChannel import java.nio.channels.SelectionKey._ -import scala.collection.immutable -import scala.util.control.NonFatal -import akka.io.UdpFF.Received -import akka.io.SelectionHandler.RegisterChannel import scala.annotation.tailrec -import java.nio.ByteBuffer +import scala.util.control.NonFatal -private[io] class UdpFFListener(selectorRouter: ActorRef, - handler: ActorRef, - endpoint: InetSocketAddress, - bindCommander: ActorRef, - val udpFF: UdpFFExt, - options: immutable.Traversable[SocketOption]) +private[io] class UdpFFListener(val udpFF: UdpFFExt, + val bindCommander: ActorRef, + val bind: Bind) extends Actor with ActorLogging with WithUdpFFSend { - import udpFF.settings._ + + import bind._ import udpFF.bufferPool + import udpFF.settings._ def selector: ActorRef = context.parent @@ -36,8 +31,13 @@ private[io] class UdpFFListener(selectorRouter: ActorRef, datagramChannel.configureBlocking(false) val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) - // FIXME: signal bind failures - socket.bind(endpoint) // will blow up the actor constructor if the bind fails + try socket.bind(endpoint) + catch { + case NonFatal(e) ⇒ + bindCommander ! CommandFailed(bind) + log.error(e, "Failed to bind UDP channel") + context.stop(self) + } datagramChannel } context.parent ! RegisterChannel(channel, OP_READ) diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 28c47ef995..8e3e03617f 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -3,7 +3,7 @@ */ package akka.io -import akka.actor.{ ActorRef, Props } +import akka.actor.Props import akka.io.IO.SelectorBasedManager import akka.io.UdpFF._ @@ -45,9 +45,9 @@ import akka.io.UdpFF._ private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udpFF.settings, udpFF.settings.NrOfSelectors) { def receive = workerForCommand { - case Bind(handler, endpoint, options) ⇒ + case b: Bind ⇒ val commander = sender - Props(new UdpFFListener(selectorPool, handler, endpoint, commander, udpFF, options)) + Props(new UdpFFListener(udpFF, commander, b)) case SimpleSender(options) ⇒ val commander = sender Props(new UdpFFSender(udpFF, options, commander)) From 91d798cee18993ff817e0fc578310a3a0da0d4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Wed, 6 Feb 2013 17:45:42 +0100 Subject: [PATCH 61/66] Updated tests to work (or be disabled) on Win --- .../scala/akka/io/TcpConnectionSpec.scala | 134 ++++++++++++------ .../test/scala/akka/io/TcpListenerSpec.scala | 34 +++-- .../main/scala/akka/io/TcpConnection.scala | 2 +- 3 files changed, 111 insertions(+), 59 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index a09dd68cdd..1e80f84fa1 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -5,7 +5,7 @@ package akka.io import java.io.IOException -import java.net.{ ConnectException, InetSocketAddress, SocketException } +import java.net.{ Socket, ConnectException, InetSocketAddress, SocketException } import java.nio.ByteBuffer import java.nio.channels.{ SelectionKey, Selector, ServerSocketChannel, SocketChannel } import java.nio.channels.spi.SelectorProvider @@ -27,6 +27,12 @@ import akka.io.Inet.SocketOption class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") { val serverAddress = temporaryServerAddress() + // Helper to avoid Windows localization specific differences + def nonWindows(body: ⇒ Any): Unit = { + if (!System.getProperty("os.name").toLowerCase().contains("win")) body + else log.warning("Detected Windows: ignoring check") + } + "An outgoing connection" must { // common behavior @@ -39,16 +45,17 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") clientChannel.socket.getReuseAddress must be(true) } - "set socket options after connecting" in withLocalServer() { localServer ⇒ + "set socket options after connecting" ignore withLocalServer() { localServer ⇒ + // Workaround for systems where SO_KEEPALIVE is true by default val userHandler = TestProbe() val selector = TestProbe() val connectionActor = - createConnectionActor(options = Vector(SO.KeepAlive(true)))(selector.ref, userHandler.ref) + createConnectionActor(options = Vector(SO.KeepAlive(false)))(selector.ref, userHandler.ref) val clientChannel = connectionActor.underlyingActor.channel - clientChannel.socket.getKeepAlive must be(false) // only set after connection is established + clientChannel.socket.getKeepAlive must be(true) // only set after connection is established EventFilter.warning(pattern = "registration timeout", occurrences = 1) intercept { selector.send(connectionActor, ChannelConnectable) - clientChannel.socket.getKeepAlive must be(true) + clientChannel.socket.getKeepAlive must be(false) } } @@ -146,45 +153,62 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") writer.expectMsg(Ack) } + /* + * Disabled on Windows: http://support.microsoft.com/kb/214397 + * + * "To optimize performance at the application layer, Winsock copies data buffers from application send calls + * to a Winsock kernel buffer. Then, the stack uses its own heuristics (such as Nagle algorithm) to determine + * when to actually put the packet on the wire. You can change the amount of Winsock kernel buffer allocated to + * the socket using the SO_SNDBUF option (it is 8K by default). If necessary, Winsock can buffer significantly more + * than the SO_SNDBUF buffer size. In most cases, the send completion in the application only indicates the data + * buffer in an application send call is copied to the Winsock kernel buffer and does not indicate that the data + * has hit the network medium. The only exception is when you disable the Winsock buffering by setting + * SO_SNDBUF to 0." + */ "stop writing in cases of backpressure and resume afterwards" in - withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ - import setup._ - object Ack1 - object Ack2 + nonWindows { + withEstablishedConnection( + clientSocketOptions = List(SO.ReceiveBufferSize(1000000))) { setup ⇒ + import setup._ + object Ack1 + object Ack2 - clientSideChannel.socket.setSendBufferSize(1024) + clientSideChannel.socket.setSendBufferSize(1024) - val writer = TestProbe() + awaitCond(clientSideChannel.socket.getSendBufferSize == 1024) - // producing backpressure by sending much more than currently fits into - // our send buffer - val firstWrite = writeCmd(Ack1) + val writer = TestProbe() - // try to write the buffer but since the SO_SNDBUF is too small - // it will have to keep the rest of the piece and send it - // when possible - writer.send(connectionActor, firstWrite) - selector.expectMsg(WriteInterest) + // producing backpressure by sending much more than currently fits into + // our send buffer + val firstWrite = writeCmd(Ack1) - // send another write which should fail immediately - // because we don't store more than one piece in flight - val secondWrite = writeCmd(Ack2) - writer.send(connectionActor, secondWrite) - writer.expectMsg(CommandFailed(secondWrite)) + // try to write the buffer but since the SO_SNDBUF is too small + // it will have to keep the rest of the piece and send it + // when possible + writer.send(connectionActor, firstWrite) + selector.expectMsg(WriteInterest) - // reject even empty writes - writer.send(connectionActor, Write.Empty) - writer.expectMsg(CommandFailed(Write.Empty)) + // send another write which should fail immediately + // because we don't store more than one piece in flight + val secondWrite = writeCmd(Ack2) + writer.send(connectionActor, secondWrite) + writer.expectMsg(CommandFailed(secondWrite)) - // there will be immediately more space in the send buffer because - // some data will have been sent by now, so we assume we can write - // again, but still it can't write everything - selector.send(connectionActor, ChannelWritable) + // reject even empty writes + writer.send(connectionActor, Write.Empty) + writer.expectMsg(CommandFailed(Write.Empty)) - // both buffers should now be filled so no more writing - // is possible - pullFromServerSide(TestSize) - writer.expectMsg(Ack1) + // there will be immediately more space in the send buffer because + // some data will have been sent by now, so we assume we can write + // again, but still it can't write everything + selector.send(connectionActor, ChannelWritable) + + // both buffers should now be filled so no more writing + // is possible + pullFromServerSide(TestSize) + writer.expectMsg(Ack1) + } } "respect StopReading and ResumeReading" in withEstablishedConnection() { setup ⇒ @@ -193,7 +217,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") // the selector interprets StopReading to deregister interest // for reading - selector.expectMsg(StopReading) + selector.expectMsg(DisableReadInterest) connectionHandler.send(connectionActor, ResumeReading) selector.expectMsg(ReadInterest) } @@ -242,10 +266,21 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val buffer = ByteBuffer.allocate(1) val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException] - // FIXME: On windows this message is localized - //thrown.getMessage must be("Connection reset by peer") + nonWindows { thrown.getMessage must be("Connection reset by peer") } } + /* + * Partly disabled on Windows: http://support.microsoft.com/kb/214397 + * + * "To optimize performance at the application layer, Winsock copies data buffers from application send calls + * to a Winsock kernel buffer. Then, the stack uses its own heuristics (such as Nagle algorithm) to determine + * when to actually put the packet on the wire. You can change the amount of Winsock kernel buffer allocated to + * the socket using the SO_SNDBUF option (it is 8K by default). If necessary, Winsock can buffer significantly more + * than the SO_SNDBUF buffer size. In most cases, the send completion in the application only indicates the data + * buffer in an application send call is copied to the Winsock kernel buffer and does not indicate that the data + * has hit the network medium. The only exception is when you disable the Winsock buffering by setting + * SO_SNDBUF to 0." + */ "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ import setup._ @@ -259,12 +294,12 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.send(connectionActor, writeCmd(Ack)) connectionHandler.send(connectionActor, ConfirmedClose) - connectionHandler.expectNoMsg(100.millis) + nonWindows { connectionHandler.expectNoMsg(100.millis) } pullFromServerSide(TestSize) connectionHandler.expectMsg(Ack) selector.send(connectionActor, ChannelReadable) - connectionHandler.expectNoMsg(100.millis) // not yet + nonWindows { connectionHandler.expectNoMsg(100.millis) } // not yet val buffer = ByteBuffer.allocate(1) serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds)) @@ -292,7 +327,8 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") EventFilter[IOException](occurrences = 1) intercept { abortClose(serverSideChannel) selector.send(connectionActor, ChannelReadable) - connectionHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + val err = connectionHandler.expectMsgType[ErrorClosed] + nonWindows { err.cause must be("Connection reset by peer") } } // wait a while connectionHandler.expectNoMsg(200.millis) @@ -316,14 +352,15 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } // error conditions - "report failed connection attempt while not accepted" in withUnacceptedConnection() { setup ⇒ + "report failed connection attempt while not accepted" ignore withUnacceptedConnection() { setup ⇒ import setup._ // close instead of accept localServer.close() EventFilter[SocketException](occurrences = 1) intercept { selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClosed].cause must be("Connection reset by peer") + val err = userHandler.expectMsgType[ErrorClosed] + nonWindows { err.cause must be("Connection reset by peer") } } verifyActorTermination(connectionActor) @@ -336,12 +373,14 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val sel = SelectorProvider.provider().openSelector() val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) - sel.select(200) + // This timeout should be large enough to work on Windows + sel.select(3000) key.isConnectable must be(true) EventFilter[ConnectException](occurrences = 1) intercept { selector.send(connectionActor, ChannelConnectable) - userHandler.expectMsgType[ErrorClosed].cause must be("Connection refused") + val err = userHandler.expectMsgType[ErrorClosed] + nonWindows { err.cause must be("Connection refused") } } verifyActorTermination(connectionActor) @@ -572,7 +611,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") _selector: ActorRef, commander: ActorRef): TestActorRef[TcpOutgoingConnection] = { - TestActorRef( + val ref = TestActorRef( new TcpOutgoingConnection(Tcp(system), commander, Connect(serverAddress, localAddress, options)) { override def postRestart(reason: Throwable) { // ensure we never restart @@ -580,6 +619,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") } override def selector = _selector }) + + ref ! ChannelRegistered + ref } def abortClose(channel: SocketChannel): Unit = { diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala index b04d07d7d8..09ed457959 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala @@ -12,6 +12,7 @@ import Tcp._ import akka.testkit.EventFilter import akka.io.SelectionHandler._ import java.nio.channels.SelectionKey._ +import akka.io.TcpListener.{ RegisterIncoming, FailedRegisterIncoming } class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { @@ -31,18 +32,25 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { attemptConnectionToEndpoint() attemptConnectionToEndpoint() + def expectWorkerForCommand: Unit = { + selectorRouter.expectMsgPF() { + case WorkerForCommand(RegisterIncoming(chan), commander, _) ⇒ + chan.isOpen must be(true) + commander must be === listener + } + } + // since the batch-accept-limit is 2 we must only receive 2 accepted connections listener ! ChannelAcceptable parent.expectMsg(AcceptInterest) - // FIXME: ugly stuff here - selectorRouter.expectMsgType[WorkerForCommand] - selectorRouter.expectMsgType[WorkerForCommand] + expectWorkerForCommand + expectWorkerForCommand selectorRouter.expectNoMsg(100.millis) // and pick up the last remaining connection on the next ChannelAcceptable listener ! ChannelAcceptable - selectorRouter.expectMsgType[WorkerForCommand] + expectWorkerForCommand } "react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup { @@ -61,15 +69,17 @@ class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") { attemptConnectionToEndpoint() listener ! ChannelAcceptable - val props = selectorRouter.expectMsgType[WorkerForCommand].childProps - // FIXME: need to instantiate propss - //selectorRouter.expectMsgType[RegisterChannel].channel.isOpen must be(true) + val channel = selectorRouter.expectMsgPF() { + case WorkerForCommand(RegisterIncoming(chan), commander, _) ⇒ + chan.isOpen must be(true) + commander must be === listener + chan + } - // FIXME: fix this - // EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept { - // //listener ! CommandFailed(RegisterIncomingConnection(channel, handler.ref, Nil)) - // awaitCond(!channel.isOpen) - // } + EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept { + listener ! FailedRegisterIncoming(channel) + awaitCond(!channel.isOpen) + } } } diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index dffec75307..44e7b29163 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -76,7 +76,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, pendingWrite = createWrite(write) doWrite(handler) - case ChannelWritable ⇒ doWrite(handler) + case ChannelWritable ⇒ if (writePending) doWrite(handler) case cmd: CloseCommand ⇒ handleClose(handler, Some(sender), closeResponse(cmd)) } From 74396246ce35e4c4c9be75080c4fcaa5201a6b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Thu, 7 Feb 2013 17:54:42 +0100 Subject: [PATCH 62/66] Connection based UDP fixed - SelectionHandler unregisters only selected interests, not all. - Fixed tests to be properly ignored on Windows --- .../scala/akka/io/TcpConnectionSpec.scala | 179 +++++++++++++----- .../akka/io/UdpConnIntegrationSpec.scala | 2 - .../scala/akka/io/UdpFFIntegrationSpec.scala | 8 +- .../main/scala/akka/io/SelectionHandler.scala | 6 +- .../main/scala/akka/io/UdpConnection.scala | 15 +- 5 files changed, 146 insertions(+), 64 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index 1e80f84fa1..c7658d96dc 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -19,7 +19,7 @@ import akka.io.SelectionHandler._ import TestUtils._ import akka.actor.{ ActorRef, PoisonPill, Terminated } import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe } -import akka.util.ByteString +import akka.util.{ Helpers, ByteString } import akka.actor.DeathPactException import java.nio.channels.SelectionKey._ import akka.io.Inet.SocketOption @@ -28,9 +28,11 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val serverAddress = temporaryServerAddress() // Helper to avoid Windows localization specific differences - def nonWindows(body: ⇒ Any): Unit = { - if (!System.getProperty("os.name").toLowerCase().contains("win")) body - else log.warning("Detected Windows: ignoring check") + def ignoreIfWindows(): Unit = { + if (Helpers.isWindows) { + info("Detected Windows: ignoring check") + pending + } } "An outgoing connection" must { @@ -166,49 +168,47 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") * SO_SNDBUF to 0." */ "stop writing in cases of backpressure and resume afterwards" in - nonWindows { - withEstablishedConnection( - clientSocketOptions = List(SO.ReceiveBufferSize(1000000))) { setup ⇒ - import setup._ - object Ack1 - object Ack2 + withEstablishedConnection(clientSocketOptions = List(SO.ReceiveBufferSize(1000000))) { setup ⇒ + ignoreIfWindows() + import setup._ + object Ack1 + object Ack2 - clientSideChannel.socket.setSendBufferSize(1024) + clientSideChannel.socket.setSendBufferSize(1024) - awaitCond(clientSideChannel.socket.getSendBufferSize == 1024) + awaitCond(clientSideChannel.socket.getSendBufferSize == 1024) - val writer = TestProbe() + val writer = TestProbe() - // producing backpressure by sending much more than currently fits into - // our send buffer - val firstWrite = writeCmd(Ack1) + // producing backpressure by sending much more than currently fits into + // our send buffer + val firstWrite = writeCmd(Ack1) - // try to write the buffer but since the SO_SNDBUF is too small - // it will have to keep the rest of the piece and send it - // when possible - writer.send(connectionActor, firstWrite) - selector.expectMsg(WriteInterest) + // try to write the buffer but since the SO_SNDBUF is too small + // it will have to keep the rest of the piece and send it + // when possible + writer.send(connectionActor, firstWrite) + selector.expectMsg(WriteInterest) - // send another write which should fail immediately - // because we don't store more than one piece in flight - val secondWrite = writeCmd(Ack2) - writer.send(connectionActor, secondWrite) - writer.expectMsg(CommandFailed(secondWrite)) + // send another write which should fail immediately + // because we don't store more than one piece in flight + val secondWrite = writeCmd(Ack2) + writer.send(connectionActor, secondWrite) + writer.expectMsg(CommandFailed(secondWrite)) - // reject even empty writes - writer.send(connectionActor, Write.Empty) - writer.expectMsg(CommandFailed(Write.Empty)) + // reject even empty writes + writer.send(connectionActor, Write.Empty) + writer.expectMsg(CommandFailed(Write.Empty)) - // there will be immediately more space in the send buffer because - // some data will have been sent by now, so we assume we can write - // again, but still it can't write everything - selector.send(connectionActor, ChannelWritable) + // there will be immediately more space in the send buffer because + // some data will have been sent by now, so we assume we can write + // again, but still it can't write everything + selector.send(connectionActor, ChannelWritable) - // both buffers should now be filled so no more writing - // is possible - pullFromServerSide(TestSize) - writer.expectMsg(Ack1) - } + // both buffers should now be filled so no more writing + // is possible + pullFromServerSide(TestSize) + writer.expectMsg(Ack1) } "respect StopReading and ResumeReading" in withEstablishedConnection() { setup ⇒ @@ -256,7 +256,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.expectNoMsg(500.millis) } - "abort the connection and reply with `Aborted` upong reception of an `Abort` command" in withEstablishedConnection() { setup ⇒ + "abort the connection and reply with `Aborted` upong reception of an `Abort` command (simplified)" in withEstablishedConnection() { setup ⇒ import setup._ connectionHandler.send(connectionActor, Abort) @@ -266,7 +266,20 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") val buffer = ByteBuffer.allocate(1) val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException] - nonWindows { thrown.getMessage must be("Connection reset by peer") } + } + + "abort the connection and reply with `Aborted` upong reception of an `Abort` command" in withEstablishedConnection() { setup ⇒ + ignoreIfWindows() + import setup._ + + connectionHandler.send(connectionActor, Abort) + connectionHandler.expectMsg(Aborted) + + assertThisConnectionActorTerminated() + + val buffer = ByteBuffer.allocate(1) + val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException] + thrown.getMessage must be("Connection reset by peer") } /* @@ -281,7 +294,7 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") * has hit the network medium. The only exception is when you disable the Winsock buffering by setting * SO_SNDBUF to 0." */ - "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command (simplified)" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ import setup._ // we should test here that a pending write command is properly finished first @@ -294,12 +307,42 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") connectionHandler.send(connectionActor, writeCmd(Ack)) connectionHandler.send(connectionActor, ConfirmedClose) - nonWindows { connectionHandler.expectNoMsg(100.millis) } pullFromServerSide(TestSize) connectionHandler.expectMsg(Ack) selector.send(connectionActor, ChannelReadable) - nonWindows { connectionHandler.expectNoMsg(100.millis) } // not yet + + val buffer = ByteBuffer.allocate(1) + serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds)) + serverSideChannel.read(buffer) must be(-1) + serverSideChannel.close() + + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectMsg(ConfirmedClosed) + + assertThisConnectionActorTerminated() + } + + "close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒ + ignoreIfWindows() + import setup._ + + // we should test here that a pending write command is properly finished first + object Ack + // set an artificially small send buffer size so that the write is queued + // inside the connection actor + clientSideChannel.socket.setSendBufferSize(1024) + + // we send a write and a close command directly afterwards + connectionHandler.send(connectionActor, writeCmd(Ack)) + connectionHandler.send(connectionActor, ConfirmedClose) + + connectionHandler.expectNoMsg(100.millis) + pullFromServerSide(TestSize) + connectionHandler.expectMsg(Ack) + + selector.send(connectionActor, ChannelReadable) + connectionHandler.expectNoMsg(100.millis) // not yet val buffer = ByteBuffer.allocate(1) serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds)) @@ -321,14 +364,29 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertThisConnectionActorTerminated() } - "report when peer aborted the connection" in withEstablishedConnection() { setup ⇒ + "report when peer aborted the connection (simplified)" in withEstablishedConnection() { setup ⇒ import setup._ EventFilter[IOException](occurrences = 1) intercept { abortClose(serverSideChannel) selector.send(connectionActor, ChannelReadable) val err = connectionHandler.expectMsgType[ErrorClosed] - nonWindows { err.cause must be("Connection reset by peer") } + } + // wait a while + connectionHandler.expectNoMsg(200.millis) + + assertThisConnectionActorTerminated() + } + + "report when peer aborted the connection" in withEstablishedConnection() { setup ⇒ + import setup._ + ignoreIfWindows() + + EventFilter[IOException](occurrences = 1) intercept { + abortClose(serverSideChannel) + selector.send(connectionActor, ChannelReadable) + val err = connectionHandler.expectMsgType[ErrorClosed] + err.cause must be("Connection reset by peer") } // wait a while connectionHandler.expectNoMsg(200.millis) @@ -351,23 +409,25 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") assertThisConnectionActorTerminated() } - // error conditions - "report failed connection attempt while not accepted" ignore withUnacceptedConnection() { setup ⇒ + // This tets is disabled on windows, as the assumption that not calling accept on a server socket means that + // no TCP level connection has been established with the client does not hold. + "report failed connection attempt while not accepted" in withUnacceptedConnection() { setup ⇒ import setup._ + ignoreIfWindows() // close instead of accept localServer.close() EventFilter[SocketException](occurrences = 1) intercept { selector.send(connectionActor, ChannelConnectable) val err = userHandler.expectMsgType[ErrorClosed] - nonWindows { err.cause must be("Connection reset by peer") } + err.cause must be("Connection reset by peer") } verifyActorTermination(connectionActor) } val UnboundAddress = temporaryServerAddress() - "report failed connection attempt when target is unreachable" in + "report failed connection attempt when target is unreachable (simplified)" in withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ import setup._ @@ -380,7 +440,26 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") EventFilter[ConnectException](occurrences = 1) intercept { selector.send(connectionActor, ChannelConnectable) val err = userHandler.expectMsgType[ErrorClosed] - nonWindows { err.cause must be("Connection refused") } + } + + verifyActorTermination(connectionActor) + } + + "report failed connection attempt when target is unreachable" in + withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒ + import setup._ + ignoreIfWindows() + + val sel = SelectorProvider.provider().openSelector() + val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ) + // This timeout should be large enough to work on Windows + sel.select(3000) + + key.isConnectable must be(true) + EventFilter[ConnectException](occurrences = 1) intercept { + selector.send(connectionActor, ChannelConnectable) + val err = userHandler.expectMsgType[ErrorClosed] + err.cause must be("Connection refused") } verifyActorTermination(connectionActor) diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala index ece503de33..91742b8860 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpConnIntegrationSpec.scala @@ -40,8 +40,6 @@ class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Impli a } - println(clientAddress) - server ! UdpFF.Send(data2, clientAddress) // FIXME: Currently this line fails diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala index a0f138f041..270711cc76 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala @@ -41,7 +41,7 @@ class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Implici } "be able to send with binding" in { - val (serverAddress, _) = bindUdp(testActor) + val (serverAddress, server) = bindUdp(testActor) val (clientAddress, client) = bindUdp(testActor) val data = ByteString("Fly little packet!") @@ -52,6 +52,12 @@ class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Implici d must be === data a must be === clientAddress } + server ! Send(data, clientAddress) + expectMsgPF() { + case Received(d, a) ⇒ + d must be === data + a must be === serverAddress + } } } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index e7046d8b34..57c02f0d25 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -188,9 +188,11 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler while (iterator.hasNext) { val key = iterator.next if (key.isValid) { - key.interestOps(0) // prevent immediate reselection by always clearing + // Cache because the performance implications of calling this on different platforms are not clear + val readyOps = key.readyOps() + key.interestOps(key.interestOps & ~readyOps) // prevent immediate reselection by always clearing val connection = key.attachment.asInstanceOf[ActorRef] - key.readyOps match { + readyOps match { case OP_READ ⇒ connection ! ChannelReadable case OP_WRITE ⇒ connection ! ChannelWritable case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable diff --git a/akka-actor/src/main/scala/akka/io/UdpConnection.scala b/akka-actor/src/main/scala/akka/io/UdpConnection.scala index 32e6d09bb6..ff91a464fa 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnection.scala @@ -4,17 +4,14 @@ package akka.io import akka.actor.{ Actor, ActorLogging, ActorRef } -import akka.io.Inet.SocketOption import akka.io.SelectionHandler._ import akka.io.UdpConn._ import akka.util.ByteString -import java.net.InetSocketAddress +import java.nio.ByteBuffer import java.nio.channels.DatagramChannel import java.nio.channels.SelectionKey._ -import scala.collection.immutable -import scala.util.control.NonFatal -import java.nio.ByteBuffer import scala.annotation.tailrec +import scala.util.control.NonFatal private[io] class UdpConnection(val udpConn: UdpConnExt, val commander: ActorRef, @@ -22,8 +19,8 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, def selector: ActorRef = context.parent - import udpConn._ import connect._ + import udpConn._ import udpConn.settings._ var pendingSend: (Send, ActorRef) = null @@ -36,7 +33,7 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) try { - localAddress foreach { socket.bind } // will blow up the actor constructor if the bind fails + localAddress.foreach { socket.bind _ } // will blow up the actor constructor if the bind fails datagramChannel.connect(remoteAddress) } catch { case NonFatal(e) ⇒ @@ -52,14 +49,13 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, def receive = { case ChannelRegistered ⇒ commander ! Connected - selector ! ReadInterest context.become(connected, discardOld = true) } def connected: Receive = { case StopReading ⇒ selector ! DisableReadInterest case ResumeReading ⇒ selector ! ReadInterest - case ChannelReadable ⇒ println("read"); doRead(handler) + case ChannelReadable ⇒ doRead(handler) case Close ⇒ log.debug("Closing UDP connection to {}", remoteAddress) @@ -89,6 +85,7 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, buffer.limit(DirectBufferSize) if (channel.read(buffer) > 0) { + buffer.flip() handler ! Received(ByteString(buffer)) innerRead(readsLeft - 1, buffer) } From e9da097621105e72f098a77db50b1acef12d5604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Sun, 10 Feb 2013 13:52:52 +0100 Subject: [PATCH 63/66] Fixed according to revew comments --- .../scala/akka/io/TcpConnectionSpec.scala | 5 +++- .../scala/akka/io/UdpFFIntegrationSpec.scala | 6 ++--- akka-actor/src/main/resources/reference.conf | 12 ++++----- akka-actor/src/main/scala/akka/io/IO.scala | 2 +- .../main/scala/akka/io/SelectionHandler.scala | 27 ++++++++++--------- akka-actor/src/main/scala/akka/io/Tcp.scala | 8 +++--- .../main/scala/akka/io/TcpConnection.scala | 12 ++++----- .../src/main/scala/akka/io/TcpListener.scala | 2 +- .../src/main/scala/akka/io/TcpManager.scala | 2 +- .../scala/akka/io/TcpOutgoingConnection.scala | 2 +- akka-actor/src/main/scala/akka/io/Udp.scala | 4 +-- .../src/main/scala/akka/io/UdpConn.scala | 6 ++--- .../main/scala/akka/io/UdpConnManager.scala | 2 +- .../main/scala/akka/io/UdpConnection.scala | 13 ++++----- akka-actor/src/main/scala/akka/io/UdpFF.scala | 6 ++--- .../main/scala/akka/io/UdpFFListener.scala | 15 ++++++----- .../src/main/scala/akka/io/UdpFFManager.scala | 2 +- .../src/main/scala/akka/io/UdpFFSender.scala | 13 ++++++--- .../main/scala/akka/io/WithUdpFFSend.scala | 24 +++++++++-------- 19 files changed, 87 insertions(+), 76 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index c7658d96dc..cfd46d77c6 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -14,7 +14,7 @@ import scala.collection.immutable import scala.concurrent.duration._ import scala.util.control.NonFatal import org.scalatest.matchers._ -import Tcp._ +import akka.io.Tcp._ import akka.io.SelectionHandler._ import TestUtils._ import akka.actor.{ ActorRef, PoisonPill, Terminated } @@ -169,6 +169,9 @@ class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") */ "stop writing in cases of backpressure and resume afterwards" in withEstablishedConnection(clientSocketOptions = List(SO.ReceiveBufferSize(1000000))) { setup ⇒ + info("Currently ignored as SO_SNDBUF is usually a lower bound on the send buffer so the test fails as no real " + + "backpressure present.") + pending ignoreIfWindows() import setup._ object Ack1 diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala index 270711cc76..6cb6042b2f 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala @@ -34,10 +34,8 @@ class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Implici val data = ByteString("To infinity and beyond!") simpleSender ! Send(data, serverAddress) - expectMsgPF() { - case Received(d, _) ⇒ - d must be === data - } + expectMsgType[Received].data must be === data + } "be able to send with binding" in { diff --git a/akka-actor/src/main/resources/reference.conf b/akka-actor/src/main/resources/reference.conf index c17594a307..fd5051deb8 100644 --- a/akka-actor/src/main/resources/reference.conf +++ b/akka-actor/src/main/resources/reference.conf @@ -416,7 +416,7 @@ akka { # The maximal number of direct buffers kept in the direct buffer pool for # reuse. - max-direct-buffer-pool-size = 1000 + direct-buffer-pool-limit = 1000 # The duration a connection actor waits for a `Register` message from # its commander before aborting the connection. @@ -425,7 +425,7 @@ akka { # The maximum number of bytes delivered by a `Received` message. Before # more data is read from the network the connection actor will try to # do other work. - received-message-size-limit = unlimited + max-received-message-size = unlimited # Enable fine grained logging of what goes on inside the implementation. # Be aware that this may log more than once per message sent to the actors @@ -473,7 +473,7 @@ akka { # The maximum number of datagrams that are read in one go, # higher numbers decrease latency, lower numbers increase fairness on # the worker-dispatcher - batch-receive-limit = 3 + receive-throughput = 3 # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. @@ -481,7 +481,7 @@ akka { # The maximal number of direct buffers kept in the direct buffer pool for # reuse. - max-direct-buffer-pool-size = 1000 + direct-buffer-pool-limit = 1000 # The maximum number of bytes delivered by a `Received` message. Before # more data is read from the network the connection actor will try to @@ -534,7 +534,7 @@ akka { # The maximum number of datagrams that are read in one go, # higher numbers decrease latency, lower numbers increase fairness on # the worker-dispatcher - batch-receive-limit = 3 + receive-throughput = 3 # The number of bytes per direct buffer in the pool used to read or write # network data from the kernel. @@ -542,7 +542,7 @@ akka { # The maximal number of direct buffers kept in the direct buffer pool for # reuse. - max-direct-buffer-pool-size = 1000 + direct-buffer-pool-limit = 1000 # The maximum number of bytes delivered by a `Received` message. Before # more data is read from the network the connection actor will try to diff --git a/akka-actor/src/main/scala/akka/io/IO.scala b/akka-actor/src/main/scala/akka/io/IO.scala index 5b10023990..e238ffbaf2 100644 --- a/akka-actor/src/main/scala/akka/io/IO.scala +++ b/akka-actor/src/main/scala/akka/io/IO.scala @@ -33,7 +33,7 @@ object IO { WorkerForCommand(cmd, commander, props) } - def workerForCommand(pf: PartialFunction[Any, Props]): Receive = { + def workerForCommandHandler(pf: PartialFunction[Any, Props]): Receive = { case cmd: HasFailureMessage if pf.isDefinedAt(cmd) ⇒ selectorPool ! createWorkerMessage(pf)(cmd) } } diff --git a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala index 57c02f0d25..0d1c0d439e 100644 --- a/akka-actor/src/main/scala/akka/io/SelectionHandler.scala +++ b/akka-actor/src/main/scala/akka/io/SelectionHandler.scala @@ -94,15 +94,16 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler override def postStop() { try { - val iterator = selector.keys.iterator - while (iterator.hasNext) { - val key = iterator.next() - try key.channel.close() - catch { - case NonFatal(e) ⇒ log.error(e, "Error closing channel") + try { + val iterator = selector.keys.iterator + while (iterator.hasNext) { + val key = iterator.next() + try key.channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing channel") + } } - } - selector.close() + } finally selector.close() } catch { case NonFatal(e) ⇒ log.error(e, "Error closing selector") } @@ -112,11 +113,11 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler override def supervisorStrategy = SupervisorStrategy.stoppingStrategy def withCapacityProtection(cmd: WorkerForCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = { - log.debug("Executing {}", cmd) + log.debug("Executing [{}]", cmd) if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) { body } else { - log.warning("Rejecting '{}' with {} retries left, retrying...", cmd, retriesLeft) + log.warning("Rejecting [{}] with [{}] retries left, retrying...", cmd, retriesLeft) context.parent forward Retry(cmd, retriesLeft - 1) } } @@ -198,9 +199,9 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable case x if (x & OP_ACCEPT) > 0 ⇒ connection ! ChannelAcceptable case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable - case x ⇒ log.warning("Invalid readyOps: {}", x) + case x ⇒ log.warning("Invalid readyOps: [{}]", x) } - } else log.warning("Invalid selection key: {}", key) + } else log.warning("Invalid selection key: [{}]", key) } keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected } @@ -217,7 +218,7 @@ private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandler try tryRun() catch { case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown - case NonFatal(e) ⇒ log.error(e, "Error during selector management task: {}", e) + case NonFatal(e) ⇒ log.error(e, "Error during selector management task: [{}]", e) } } } diff --git a/akka-actor/src/main/scala/akka/io/Tcp.scala b/akka-actor/src/main/scala/akka/io/Tcp.scala index 95218f16cf..97076477cf 100644 --- a/akka-actor/src/main/scala/akka/io/Tcp.scala +++ b/akka-actor/src/main/scala/akka/io/Tcp.scala @@ -16,7 +16,7 @@ import akka.actor._ object Tcp extends ExtensionKey[TcpExt] { // Java API - override def get(system: ActorSystem): TcpExt = system.extension(this) + override def get(system: ActorSystem): TcpExt = super.get(system) // shared socket options object SO extends Inet.SoForwarders { @@ -124,12 +124,12 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { val BatchAcceptLimit = getInt("batch-accept-limit") val DirectBufferSize = getIntBytes("direct-buffer-size") - val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") + val MaxDirectBufferPoolSize = getInt("direct-buffer-pool-limit") val RegisterTimeout = getString("register-timeout") match { case "infinite" ⇒ Duration.Undefined case x ⇒ Duration(x) } - val ReceivedMessageSizeLimit = getString("received-message-size-limit") match { + val ReceivedMessageSizeLimit = getString("max-received-message-size") match { case "unlimited" ⇒ Int.MaxValue case x ⇒ getIntBytes("received-message-size-limit") } @@ -150,7 +150,7 @@ class TcpExt(system: ExtendedActorSystem) extends IO.Extension { } } - val manager = { + val manager: ActorRef = { system.asInstanceOf[ActorSystemImpl].systemActorOf( props = Props(new TcpManager(this)).withDispatcher(Settings.ManagementDispatcher), name = "IO-TCP") diff --git a/akka-actor/src/main/scala/akka/io/TcpConnection.scala b/akka-actor/src/main/scala/akka/io/TcpConnection.scala index 44e7b29163..12e73bdaa1 100644 --- a/akka-actor/src/main/scala/akka/io/TcpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpConnection.scala @@ -40,7 +40,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, /** connection established, waiting for registration from user handler */ def waitingForRegistration(commander: ActorRef): Receive = { case Register(handler) ⇒ - if (TraceLogging) log.debug("{} registered as connection handler", handler) + if (TraceLogging) log.debug("[{}] registered as connection handler", handler) doRead(handler, None) // immediately try reading context.setReceiveTimeout(Duration.Undefined) @@ -54,7 +54,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, case ReceiveTimeout ⇒ // after sending `Register` user should watch this actor to make sure // it didn't die because of the timeout - log.warning("Configured registration timeout of {} expired, stopping", RegisterTimeout) + log.warning("Configured registration timeout of [{}] expired, stopping", RegisterTimeout) context.stop(self) } @@ -145,12 +145,12 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, if (TraceLogging) log.debug("Read nothing.") selector ! ReadInterest case GotCompleteData(data) ⇒ - if (TraceLogging) log.debug("Read {} bytes.", data.length) + if (TraceLogging) log.debug("Read [{}] bytes.", data.length) handler ! Received(data) selector ! ReadInterest case MoreDataWaiting(data) ⇒ - if (TraceLogging) log.debug("Read {} bytes. More data waiting.", data.length) + if (TraceLogging) log.debug("Read [{}] bytes. More data waiting.", data.length) handler ! Received(data) self ! ChannelReadable @@ -167,7 +167,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, val toWrite = pendingWrite.buffer.remaining() require(toWrite != 0) val writtenBytes = channel.write(pendingWrite.buffer) - if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) + if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes) pendingWrite = pendingWrite.consume(writtenBytes) @@ -248,7 +248,7 @@ private[io] abstract class TcpConnection(val channel: SocketChannel, case NonFatal(e) ⇒ // setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574 // (also affected: OS/X Java 1.6.0_37) - if (TraceLogging) log.debug("setSoLinger(true, 0) failed with {}", e) + if (TraceLogging) log.debug("setSoLinger(true, 0) failed with [{}]", e) } channel.close() } diff --git a/akka-actor/src/main/scala/akka/io/TcpListener.scala b/akka-actor/src/main/scala/akka/io/TcpListener.scala index 51419fa0a5..d806fe8490 100644 --- a/akka-actor/src/main/scala/akka/io/TcpListener.scala +++ b/akka-actor/src/main/scala/akka/io/TcpListener.scala @@ -45,7 +45,7 @@ private[io] class TcpListener(val selectorRouter: ActorRef, catch { case NonFatal(e) ⇒ bindCommander ! CommandFailed(bind) - log.error(e, "Bind failed for TCP channel") + log.error(e, "Bind failed for TCP channel on endpoint [{}]", endpoint) context.stop(self) } serverSocketChannel diff --git a/akka-actor/src/main/scala/akka/io/TcpManager.scala b/akka-actor/src/main/scala/akka/io/TcpManager.scala index 032bcfd6bc..aa80e96c10 100644 --- a/akka-actor/src/main/scala/akka/io/TcpManager.scala +++ b/akka-actor/src/main/scala/akka/io/TcpManager.scala @@ -45,7 +45,7 @@ import akka.io.IO.SelectorBasedManager */ private[io] class TcpManager(tcp: TcpExt) extends SelectorBasedManager(tcp.Settings, tcp.Settings.NrOfSelectors) with ActorLogging { - def receive = workerForCommand { + def receive = workerForCommandHandler { case c: Connect ⇒ val commander = sender Props(new TcpOutgoingConnection(tcp, commander, c)) diff --git a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala index 39817efe99..03d978293e 100644 --- a/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala +++ b/akka-actor/src/main/scala/akka/io/TcpOutgoingConnection.scala @@ -31,7 +31,7 @@ private[io] class TcpOutgoingConnection(_tcp: TcpExt, def receive: Receive = { case ChannelRegistered ⇒ - log.debug("Attempting connection to {}", remoteAddress) + log.debug("Attempting connection to [{}]", remoteAddress) if (channel.connect(remoteAddress)) completeConnect(commander, options) else { diff --git a/akka-actor/src/main/scala/akka/io/Udp.scala b/akka-actor/src/main/scala/akka/io/Udp.scala index 83e8b0d5f1..840dda666d 100644 --- a/akka-actor/src/main/scala/akka/io/Udp.scala +++ b/akka-actor/src/main/scala/akka/io/Udp.scala @@ -28,8 +28,8 @@ object Udp { val NrOfSelectors = getInt("nr-of-selectors") val DirectBufferSize = getIntBytes("direct-buffer-size") - val MaxDirectBufferPoolSize = getInt("max-direct-buffer-pool-size") - val BatchReceiveLimit = getInt("batch-receive-limit") + val MaxDirectBufferPoolSize = getInt("direct-buffer-pool-limit") + val BatchReceiveLimit = getInt("receive-throughput") val ManagementDispatcher = getString("management-dispatcher") diff --git a/akka-actor/src/main/scala/akka/io/UdpConn.scala b/akka-actor/src/main/scala/akka/io/UdpConn.scala index df6715d2c5..aee429a716 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConn.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConn.scala @@ -12,7 +12,7 @@ import scala.collection.immutable object UdpConn extends ExtensionKey[UdpConnExt] { // Java API - override def get(system: ActorSystem): UdpConnExt = system.extension(this) + override def get(system: ActorSystem): UdpConnExt = super.get(system) trait Command extends IO.HasFailureMessage { def failureMessage = CommandFailed(this) @@ -51,9 +51,9 @@ object UdpConn extends ExtensionKey[UdpConnExt] { class UdpConnExt(system: ExtendedActorSystem) extends IO.Extension { - val settings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) + val settings: UdpSettings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) - val manager = { + val manager: ActorRef = { system.asInstanceOf[ActorSystemImpl].systemActorOf( props = Props(new UdpConnManager(this)), name = "IO-UDP-CONN") diff --git a/akka-actor/src/main/scala/akka/io/UdpConnManager.scala b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala index a93a21259d..3868289c6b 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnManager.scala @@ -9,7 +9,7 @@ import akka.io.UdpConn.Connect class UdpConnManager(udpConn: UdpConnExt) extends SelectorBasedManager(udpConn.settings, udpConn.settings.NrOfSelectors) { - def receive = workerForCommand { + def receive = workerForCommandHandler { case c: Connect ⇒ val commander = sender Props(new UdpConnection(udpConn, commander, c)) diff --git a/akka-actor/src/main/scala/akka/io/UdpConnection.scala b/akka-actor/src/main/scala/akka/io/UdpConnection.scala index ff91a464fa..6d52adfb3b 100644 --- a/akka-actor/src/main/scala/akka/io/UdpConnection.scala +++ b/akka-actor/src/main/scala/akka/io/UdpConnection.scala @@ -33,18 +33,19 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, val socket = datagramChannel.socket options.foreach(_.beforeDatagramBind(socket)) try { - localAddress.foreach { socket.bind _ } // will blow up the actor constructor if the bind fails + localAddress foreach socket.bind datagramChannel.connect(remoteAddress) } catch { case NonFatal(e) ⇒ - log.error(e, "Failure while connecting UDP channel") + log.error(e, "Failure while connecting UDP channel to remote address [{}] local address [{}]", + remoteAddress, localAddress.map { _.toString }.getOrElse("undefined")) commander ! CommandFailed(connect) context.stop(self) } datagramChannel } selector ! RegisterChannel(channel, OP_READ) - log.debug("Successfully connected to {}", remoteAddress) + log.debug("Successfully connected to [{}]", remoteAddress) def receive = { case ChannelRegistered ⇒ @@ -58,10 +59,10 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, case ChannelReadable ⇒ doRead(handler) case Close ⇒ - log.debug("Closing UDP connection to {}", remoteAddress) + log.debug("Closing UDP connection to [{}]", remoteAddress) channel.close() sender ! Disconnected - log.debug("Connection closed to {}, stopping listener", remoteAddress) + log.debug("Connection closed to [{}], stopping listener", remoteAddress) context.stop(self) case send: Send if writePending ⇒ @@ -106,7 +107,7 @@ private[io] class UdpConnection(val udpConn: UdpConnExt, send.payload.copyToBuffer(buffer) buffer.flip() val writtenBytes = channel.write(buffer) - if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) + if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes) // Datagram channel either sends the whole message, or nothing if (writtenBytes == 0) commander ! CommandFailed(send) diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index f6d86f7053..df935ebbaa 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -13,7 +13,7 @@ import scala.collection.immutable object UdpFF extends ExtensionKey[UdpFFExt] { // Java API - override def get(system: ActorSystem): UdpFFExt = system.extension(this) + override def get(system: ActorSystem): UdpFFExt = super.get(system) trait Command extends IO.HasFailureMessage { def failureMessage = CommandFailed(this) @@ -53,9 +53,9 @@ object UdpFF extends ExtensionKey[UdpFFExt] { class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension { - val settings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) + val settings: UdpSettings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget")) - val manager = { + val manager: ActorRef = { system.asInstanceOf[ActorSystemImpl].systemActorOf( props = Props(new UdpFFManager(this)), name = "IO-UDP-FF") diff --git a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala index 0edccefab3..add5775832 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFListener.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFListener.scala @@ -35,13 +35,13 @@ private[io] class UdpFFListener(val udpFF: UdpFFExt, catch { case NonFatal(e) ⇒ bindCommander ! CommandFailed(bind) - log.error(e, "Failed to bind UDP channel") + log.error(e, "Failed to bind UDP channel to endpoint [{}]", endpoint) context.stop(self) } datagramChannel } context.parent ! RegisterChannel(channel, OP_READ) - log.debug("Successfully bound to {}", endpoint) + log.debug("Successfully bound to [{}]", endpoint) def receive: Receive = { case ChannelRegistered ⇒ @@ -55,11 +55,12 @@ private[io] class UdpFFListener(val udpFF: UdpFFExt, case ChannelReadable ⇒ doReceive(handler) case Unbind ⇒ - log.debug("Unbinding endpoint {}", endpoint) - channel.close() - sender ! Unbound - log.debug("Unbound endpoint {}, stopping listener", endpoint) - context.stop(self) + log.debug("Unbinding endpoint [{}]", endpoint) + try { + channel.close() + sender ! Unbound + log.debug("Unbound endpoint [{}], stopping listener", endpoint) + } finally context.stop(self) } def doReceive(handler: ActorRef): Unit = { diff --git a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala index 8e3e03617f..16d835ae49 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFManager.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFManager.scala @@ -44,7 +44,7 @@ import akka.io.UdpFF._ */ private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udpFF.settings, udpFF.settings.NrOfSelectors) { - def receive = workerForCommand { + def receive = workerForCommandHandler { case b: Bind ⇒ val commander = sender Props(new UdpFFListener(udpFF, commander, b)) diff --git a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala index 5303aa79d9..1120efba33 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFFSender.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFFSender.scala @@ -9,6 +9,7 @@ import akka.io.UdpFF._ import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel } import scala.collection.immutable import akka.io.Inet.SocketOption +import scala.util.control.NonFatal /** * Base class for TcpIncomingConnection and TcpOutgoingConnection. @@ -23,9 +24,7 @@ private[io] class UdpFFSender(val udpFF: UdpFFExt, options: immutable.Traversabl datagramChannel.configureBlocking(false) val socket = datagramChannel.socket - options foreach { o ⇒ - o.beforeDatagramBind(socket) - } + options foreach { _.beforeDatagramBind(socket) } datagramChannel } @@ -37,7 +36,13 @@ private[io] class UdpFFSender(val udpFF: UdpFFExt, options: immutable.Traversabl commander ! SimpleSendReady } - override def postStop(): Unit = if (channel.isOpen) channel.close() + override def postStop(): Unit = if (channel.isOpen) { + log.debug("Closing DatagramChannel after being stopped") + try channel.close() + catch { + case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel") + } + } override def postRestart(reason: Throwable): Unit = throw new IllegalStateException("Restarting not supported for connection actors.") diff --git a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala index 09dd37666c..99ed9393e2 100644 --- a/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala +++ b/akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala @@ -11,11 +11,12 @@ import java.nio.channels.DatagramChannel private[io] trait WithUdpFFSend { me: Actor with ActorLogging ⇒ - var pendingSend: (Send, ActorRef) = null + var pendingSend: Send = null + var pendingCommander: ActorRef = null // If send fails first, we allow a second go after selected writable, but no more. This flag signals that // pending send was already tried once. var retriedSend = false - def writePending = pendingSend ne null + def hasWritePending = pendingSend ne null def selector: ActorRef def channel: DatagramChannel @@ -26,7 +27,7 @@ private[io] trait WithUdpFFSend { def sendHandlers: Receive = { - case send: Send if writePending ⇒ + case send: Send if hasWritePending ⇒ if (TraceLogging) log.debug("Dropping write because queue is full") sender ! CommandFailed(send) @@ -35,10 +36,11 @@ private[io] trait WithUdpFFSend { sender ! send.ack case send: Send ⇒ - pendingSend = (send, sender) + pendingSend = send + pendingCommander = sender doSend() - case ChannelWritable ⇒ doSend() + case ChannelWritable ⇒ if (hasWritePending) doSend() } @@ -46,24 +48,24 @@ private[io] trait WithUdpFFSend { val buffer = udpFF.bufferPool.acquire() try { - val (send, commander) = pendingSend buffer.clear() - send.payload.copyToBuffer(buffer) + pendingSend.payload.copyToBuffer(buffer) buffer.flip() - val writtenBytes = channel.send(buffer, send.target) - if (TraceLogging) log.debug("Wrote {} bytes to channel", writtenBytes) + val writtenBytes = channel.send(buffer, pendingSend.target) + if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes) // Datagram channel either sends the whole message, or nothing if (writtenBytes == 0) { if (retriedSend) { - commander ! CommandFailed(send) + pendingCommander ! CommandFailed(pendingSend) retriedSend = false pendingSend = null + pendingCommander = null } else { selector ! WriteInterest retriedSend = true } - } else if (send.wantsAck) commander ! send.ack + } else if (pendingSend.wantsAck) pendingCommander ! pendingSend.ack } finally { udpFF.bufferPool.release(buffer) From bddcf9ba8caa94e06eaa9b66db3e208796a0aeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Sun, 10 Feb 2013 16:46:59 +0100 Subject: [PATCH 64/66] First iteration of documentation --- akka-docs/rst/scala/io-old.rst | 248 ++++++++++++++++++++++++++++++++ akka-docs/rst/scala/io.rst | 253 +++++++++++---------------------- 2 files changed, 334 insertions(+), 167 deletions(-) create mode 100644 akka-docs/rst/scala/io-old.rst diff --git a/akka-docs/rst/scala/io-old.rst b/akka-docs/rst/scala/io-old.rst new file mode 100644 index 0000000000..697f38651d --- /dev/null +++ b/akka-docs/rst/scala/io-old.rst @@ -0,0 +1,248 @@ +.. _io-scala-old: + +.. warning:: + This is the documentation of the old IO implementation that is considered now deprecated. Please take a look + at new IO API: :ref:`io-scala` + +IO (Scala) +========== + + +Introduction +------------ + +This documentation is in progress and some sections may be incomplete. More will be coming. + +Components +---------- + +ByteString +^^^^^^^^^^ + +A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network IO on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this IO support, so ``ByteString`` was developed. + +``ByteString`` is a `Rope-like `_ data structure that is immutable and efficient. When 2 ``ByteString``\s are concatenated together they are both stored within the resulting ``ByteString`` instead of copying both to a new ``Array``. Operations such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice. + +``ByteString`` inherits all methods from ``IndexedSeq``, and it also has some new ones. For more information, look up the ``akka.util.ByteString`` class and it's companion object in the ScalaDoc. + +``ByteString`` also comes with it's own optimized builder and iterator classes ``ByteStringBuilder`` and ``ByteIterator`` which provides special features in addition to the standard builder / iterator methods: + +Compatibility with java.io +.......................... + +A ``ByteStringBuilder`` can be wrapped in a `java.io.OutputStream` via the ``asOutputStream`` method. Likewise, ``ByteIterator`` can we wrapped in a ``java.io.InputStream`` via ``asInputStream``. Using these, ``akka.io`` applications can integrate legacy code based on ``java.io`` streams. + +Encoding and decoding of binary data +.................................... + +``ByteStringBuilder`` and ``ByteIterator`` support encoding and decoding of binary data. As an example, consider a stream of binary data frames with the following format: + +.. code-block:: text + + frameLen: Int + n: Int + m: Int + n times { + a: Short + b: Long + } + data: m times Double + +In this example, the data is to be stored in arrays of ``a``, ``b`` and ``data``. + +Decoding of such frames can be efficiently implemented in the following fashion: + +.. includecode:: code/docs/io/BinaryCoding.scala + :include: decoding + +This implementation naturally follows the example data format. In a true Scala application, one might, of course, want use specialized immutable Short/Long/Double containers instead of mutable Arrays. + +After extracting data from a ``ByteIterator``, the remaining content can also be turned back into a ``ByteString`` using the ``toSeq`` method + +.. includecode:: code/docs/io/BinaryCoding.scala + :include: rest-to-seq + +with no copying from bytes to rest involved. In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings. + +Encoding of data also is very natural, using ``ByteStringBuilder`` + +.. includecode:: code/docs/io/BinaryCoding.scala + :include: encoding + + +The encoded data then can be sent over socket (see ``IOManager``): + +.. includecode:: code/docs/io/BinaryCoding.scala + :include: sending + + +IO.Handle +^^^^^^^^^ + +``IO.Handle`` is an immutable reference to a Java NIO ``Channel``. Passing mutable ``Channel``\s between ``Actor``\s could lead to unsafe behavior, so instead subclasses of the ``IO.Handle`` trait are used. Currently there are 2 concrete subclasses: ``IO.SocketHandle`` (representing a ``SocketChannel``) and ``IO.ServerHandle`` (representing a ``ServerSocketChannel``). + +IOManager +^^^^^^^^^ + +The ``IOManager`` takes care of the low level IO details. Each ``ActorSystem`` has it's own ``IOManager``, which can be accessed calling ``IOManager(system: ActorSystem)``. ``Actor``\s communicate with the ``IOManager`` with specific messages. The messages sent from an ``Actor`` to the ``IOManager`` are handled automatically when using certain methods and the messages sent from an ``IOManager`` are handled within an ``Actor``\'s ``receive`` method. + +Connecting to a remote host: + +.. code-block:: scala + + val address = new InetSocketAddress("remotehost", 80) + val socket = IOManager(actorSystem).connect(address) + +.. code-block:: scala + + val socket = IOManager(actorSystem).connect("remotehost", 80) + +Creating a server: + +.. code-block:: scala + + val address = new InetSocketAddress("localhost", 80) + val serverSocket = IOManager(actorSystem).listen(address) + +.. code-block:: scala + + val serverSocket = IOManager(actorSystem).listen("localhost", 80) + +Receiving messages from the ``IOManager``: + +.. code-block:: scala + + def receive = { + + case IO.Listening(server, address) => + println("The server is listening on socket " + address) + + case IO.Connected(socket, address) => + println("Successfully connected to " + address) + + case IO.NewClient(server) => + println("New incoming connection on server") + val socket = server.accept() + println("Writing to new client socket") + socket.write(bytes) + println("Closing socket") + socket.close() + + case IO.Read(socket, bytes) => + println("Received incoming data from socket") + + case IO.Closed(socket: IO.SocketHandle, cause) => + println("Socket has closed, cause: " + cause) + + case IO.Closed(server: IO.ServerHandle, cause) => + println("Server socket has closed, cause: " + cause) + + } + +IO.Iteratee +^^^^^^^^^^^ + +Included with Akka's IO support is a basic implementation of ``Iteratee``\s. ``Iteratee``\s are an effective way of handling a stream of data without needing to wait for all the data to arrive. This is especially useful when dealing with non blocking IO since we will usually receive data in chunks which may not include enough information to process, or it may contain much more data than we currently need. + +This ``Iteratee`` implementation is much more basic than what is usually found. There is only support for ``ByteString`` input, and enumerators aren't used. The reason for this limited implementation is to reduce the amount of explicit type signatures needed and to keep things simple. It is important to note that Akka's ``Iteratee``\s are completely optional, incoming data can be handled in any way, including other ``Iteratee`` libraries. + +``Iteratee``\s work by processing the data that it is given and returning either the result (with any unused input) or a continuation if more input is needed. They are monadic, so methods like ``flatMap`` can be used to pass the result of an ``Iteratee`` to another. + +The basic ``Iteratee``\s included in the IO support can all be found in the ScalaDoc under ``akka.actor.IO``, and some of them are covered in the example below. + +Examples +-------- + +Http Server +^^^^^^^^^^^ + +This example will create a simple high performance HTTP server. We begin with our imports: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: imports + +Some commonly used constants: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: constants + +And case classes to hold the resulting request: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: request-class + +Now for our first ``Iteratee``. There are 3 main sections of a HTTP request: the request line, the headers, and an optional body. The main request ``Iteratee`` handles each section separately: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-request + +In the above code ``readRequest`` takes the results of 3 different ``Iteratees`` (``readRequestLine``, ``readHeaders``, ``readBody``) and combines them into a single ``Request`` object. ``readRequestLine`` actually returns a tuple, so we extract it's individual components. ``readBody`` depends on values contained within the header section, so we must pass those to the method. + +The request line has 3 parts to it: the HTTP method, the requested URI, and the HTTP version. The parts are separated by a single space, and the entire request line ends with a ``CRLF``. + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-request-line + +Reading the request method is simple as it is a single string ending in a space. The simple ``Iteratee`` that performs this is ``IO.takeUntil(delimiter: ByteString): Iteratee[ByteString]``. It keeps consuming input until the specified delimiter is found. Reading the HTTP version is also a simple string that ends with a ``CRLF``. + +The ``ascii`` method is a helper that takes a ``ByteString`` and parses it as a ``US-ASCII`` ``String``. + +Reading the request URI is a bit more complicated because we want to parse the individual components of the URI instead of just returning a simple string: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-request-uri + +For this example we are only interested in handling absolute paths. To detect if we the URI is an absolute path we use ``IO.peek(length: Int): Iteratee[ByteString]``, which returns a ``ByteString`` of the request length but doesn't actually consume the input. We peek at the next bit of input and see if it matches our ``PATH`` constant (defined above as ``ByteString("/")``). If it doesn't match we throw an error, but for a more robust solution we would want to handle other valid URIs. + +Next we handle the path itself: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-path + +The ``step`` method is a recursive method that takes a ``List`` of the accumulated path segments. It first checks if the remaining input starts with the ``PATH`` constant, and if it does, it drops that input, and returns the ``readUriPart`` ``Iteratee`` which has it's result added to the path segment accumulator and the ``step`` method is run again. + +If after reading in a path segment the next input does not start with a path, we reverse the accumulated segments and return it (dropping the last segment if it is blank). + +Following the path we read in the query (if it exists): + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-query + +It is much simpler than reading the path since we aren't doing any parsing of the query since there is no standard format of the query string. + +Both the path and query used the ``readUriPart`` ``Iteratee``, which is next: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-uri-part + +Here we have several ``Set``\s that contain valid characters pulled from the URI spec. The ``readUriPart`` method takes a ``Set`` of valid characters (already mapped to ``Byte``\s) and will continue to match characters until it reaches on that is not part of the ``Set``. If it is a percent encoded character then that is handled as a valid character and processing continues, or else we are done collecting this part of the URI. + +Headers are next: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-headers + +And if applicable, we read in the message body: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: read-body + +Finally we get to the actual ``Actor``: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: actor + +And it's companion object: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: actor-companion + +And the OKResponse: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: ok-response + +A ``main`` method to start everything up: + +.. includecode:: code/docs/io/HTTPServer.scala + :include: main diff --git a/akka-docs/rst/scala/io.rst b/akka-docs/rst/scala/io.rst index abeb6b729c..3ef25a4e08 100644 --- a/akka-docs/rst/scala/io.rst +++ b/akka-docs/rst/scala/io.rst @@ -3,14 +3,67 @@ IO (Scala) ========== - Introduction ------------ +The ``akka.io`` package has been developed in collaboration between the Akka +and `spray.io`_ teams. Its design incorporates the experiences with the +``spray-io`` module along with improvements that were jointly developed for +more general consumption as an actor-based service. + This documentation is in progress and some sections may be incomplete. More will be coming. -Components ----------- +.. note:: + The old IO implementation has been deprecated and its documentation has been moved: :ref:`io-scala-old` + +Terminology, Concepts +--------------------- +The I/O API is completely actor based, meaning that all operations are implemented as message passing instead of +direct method calls. Every I/O driver (TCP, UDP) has a special actor, called *manager* that serves +as the entry point for the API. The manager is accessible through an extension, for example the following code +looks up the TCP manager and returns its ``ActorRef``: + +.. code-block:: scala + + import akka.io.IO + import akka.io.Tcp + val tcpManager = IO(Tcp) + +For various I/O commands the manager instantiates worker actors that will expose themselves to the user of the +API by replying to the command. For example after a ``Connect`` command sent to the TCP manager the manager creates +an actor representing the TCP connection. All operations related to the given TCP connections can be invoked by sending +messages to the connection actor which announces itself by sending a ``Connected`` message. + +DeathWatch and Resource Management +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Worker actors usually need a user-side counterpart actor listening for events (such events could be inbound connections, +incoming bytes or acknowledgements for writes). These worker actors *watch* their listener counterparts, therefore the +resources assigned to them are automatically released when the listener stops. This design makes the API more robust +against resource leaks. + +Write models (Ack, Nack) +^^^^^^^^^^^^^^^^^^^^^^^^ + +Basically all of the I/O devices have a maximum throughput which limits the frequency and size of writes. When an +application tries to push more data then a device can handle, the driver has to buffer all bytes that the device has +not yet been able to write. With this approach it is possible to handle short bursts of intensive writes --- but no buffer is infinite. +Therefore, the driver has to notify the writer (a user-side actor) either that no further writes are possible, or by +explicitly notifying it when the next chunk is possible to be written or buffered. + +Both of these models are available in the TCP and UDP implementations of Akka IO. Ack based flow control can be enabled +by providing an ack object in the write message (``Write`` in the case of TCP and ``Send`` for UDP) that will be used by +the worker to notify the writer about the success. + +If a write (or any other command) fails, the driver notifies the commander with a special message (``CommandFailed`` in +the case of UDP and TCP). This message also serves as a means to notify the writer of a failed write. Please note, that +in a Nack based flow-control setting the writer has to buffer some of the writes as the failure notification for a +write ``W1`` might arrive after additional write commands ``W2`` ``W3`` has been sent. + +.. warning:: + An acknowledged write does not mean acknowledged delivery or storage. The Ack/Nack + protocol described here is a means of flow control not error handling: receiving an Ack for a write signals that the + I/O driver is ready to accept a new one. ByteString ^^^^^^^^^^ @@ -57,7 +110,7 @@ After extracting data from a ``ByteIterator``, the remaining content can also be .. includecode:: code/docs/io/BinaryCoding.scala :include: rest-to-seq - + with no copying from bytes to rest involved. In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings. Encoding of data also is very natural, using ``ByteStringBuilder`` @@ -65,180 +118,46 @@ Encoding of data also is very natural, using ``ByteStringBuilder`` .. includecode:: code/docs/io/BinaryCoding.scala :include: encoding - -The encoded data then can be sent over socket (see ``IOManager``): - -.. includecode:: code/docs/io/BinaryCoding.scala - :include: sending +Using TCP +--------- +TODO -IO.Handle -^^^^^^^^^ +Connecting +^^^^^^^^^^ -``IO.Handle`` is an immutable reference to a Java NIO ``Channel``. Passing mutable ``Channel``\s between ``Actor``\s could lead to unsafe behavior, so instead subclasses of the ``IO.Handle`` trait are used. Currently there are 2 concrete subclasses: ``IO.SocketHandle`` (representing a ``SocketChannel``) and ``IO.ServerHandle`` (representing a ``ServerSocketChannel``). +TODO -IOManager -^^^^^^^^^ +Accepting connections +^^^^^^^^^^^^^^^^^^^^^ -The ``IOManager`` takes care of the low level IO details. Each ``ActorSystem`` has it's own ``IOManager``, which can be accessed calling ``IOManager(system: ActorSystem)``. ``Actor``\s communicate with the ``IOManager`` with specific messages. The messages sent from an ``Actor`` to the ``IOManager`` are handled automatically when using certain methods and the messages sent from an ``IOManager`` are handled within an ``Actor``\'s ``receive`` method. +TODO -Connecting to a remote host: +Using UDP +--------- -.. code-block:: scala +TODO - val address = new InetSocketAddress("remotehost", 80) - val socket = IOManager(actorSystem).connect(address) +Connectionless UDP +^^^^^^^^^^^^^^^^^^^ + - Simple send + - Bind and send -.. code-block:: scala +Connection based UDP +^^^^^^^^^^^^^^^^^^^^ - val socket = IOManager(actorSystem).connect("remotehost", 80) +.. note:: + There is some performance benefit in using connection based UDP API over the connectionless one -- if its possible. + If there is a SecurityManager enabled on the system, every connectionless message send has to go through a security + check, while in the case of connection-based UDP the security check is cached after connection, thus writes does + not suffer an additional performance penalty. -Creating a server: +Integration with Iteratees +-------------------------- -.. code-block:: scala +Architecture in-depth +--------------------- - val address = new InetSocketAddress("localhost", 80) - val serverSocket = IOManager(actorSystem).listen(address) +For further details on the design and internal architecture see :ref:`io-layer`. -.. code-block:: scala - - val serverSocket = IOManager(actorSystem).listen("localhost", 80) - -Receiving messages from the ``IOManager``: - -.. code-block:: scala - - def receive = { - - case IO.Listening(server, address) => - println("The server is listening on socket " + address) - - case IO.Connected(socket, address) => - println("Successfully connected to " + address) - - case IO.NewClient(server) => - println("New incoming connection on server") - val socket = server.accept() - println("Writing to new client socket") - socket.write(bytes) - println("Closing socket") - socket.close() - - case IO.Read(socket, bytes) => - println("Received incoming data from socket") - - case IO.Closed(socket: IO.SocketHandle, cause) => - println("Socket has closed, cause: " + cause) - - case IO.Closed(server: IO.ServerHandle, cause) => - println("Server socket has closed, cause: " + cause) - - } - -IO.Iteratee -^^^^^^^^^^^ - -Included with Akka's IO support is a basic implementation of ``Iteratee``\s. ``Iteratee``\s are an effective way of handling a stream of data without needing to wait for all the data to arrive. This is especially useful when dealing with non blocking IO since we will usually receive data in chunks which may not include enough information to process, or it may contain much more data than we currently need. - -This ``Iteratee`` implementation is much more basic than what is usually found. There is only support for ``ByteString`` input, and enumerators aren't used. The reason for this limited implementation is to reduce the amount of explicit type signatures needed and to keep things simple. It is important to note that Akka's ``Iteratee``\s are completely optional, incoming data can be handled in any way, including other ``Iteratee`` libraries. - -``Iteratee``\s work by processing the data that it is given and returning either the result (with any unused input) or a continuation if more input is needed. They are monadic, so methods like ``flatMap`` can be used to pass the result of an ``Iteratee`` to another. - -The basic ``Iteratee``\s included in the IO support can all be found in the ScalaDoc under ``akka.actor.IO``, and some of them are covered in the example below. - -Examples --------- - -Http Server -^^^^^^^^^^^ - -This example will create a simple high performance HTTP server. We begin with our imports: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: imports - -Some commonly used constants: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: constants - -And case classes to hold the resulting request: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: request-class - -Now for our first ``Iteratee``. There are 3 main sections of a HTTP request: the request line, the headers, and an optional body. The main request ``Iteratee`` handles each section separately: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-request - -In the above code ``readRequest`` takes the results of 3 different ``Iteratees`` (``readRequestLine``, ``readHeaders``, ``readBody``) and combines them into a single ``Request`` object. ``readRequestLine`` actually returns a tuple, so we extract it's individual components. ``readBody`` depends on values contained within the header section, so we must pass those to the method. - -The request line has 3 parts to it: the HTTP method, the requested URI, and the HTTP version. The parts are separated by a single space, and the entire request line ends with a ``CRLF``. - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-request-line - -Reading the request method is simple as it is a single string ending in a space. The simple ``Iteratee`` that performs this is ``IO.takeUntil(delimiter: ByteString): Iteratee[ByteString]``. It keeps consuming input until the specified delimiter is found. Reading the HTTP version is also a simple string that ends with a ``CRLF``. - -The ``ascii`` method is a helper that takes a ``ByteString`` and parses it as a ``US-ASCII`` ``String``. - -Reading the request URI is a bit more complicated because we want to parse the individual components of the URI instead of just returning a simple string: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-request-uri - -For this example we are only interested in handling absolute paths. To detect if we the URI is an absolute path we use ``IO.peek(length: Int): Iteratee[ByteString]``, which returns a ``ByteString`` of the request length but doesn't actually consume the input. We peek at the next bit of input and see if it matches our ``PATH`` constant (defined above as ``ByteString("/")``). If it doesn't match we throw an error, but for a more robust solution we would want to handle other valid URIs. - -Next we handle the path itself: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-path - -The ``step`` method is a recursive method that takes a ``List`` of the accumulated path segments. It first checks if the remaining input starts with the ``PATH`` constant, and if it does, it drops that input, and returns the ``readUriPart`` ``Iteratee`` which has it's result added to the path segment accumulator and the ``step`` method is run again. - -If after reading in a path segment the next input does not start with a path, we reverse the accumulated segments and return it (dropping the last segment if it is blank). - -Following the path we read in the query (if it exists): - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-query - -It is much simpler than reading the path since we aren't doing any parsing of the query since there is no standard format of the query string. - -Both the path and query used the ``readUriPart`` ``Iteratee``, which is next: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-uri-part - -Here we have several ``Set``\s that contain valid characters pulled from the URI spec. The ``readUriPart`` method takes a ``Set`` of valid characters (already mapped to ``Byte``\s) and will continue to match characters until it reaches on that is not part of the ``Set``. If it is a percent encoded character then that is handled as a valid character and processing continues, or else we are done collecting this part of the URI. - -Headers are next: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-headers - -And if applicable, we read in the message body: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: read-body - -Finally we get to the actual ``Actor``: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: actor - -And it's companion object: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: actor-companion - -And the OKResponse: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: ok-response - -A ``main`` method to start everything up: - -.. includecode:: code/docs/io/HTTPServer.scala - :include: main +.. _spray.io: http://spray.io \ No newline at end of file From 63264e847af024653ad6f15d0474449e386ac3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Sun, 10 Feb 2013 18:34:29 +0100 Subject: [PATCH 65/66] Second iteration of documentation - Added basic UDP and TCP sections --- akka-docs/rst/scala/io.rst | 250 ++++++++++++++++++++++++++++++++++--- 1 file changed, 236 insertions(+), 14 deletions(-) diff --git a/akka-docs/rst/scala/io.rst b/akka-docs/rst/scala/io.rst index 3ef25a4e08..631f7a4c69 100644 --- a/akka-docs/rst/scala/io.rst +++ b/akka-docs/rst/scala/io.rst @@ -1,6 +1,6 @@ .. _io-scala: -IO (Scala) +I/O (Scala) ========== Introduction @@ -14,7 +14,7 @@ more general consumption as an actor-based service. This documentation is in progress and some sections may be incomplete. More will be coming. .. note:: - The old IO implementation has been deprecated and its documentation has been moved: :ref:`io-scala-old` + The old I/O implementation has been deprecated and its documentation has been moved: :ref:`io-scala-old` Terminology, Concepts --------------------- @@ -25,8 +25,6 @@ looks up the TCP manager and returns its ``ActorRef``: .. code-block:: scala - import akka.io.IO - import akka.io.Tcp val tcpManager = IO(Tcp) For various I/O commands the manager instantiates worker actors that will expose themselves to the user of the @@ -42,6 +40,9 @@ incoming bytes or acknowledgements for writes). These worker actors *watch* thei resources assigned to them are automatically released when the listener stops. This design makes the API more robust against resource leaks. +Thanks to the completely actor based approach of the I/O API the opposite direction works as well: a user actor +responsible for handling a connection might watch the connection actor to be notified if it unexpectedly terminates. + Write models (Ack, Nack) ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -51,7 +52,7 @@ not yet been able to write. With this approach it is possible to handle short bu Therefore, the driver has to notify the writer (a user-side actor) either that no further writes are possible, or by explicitly notifying it when the next chunk is possible to be written or buffered. -Both of these models are available in the TCP and UDP implementations of Akka IO. Ack based flow control can be enabled +Both of these models are available in the TCP and UDP implementations of Akka I/O. Ack based flow control can be enabled by providing an ack object in the write message (``Write`` in the case of TCP and ``Send`` for UDP) that will be used by the worker to notify the writer about the success. @@ -68,7 +69,7 @@ write ``W1`` might arrive after additional write commands ``W2`` ``W3`` has been ByteString ^^^^^^^^^^ -A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network IO on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this IO support, so ``ByteString`` was developed. +A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network I/O on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this I/O support, so ``ByteString`` was developed. ``ByteString`` is a `Rope-like `_ data structure that is immutable and efficient. When 2 ``ByteString``\s are concatenated together they are both stored within the resulting ``ByteString`` instead of copying both to a new ``Array``. Operations such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice. @@ -121,40 +122,261 @@ Encoding of data also is very natural, using ``ByteStringBuilder`` Using TCP --------- -TODO +As with all of the Akka I/O APIs, everything starts with acquiring a reference to the appropriate manager: + +.. code-block:: scala + + import akka.io.IO + import akka.io.Tcp + val tcpManager = IO(Tcp) + +This is an actor that handles the underlying low level I/O resources (Selectors, channels) and instantiates workers for +specific tasks, like listening to incoming connections. Connecting ^^^^^^^^^^ -TODO +The first step of connecting to a remote address is sending a ``Connect`` message to the TCP manager: + +.. code-block:: scala + + import akka.io.Tcp._ + IO(Tcp) ! Connect(remoteSocketAddress) + // It is also possible to set various socket options or specify a local address: + IO(Tcp) ! Connect(remoteSocketAddress, Some(localSocketAddress), List(SO.KeepAlive(true))) + +After issuing the Connect command the TCP manager spawns a worker actor that will handle commands related to the +connection. This worker actor will reveal itself by replying with a ``Connected`` message to the actor who sent the +``Connect`` command. + +.. code-block:: scala + + case Connected(remoteAddress, localAddress) => + connectionActor = sender + +At this point, there is still no listener associated with the connection. To finish the connection setup a ``Register`` +has to be sent to the connection actor with the listener ``ActorRef`` as a parameter. + +.. code-block:: scala + + connectionActor ! Register(listener) + +After registration, the listener actor provided in the ``listener`` parameter will be watched by the connection actor. +If the listener stops, the connection is closed, and all resources allocated for the connection released. During the +lifetime the listener may receive various event notifications: + +.. code-block:: scala + + case Received(dataByteString) => // handle incoming chunk of data + case CommandFailed(cmd) => // handle failure of command: cmd + case _: ConnectionClosed => // handle closed connections + +The last line handles all connection close events in the same way. It is possible to listen for more fine-grained +connection events, see the appropriate section below. + Accepting connections ^^^^^^^^^^^^^^^^^^^^^ +To create a TCP server and listen for inbound connection, a ``Bind`` command has to be sent to the TCP manager: + +.. code-block:: scala + + import akka.io.IO + import akka.io.Tcp + IO(Tcp) ! Bind(handler, localAddress) + +The actor sending the ``Bind`` message will receive a ``Bound`` message signalling that the server is ready to accept +incoming connections. Accepting connections is very similar to the last two steps of opening outbound connections: when +an incoming connection is established, the actor provided in ``handler`` will receive a ``Connected`` message whose +sender is the connection actor: + +.. code-block:: scala + + case Connected(remoteAddress, localAddress) => + connectionActor = sender + +At this point, there is still no listener associated with the connection. To finish the connection setup a ``Register`` +has to be sent to the connection actor with the listener ``ActorRef`` as a parameter. + +.. code-block:: scala + + connectionActor ! Register(listener) + +After registration, the listener actor provided in the ``listener`` parameter will be watched by the connection actor. +If the listener stops, the connection is closed, and all resources allocated for the connection released. During the +lifetime the listener will receive various event notifications in the same way as we has seen in the outbound +connection case. + +Closing connections +^^^^^^^^^^^^^^^^^^^ + +A connection can be closed by sending one of the commands ``Close``, ``ConfirmedClose`` or ``Abort`` to the connection +actor. + +``Close`` will close the connection by sending a ``FIN`` message, but without waiting for confirmation from +the remote endpoint. Pending writes will be flushed. If the close is successful, the listener will be notified with +``Closed`` + +``ConfirmedClose`` will close the sending direction of the connection by sending a ``FIN`` message, but receives +will continue until the remote endpoint closes the connection, too. Pending writes will be flushed. If the close is +successful, the listener will be notified with ``ConfirmedClosed`` + +``Abort`` will immediately terminate the connection by sending a ``RST`` message to the remote endpoint. Pending +writes will be not flushed. If the close is successful, the listener will be notified with ``Aborted`` + +``PeerClosed`` will be sent to the listener if the connection has been closed by the remote endpoint. + +``ErrorClosed`` will be sent to the listener whenever an error happened that forced the connection to be closed. + +All close notifications are subclasses of ``ConnectionClosed`` so listeners who do not need fine-grained close events +may handle all close events in the same way. + +Throttling Reads and Writes +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + TODO Using UDP --------- -TODO +UDP support comes in two flavors: connectionless, and connection based: + +.. code-block:: scala + + import akka.io.IO + import akka.io.UdpFF + val connectionLessUdp = IO(UdpFF) + // ... or ... + import akka.io.UdpConn + val connectionBasedUdp = IO(UdpConn) + +UDP servers can be only implemented by the connectionless API, but clients can use both. Connectionless UDP -^^^^^^^^^^^^^^^^^^^ - - Simple send - - Bind and send +^^^^^^^^^^^^^^^^^^ + +Simple Send +............ + +To simply send a UDP datagram without listening to an answer one needs to send the ``SimpleSender`` command to the +manager: + +.. code-block:: scala + + IO(UdpFF) ! SimpleSender() + // or with socket options: + import akka.io.Udp._ + IO(UdpFF) ! SimpleSender(List(SO.Broadcast(true))) + +The manager will create a worker for sending, and the worker will reply with a ``SimpleSendReady`` message: + +.. code-block:: scala + + case SimpleSendReady => + simpleSender = sender + +After saving the sender of the ``SimpleSendReady`` message it is possible to send out UDP datagrams with a simple +message send: + +.. code-block:: scala + + simpleSender ! Send(data, serverAddress) + + +Bind (and Send) +............... + +To listen for UDP datagrams arriving on a given port, the ``Bind`` command has to be sent to the connectionless UDP +manager + +.. code-block:: scala + + IO(UdpFF) ! Bind(handler, localAddress) + +After the bind succeeds, the sender of the ``Bind`` command will be notified with a ``Bound`` message. The sender of +this message is the worker for the UDP channel bound to the local address. + +.. code-block:: scala + + case Bound => + udpWorker = sender // Save the worker ref for later use + +The actor passed in the ``handler`` parameter will receive inbound UDP datagrams sent to the bound address: + +.. code-block:: scala + + case Received(dataByteString, remoteAddress) => // Do something with the data + +The ``Received`` message contains the payload of the datagram and the address of the sender. + +It is also possible to send UDP datagrams using the ``ActorRef`` of the worker saved in ``udpWorker``: + +.. code-block:: scala + + udpWorker ! Send(data, serverAddress) + +.. note:: + The difference between using a bound UDP worker to send instead of a simple-send worker is that in the former case + the sender field of the UDP datagram will be the bound local address, while in the latter it will be an undetermined + ephemeral port. Connection based UDP ^^^^^^^^^^^^^^^^^^^^ +The service provided by the connection based UDP API is similar to the bind-and-send service we have seen earlier, but +the main difference is that a connection is only able to send to the remoteAddress it was connected to, and will +receive datagrams only from that address. + +Connecting is similar to what we have seen in the previous section: + +.. code-block:: scala + + IO(UdpConn) ! Connect(handler, remoteAddress) + // or, with more options: + IO(UdpConn) ! Connect(handler, Some(localAddress), remoteAddress, List(SO.Broadcast(true))) + +After the connect succeeds, the sender of the ``Connect`` command will be notified with a ``Connected`` message. The sender of +this message is the worker for the UDP connection. + +.. code-block:: scala + + case Connected => + udpConnectionActor = sender // Save the worker ref for later use + +The actor passed in the ``handler`` parameter will receive inbound UDP datagrams sent to the bound address: + +.. code-block:: scala + + case Received(dataByteString) => // Do something with the data + +The ``Received`` message contains the payload of the datagram but unlike in the connectionless case, no sender address +will be provided, as an UDP connection only receives messages from the endpoint it has been connected to. + +It is also possible to send UDP datagrams using the ``ActorRef`` of the worker saved in ``udpWorker``: + +.. code-block:: scala + + udpConnectionActor ! Send(data) + +Again, the send does not contain a remote address, as it is always the endpoint we have been connected to. + .. note:: - There is some performance benefit in using connection based UDP API over the connectionless one -- if its possible. + There is a small performance benefit in using connection based UDP API over the connectionless one. If there is a SecurityManager enabled on the system, every connectionless message send has to go through a security - check, while in the case of connection-based UDP the security check is cached after connection, thus writes does + check, while in the case of connection-based UDP the security check is cached after connect, thus writes does not suffer an additional performance penalty. +Throttling Reads and Writes +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +TODO + Integration with Iteratees -------------------------- +TODO + Architecture in-depth --------------------- From 32d1a0072bd4b4f7142a153eb490d6c9089cb343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Tue, 12 Feb 2013 13:08:41 +0100 Subject: [PATCH 66/66] SimpleSender now works with the companion object --- .../src/test/scala/akka/io/UdpFFIntegrationSpec.scala | 2 +- akka-actor/src/main/scala/akka/io/UdpFF.scala | 1 + akka-docs/rst/scala/io.rst | 10 +++------- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala index 6cb6042b2f..88d41a21c4 100644 --- a/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/UdpFFIntegrationSpec.scala @@ -22,7 +22,7 @@ class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with Implici val simpleSender: ActorRef = { val commander = TestProbe() - commander.send(IO(UdpFF), SimpleSender(Nil)) + commander.send(IO(UdpFF), SimpleSender) commander.expectMsg(SimpleSendReady) commander.sender } diff --git a/akka-actor/src/main/scala/akka/io/UdpFF.scala b/akka-actor/src/main/scala/akka/io/UdpFF.scala index df935ebbaa..838f53a88d 100644 --- a/akka-actor/src/main/scala/akka/io/UdpFF.scala +++ b/akka-actor/src/main/scala/akka/io/UdpFF.scala @@ -35,6 +35,7 @@ object UdpFF extends ExtensionKey[UdpFFExt] { case object Unbind extends Command case class SimpleSender(options: immutable.Traversable[SocketOption] = Nil) extends Command + object SimpleSender extends SimpleSender(Nil) case object StopReading extends Command case object ResumeReading extends Command diff --git a/akka-docs/rst/scala/io.rst b/akka-docs/rst/scala/io.rst index 631f7a4c69..5f0331d8a2 100644 --- a/akka-docs/rst/scala/io.rst +++ b/akka-docs/rst/scala/io.rst @@ -235,7 +235,7 @@ may handle all close events in the same way. Throttling Reads and Writes ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +*This section is not yet ready. More coming soon* Using UDP --------- @@ -264,7 +264,7 @@ manager: .. code-block:: scala - IO(UdpFF) ! SimpleSender() + IO(UdpFF) ! SimpleSender // or with socket options: import akka.io.Udp._ IO(UdpFF) ! SimpleSender(List(SO.Broadcast(true))) @@ -370,12 +370,8 @@ Again, the send does not contain a remote address, as it is always the endpoint Throttling Reads and Writes ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +*This section is not yet ready. More coming soon* -Integration with Iteratees --------------------------- - -TODO Architecture in-depth ---------------------