Adds initial version of zeromq support for akka 2.0
This commit is contained in:
parent
fd68752fbe
commit
a5c55fd017
10 changed files with 602 additions and 0 deletions
|
|
@ -146,6 +146,12 @@ akka {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zeromq-dispatcher {
|
||||||
|
# A zeromq socket needs to be pinned to the thread that created it.
|
||||||
|
# Changing this value results in weird errors and race conditions within zeromq
|
||||||
|
type = "PinnedDispatcher"
|
||||||
|
}
|
||||||
|
|
||||||
default-dispatcher {
|
default-dispatcher {
|
||||||
# Must be one of the following
|
# Must be one of the following
|
||||||
# Dispatcher, (BalancingDispatcher, only valid when all actors using it are of
|
# Dispatcher, (BalancingDispatcher, only valid when all actors using it are of
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import org.zeromq.ZMQ.{ Socket, Poller }
|
||||||
|
import org.zeromq.{ ZMQ ⇒ JZMQ }
|
||||||
|
import akka.actor._
|
||||||
|
import akka.dispatch.{ Promise, Dispatchers, Future }
|
||||||
|
|
||||||
|
private[zeromq] sealed trait PollLifeCycle
|
||||||
|
private[zeromq] case object NoResults extends PollLifeCycle
|
||||||
|
private[zeromq] case object Results extends PollLifeCycle
|
||||||
|
private[zeromq] case object Closing extends PollLifeCycle
|
||||||
|
|
||||||
|
private[zeromq] class ConcurrentSocketActor(params: SocketParameters) extends Actor {
|
||||||
|
|
||||||
|
private val noBytes = Array[Byte]()
|
||||||
|
private val socket: Socket = params.context.socket(params.socketType)
|
||||||
|
private val poller: Poller = params.context.poller
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case Send(frames) ⇒
|
||||||
|
sendFrames(frames)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case ZMQMessage(frames) ⇒
|
||||||
|
sendFrames(frames)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case Connect(endpoint) ⇒
|
||||||
|
socket.connect(endpoint)
|
||||||
|
notifyListener(Connecting)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case Bind(endpoint) ⇒
|
||||||
|
socket.bind(endpoint)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case Subscribe(topic) ⇒
|
||||||
|
socket.subscribe(topic.toArray)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case Unsubscribe(topic) ⇒
|
||||||
|
socket.unsubscribe(topic.toArray)
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
case Linger(value) ⇒
|
||||||
|
socket.setLinger(value)
|
||||||
|
case Linger ⇒
|
||||||
|
sender ! socket.getLinger
|
||||||
|
case ReconnectIVL ⇒
|
||||||
|
sender ! socket.getReconnectIVL
|
||||||
|
case ReconnectIVL(value) ⇒
|
||||||
|
socket.setReconnectIVL(value)
|
||||||
|
case Backlog ⇒
|
||||||
|
sender ! socket.getBacklog
|
||||||
|
case Backlog(value) ⇒
|
||||||
|
socket.setBacklog(value)
|
||||||
|
case ReconnectIVLMax ⇒
|
||||||
|
sender ! socket.getReconnectIVLMax
|
||||||
|
case ReconnectIVLMax(value) ⇒
|
||||||
|
socket.setReconnectIVLMax(value)
|
||||||
|
case MaxMsgSize ⇒
|
||||||
|
sender ! socket.getMaxMsgSize
|
||||||
|
case MaxMsgSize(value) ⇒
|
||||||
|
socket.setMaxMsgSize(value)
|
||||||
|
case SndHWM ⇒
|
||||||
|
sender ! socket.getSndHWM
|
||||||
|
case SndHWM(value) ⇒
|
||||||
|
socket.setSndHWM(value)
|
||||||
|
case RcvHWM ⇒
|
||||||
|
sender ! socket.getRcvHWM
|
||||||
|
case RcvHWM(value) ⇒
|
||||||
|
socket.setRcvHWM(value)
|
||||||
|
case HWM(value) ⇒
|
||||||
|
socket.setHWM(value)
|
||||||
|
case Swap ⇒
|
||||||
|
sender ! socket.getSwap
|
||||||
|
case Swap(value) ⇒
|
||||||
|
socket.setSwap(value)
|
||||||
|
case Affinity ⇒
|
||||||
|
sender ! socket.getAffinity
|
||||||
|
case Affinity(value) ⇒
|
||||||
|
socket.setAffinity(value)
|
||||||
|
case Identity ⇒
|
||||||
|
sender ! socket.getIdentity
|
||||||
|
case Identity(value) ⇒
|
||||||
|
socket.setIdentity(value)
|
||||||
|
case Rate ⇒
|
||||||
|
sender ! socket.getRate
|
||||||
|
case Rate(value) ⇒
|
||||||
|
socket.setRate(value)
|
||||||
|
case RecoveryInterval ⇒
|
||||||
|
sender ! socket.getRecoveryInterval
|
||||||
|
case RecoveryInterval(value) ⇒
|
||||||
|
socket.setRecoveryInterval(value)
|
||||||
|
case MulticastLoop ⇒
|
||||||
|
sender ! socket.hasMulticastLoop
|
||||||
|
case MulticastLoop(value) ⇒
|
||||||
|
socket.setMulticastLoop(value)
|
||||||
|
case MulticastHops ⇒
|
||||||
|
sender ! socket.getMulticastHops
|
||||||
|
case MulticastHops(value) ⇒
|
||||||
|
socket.setMulticastHops(value)
|
||||||
|
case ReceiveTimeOut ⇒
|
||||||
|
sender ! socket.getReceiveTimeOut
|
||||||
|
case ReceiveTimeOut(value) ⇒
|
||||||
|
socket.setReceiveTimeOut(value)
|
||||||
|
case SendTimeOut ⇒
|
||||||
|
sender ! socket.getSendTimeOut
|
||||||
|
case SendTimeOut(value) ⇒
|
||||||
|
socket.setSendTimeOut(value)
|
||||||
|
case SendBufferSize ⇒
|
||||||
|
sender ! socket.getSendBufferSize
|
||||||
|
case SendBufferSize(value) ⇒
|
||||||
|
socket.setSendBufferSize(value)
|
||||||
|
case ReceiveBufferSize ⇒
|
||||||
|
sender ! socket.getReceiveBufferSize
|
||||||
|
case ReceiveBufferSize(value) ⇒
|
||||||
|
socket.setReceiveBufferSize(value)
|
||||||
|
case ReceiveMore ⇒
|
||||||
|
sender ! socket.hasReceiveMore
|
||||||
|
case FileDescriptor ⇒
|
||||||
|
sender ! socket.getFD
|
||||||
|
case 'poll ⇒ {
|
||||||
|
currentPoll = None
|
||||||
|
pollAndReceiveFrames()
|
||||||
|
}
|
||||||
|
case 'receiveFrames ⇒ {
|
||||||
|
receiveFrames() match {
|
||||||
|
case Seq() ⇒
|
||||||
|
case frames ⇒ notifyListener(params.deserializer(frames))
|
||||||
|
}
|
||||||
|
self ! 'poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def preStart {
|
||||||
|
poller.register(socket, Poller.POLLIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop {
|
||||||
|
currentPoll foreach { _ complete Right(Closing) }
|
||||||
|
poller.unregister(socket)
|
||||||
|
socket.close
|
||||||
|
notifyListener(Closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def sendFrames(frames: Seq[Frame]) {
|
||||||
|
def sendBytes(bytes: Seq[Byte], flags: Int) {
|
||||||
|
socket.send(bytes.toArray, flags)
|
||||||
|
}
|
||||||
|
val iter = frames.iterator
|
||||||
|
while (iter.hasNext) {
|
||||||
|
val payload = iter.next.payload
|
||||||
|
val flags = if (iter.hasNext) JZMQ.SNDMORE else 0
|
||||||
|
sendBytes(payload, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentPoll: Option[Promise[PollLifeCycle]] = None
|
||||||
|
private def pollAndReceiveFrames() {
|
||||||
|
currentPoll = currentPoll orElse Some(newEventLoop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def newEventLoop: Promise[PollLifeCycle] = {
|
||||||
|
implicit val executor = context.system.dispatchers.defaultGlobalDispatcher
|
||||||
|
(Future {
|
||||||
|
if (poller.poll(params.pollTimeoutDuration.toMillis) > 0 && poller.pollin(0)) Results else NoResults
|
||||||
|
}).asInstanceOf[Promise[PollLifeCycle]] onSuccess {
|
||||||
|
case Results ⇒ if (!self.isTerminated) self ! 'receiveFrames
|
||||||
|
case NoResults ⇒ if (!self.isTerminated) self ! 'poll
|
||||||
|
case _ ⇒ currentPoll = None
|
||||||
|
} onFailure {
|
||||||
|
case ex ⇒ {
|
||||||
|
if (context.system != null) {
|
||||||
|
context.system.log.error(ex, "There was an error receiving messages on the zeromq socket")
|
||||||
|
}
|
||||||
|
if (!self.isTerminated) self ! 'poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def receiveFrames(): Seq[Frame] = {
|
||||||
|
|
||||||
|
@inline def receiveBytes(): Array[Byte] = socket.recv(0) match {
|
||||||
|
case null ⇒ noBytes
|
||||||
|
case bytes: Array[Byte] if bytes.length > 0 ⇒ bytes
|
||||||
|
case _ ⇒ noBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveBytes() match {
|
||||||
|
case `noBytes` ⇒ Vector.empty
|
||||||
|
case someBytes ⇒
|
||||||
|
var frames = Vector(Frame(someBytes))
|
||||||
|
while (socket.hasReceiveMore) receiveBytes() match {
|
||||||
|
case `noBytes` ⇒
|
||||||
|
case someBytes ⇒ frames :+= Frame(someBytes)
|
||||||
|
}
|
||||||
|
frames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def notifyListener(message: Any) {
|
||||||
|
params.listener.foreach { listener ⇒
|
||||||
|
if (listener.isTerminated)
|
||||||
|
context stop self
|
||||||
|
else
|
||||||
|
listener ! message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
akka-zeromq/src/main/scala/akka/zeromq/Context.scala
Normal file
20
akka-zeromq/src/main/scala/akka/zeromq/Context.scala
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import org.zeromq.{ ZMQ ⇒ JZMQ }
|
||||||
|
import akka.zeromq.SocketType._
|
||||||
|
|
||||||
|
class Context(numIoThreads: Int) {
|
||||||
|
private var context = JZMQ.context(numIoThreads)
|
||||||
|
def socket(socketType: SocketType) = {
|
||||||
|
context.socket(socketType.id)
|
||||||
|
}
|
||||||
|
def poller = {
|
||||||
|
context.poller
|
||||||
|
}
|
||||||
|
def term = {
|
||||||
|
context.term
|
||||||
|
}
|
||||||
|
}
|
||||||
15
akka-zeromq/src/main/scala/akka/zeromq/Deserializer.scala
Normal file
15
akka-zeromq/src/main/scala/akka/zeromq/Deserializer.scala
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
case class Frame(payload: Seq[Byte])
|
||||||
|
object Frame { def apply(s: String): Frame = Frame(s.getBytes) }
|
||||||
|
|
||||||
|
trait Deserializer {
|
||||||
|
def apply(frames: Seq[Frame]): Any
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZMQMessageDeserializer extends Deserializer {
|
||||||
|
def apply(frames: Seq[Frame]) = ZMQMessage(frames)
|
||||||
|
}
|
||||||
104
akka-zeromq/src/main/scala/akka/zeromq/Requests.scala
Normal file
104
akka-zeromq/src/main/scala/akka/zeromq/Requests.scala
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import com.google.protobuf.Message
|
||||||
|
|
||||||
|
sealed trait Request
|
||||||
|
sealed trait SocketOption extends Request
|
||||||
|
sealed trait SocketOptionQuery extends Request
|
||||||
|
|
||||||
|
case class Connect(endpoint: String) extends Request
|
||||||
|
case class Bind(endpoint: String) extends Request
|
||||||
|
private[zeromq] case object Close extends Request
|
||||||
|
|
||||||
|
case class Subscribe(payload: Seq[Byte]) extends Request
|
||||||
|
object Subscribe {
|
||||||
|
def apply(topic: String): Subscribe = {
|
||||||
|
Subscribe(topic.getBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Unsubscribe(payload: Seq[Byte]) extends Request
|
||||||
|
object Unsubscribe {
|
||||||
|
def apply(topic: String): Unsubscribe = {
|
||||||
|
Unsubscribe(topic.getBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Send(frames: Seq[Frame]) extends Request
|
||||||
|
|
||||||
|
case class ZMQMessage(frames: Seq[Frame]) {
|
||||||
|
def firstFrameAsString = {
|
||||||
|
new String(frames.head.payload.toArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object ZMQMessage {
|
||||||
|
def apply(bytes: Array[Byte]): ZMQMessage = {
|
||||||
|
ZMQMessage(Seq(Frame(bytes)))
|
||||||
|
}
|
||||||
|
def apply(message: Message): ZMQMessage = {
|
||||||
|
ZMQMessage(message.toByteArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Linger(value: Long) extends SocketOption
|
||||||
|
object Linger extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class ReconnectIVL(value: Long) extends SocketOption
|
||||||
|
object ReconnectIVL extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class Backlog(value: Long) extends SocketOption
|
||||||
|
object Backlog extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class ReconnectIVLMax(value: Long) extends SocketOption
|
||||||
|
object ReconnectIVLMax extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class MaxMsgSize(value: Long) extends SocketOption
|
||||||
|
object MaxMsgSize extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class SndHWM(value: Long) extends SocketOption
|
||||||
|
object SndHWM extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class RcvHWM(value: Long) extends SocketOption
|
||||||
|
object RcvHWM extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class HWM(value: Long) extends SocketOption
|
||||||
|
/* object HWM extends SocketOptionQuery */
|
||||||
|
|
||||||
|
case class Swap(value: Long) extends SocketOption
|
||||||
|
object Swap extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class Affinity(value: Long) extends SocketOption
|
||||||
|
object Affinity extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class Identity(value: Array[Byte]) extends SocketOption
|
||||||
|
object Identity extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class Rate(value: Long) extends SocketOption
|
||||||
|
object Rate extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class RecoveryInterval(value: Long) extends SocketOption
|
||||||
|
object RecoveryInterval extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class MulticastLoop(value: Boolean) extends SocketOption
|
||||||
|
object MulticastLoop extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class MulticastHops(value: Long) extends SocketOption
|
||||||
|
object MulticastHops extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class ReceiveTimeOut(value: Long) extends SocketOption
|
||||||
|
object ReceiveTimeOut extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class SendTimeOut(value: Long) extends SocketOption
|
||||||
|
object SendTimeOut extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class SendBufferSize(value: Long) extends SocketOption
|
||||||
|
object SendBufferSize extends SocketOptionQuery
|
||||||
|
|
||||||
|
case class ReceiveBufferSize(value: Long) extends SocketOption
|
||||||
|
object ReceiveBufferSize extends SocketOptionQuery
|
||||||
|
|
||||||
|
object ReceiveMore extends SocketOptionQuery
|
||||||
|
object FileDescriptor extends SocketOptionQuery
|
||||||
8
akka-zeromq/src/main/scala/akka/zeromq/Responses.scala
Normal file
8
akka-zeromq/src/main/scala/akka/zeromq/Responses.scala
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
sealed trait Response
|
||||||
|
case object Connecting extends Response
|
||||||
|
case object Closed extends Response
|
||||||
14
akka-zeromq/src/main/scala/akka/zeromq/SocketType.scala
Normal file
14
akka-zeromq/src/main/scala/akka/zeromq/SocketType.scala
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import org.zeromq.{ ZMQ ⇒ JZMQ }
|
||||||
|
|
||||||
|
object SocketType extends Enumeration {
|
||||||
|
type SocketType = Value
|
||||||
|
val Pub = Value(JZMQ.PUB)
|
||||||
|
val Sub = Value(JZMQ.SUB)
|
||||||
|
val Dealer = Value(JZMQ.DEALER)
|
||||||
|
val Router = Value(JZMQ.ROUTER)
|
||||||
|
}
|
||||||
65
akka-zeromq/src/main/scala/akka/zeromq/ZeroMQExtension.scala
Normal file
65
akka-zeromq/src/main/scala/akka/zeromq/ZeroMQExtension.scala
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import akka.util.Duration
|
||||||
|
import akka.util.duration._
|
||||||
|
import akka.zeromq.SocketType._
|
||||||
|
import org.zeromq.{ ZMQ ⇒ JZMQ }
|
||||||
|
import akka.actor._
|
||||||
|
import akka.dispatch.Await
|
||||||
|
|
||||||
|
case class SocketParameters(
|
||||||
|
socketType: SocketType,
|
||||||
|
context: Context,
|
||||||
|
listener: Option[ActorRef] = None,
|
||||||
|
deserializer: Deserializer = new ZMQMessageDeserializer,
|
||||||
|
pollTimeoutDuration: Duration = 100 millis)
|
||||||
|
|
||||||
|
case class ZeroMQVersion(major: Int, minor: Int, patch: Int) {
|
||||||
|
override def toString = "%d.%d.%d".format(major, minor, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
object ZeroMQExtension extends ExtensionId[ZeroMQExtension] with ExtensionIdProvider {
|
||||||
|
def lookup() = this
|
||||||
|
def createExtension(system: ActorSystemImpl) = new ZeroMQExtension(system)
|
||||||
|
}
|
||||||
|
class ZeroMQExtension(system: ActorSystem) extends Extension {
|
||||||
|
|
||||||
|
def version = {
|
||||||
|
ZeroMQVersion(JZMQ.getMajorVersion, JZMQ.getMinorVersion, JZMQ.getPatchVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val DefaultContext = newContext()
|
||||||
|
|
||||||
|
def newContext(numIoThreads: Int = 1) = {
|
||||||
|
verifyZeroMQVersion
|
||||||
|
new Context(numIoThreads)
|
||||||
|
}
|
||||||
|
|
||||||
|
def newSocket(socketType: SocketType,
|
||||||
|
listener: Option[ActorRef] = None,
|
||||||
|
context: Context = DefaultContext, // For most applications you want to use the default context
|
||||||
|
deserializer: Deserializer = new ZMQMessageDeserializer,
|
||||||
|
pollTimeoutDuration: Duration = 100 millis) = {
|
||||||
|
verifyZeroMQVersion
|
||||||
|
val params = SocketParameters(socketType, context, listener, deserializer, pollTimeoutDuration)
|
||||||
|
implicit val timeout = system.settings.ActorTimeout
|
||||||
|
val req = (zeromq ? Props(new ConcurrentSocketActor(params)).withDispatcher("zmqdispatcher")).mapTo[ActorRef]
|
||||||
|
Await.result(req, timeout.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
val zeromq: ActorRef = {
|
||||||
|
verifyZeroMQVersion
|
||||||
|
system.asInstanceOf[ActorSystemImpl].systemActorOf(Props(new Actor {
|
||||||
|
protected def receive = { case p: Props ⇒ sender ! context.actorOf(p) }
|
||||||
|
}), "zeromq")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def verifyZeroMQVersion = {
|
||||||
|
require(
|
||||||
|
JZMQ.getFullVersion > JZMQ.makeVersion(2, 1, 0),
|
||||||
|
"Unsupported ZeroMQ version: %s".format(JZMQ.getVersionString))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.zeromq
|
||||||
|
|
||||||
|
import org.scalatest.matchers.MustMatchers
|
||||||
|
import akka.testkit.{ TestProbe, DefaultTimeout, AkkaSpec }
|
||||||
|
import akka.actor.{ Actor, Props, ActorRef }
|
||||||
|
import akka.util.Timeout
|
||||||
|
import akka.util.duration._
|
||||||
|
import java.net.{ SocketException, ConnectException, Socket }
|
||||||
|
import util.Random
|
||||||
|
|
||||||
|
object ConcurrentSocketActorSpec {
|
||||||
|
val config = """
|
||||||
|
akka {
|
||||||
|
extensions = ["akka.zeromq.ZeroMQExtension$"]
|
||||||
|
actor {
|
||||||
|
zmqdispatcher {
|
||||||
|
type = "PinnedDispatcher"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConcurrentSocketActorSpec extends AkkaSpec(ConcurrentSocketActorSpec.config) with MustMatchers with DefaultTimeout {
|
||||||
|
|
||||||
|
val endpoint = "tcp://127.0.0.1:%s" format FreePort.randomFreePort()
|
||||||
|
|
||||||
|
def zmq = system.extension(ZeroMQExtension)
|
||||||
|
|
||||||
|
"ConcurrentSocketActor" should {
|
||||||
|
"support pub-sub connections" in {
|
||||||
|
checkZeroMQInstallation
|
||||||
|
val (publisherProbe, subscriberProbe) = (TestProbe(), TestProbe())
|
||||||
|
val context = zmq.newContext()
|
||||||
|
val publisher = newPublisher(context, publisherProbe.ref)
|
||||||
|
val subscriber = newSubscriber(context, subscriberProbe.ref)
|
||||||
|
val msgGenerator = newMessageGenerator(publisher)
|
||||||
|
|
||||||
|
try {
|
||||||
|
subscriberProbe.expectMsg(Connecting)
|
||||||
|
val msgNumbers = subscriberProbe.receiveWhile(2 seconds) {
|
||||||
|
case msg: ZMQMessage ⇒ {
|
||||||
|
println("RECV: " + msg.firstFrameAsString)
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
}.map(_.firstFrameAsString.toInt)
|
||||||
|
msgNumbers.length must be > 0
|
||||||
|
msgNumbers must equal(for (i ← msgNumbers.head to msgNumbers.last) yield i)
|
||||||
|
} finally {
|
||||||
|
system stop msgGenerator
|
||||||
|
within(2 seconds) { awaitCond(msgGenerator.isTerminated) }
|
||||||
|
system stop subscriber
|
||||||
|
system stop publisher
|
||||||
|
subscriberProbe.receiveWhile(1 seconds) {
|
||||||
|
case msg ⇒ msg
|
||||||
|
}.last must equal(Closed)
|
||||||
|
context.term
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"support zero-length message frames" in {
|
||||||
|
checkZeroMQInstallation
|
||||||
|
val publisherProbe = TestProbe()
|
||||||
|
val context = zmq.newContext()
|
||||||
|
val publisher = newPublisher(context, publisherProbe.ref)
|
||||||
|
|
||||||
|
try {
|
||||||
|
publisher ! ZMQMessage(Seq[Frame]())
|
||||||
|
} finally {
|
||||||
|
system stop publisher
|
||||||
|
publisherProbe.within(5 seconds) {
|
||||||
|
publisherProbe.expectMsg(Closed)
|
||||||
|
}
|
||||||
|
context.term
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def newPublisher(context: Context, listener: ActorRef) = {
|
||||||
|
val publisher = zmq.newSocket(SocketType.Pub, context = context, listener = Some(listener))
|
||||||
|
publisher ! Bind(endpoint)
|
||||||
|
publisher
|
||||||
|
}
|
||||||
|
def newSubscriber(context: Context, listener: ActorRef) = {
|
||||||
|
val subscriber = zmq.newSocket(SocketType.Sub, context = context, listener = Some(listener))
|
||||||
|
subscriber ! Connect(endpoint)
|
||||||
|
subscriber ! Subscribe(Seq())
|
||||||
|
subscriber
|
||||||
|
}
|
||||||
|
def newMessageGenerator(actorRef: ActorRef) = {
|
||||||
|
system.actorOf(Props(new MessageGeneratorActor(actorRef)).withTimeout(Timeout(10 millis)))
|
||||||
|
}
|
||||||
|
def checkZeroMQInstallation = try {
|
||||||
|
zmq.version match {
|
||||||
|
case ZeroMQVersion(2, 1, _) ⇒ Unit
|
||||||
|
case version ⇒ invalidZeroMQVersion(version)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: LinkageError ⇒ zeroMQNotInstalled
|
||||||
|
}
|
||||||
|
def invalidZeroMQVersion(version: ZeroMQVersion) {
|
||||||
|
info("WARNING: The tests are not run because invalid ZeroMQ version: %s. Version >= 2.1.x required.".format(version))
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
def zeroMQNotInstalled {
|
||||||
|
info("WARNING: The tests are not run because ZeroMQ is not installed. Version >= 2.1.x required.")
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class MessageGeneratorActor(actorRef: ActorRef) extends Actor {
|
||||||
|
var messageNumber: Int = 0
|
||||||
|
|
||||||
|
protected def receive = {
|
||||||
|
case _ ⇒
|
||||||
|
val payload = "%s".format(messageNumber)
|
||||||
|
messageNumber = messageNumber + 1
|
||||||
|
actorRef ! ZMQMessage(payload.getBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object FreePort {
|
||||||
|
|
||||||
|
def isPortFree(port: Int) = {
|
||||||
|
try {
|
||||||
|
val socket = new Socket("127.0.0.1", port)
|
||||||
|
socket.close()
|
||||||
|
false
|
||||||
|
} catch {
|
||||||
|
case e: ConnectException ⇒ true
|
||||||
|
case e: SocketException if e.getMessage == "Connection reset by peer" ⇒ true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def newPort = Random.nextInt(55365) + 10000
|
||||||
|
|
||||||
|
def randomFreePort(maxRetries: Int = 50) = {
|
||||||
|
var count = 0
|
||||||
|
var freePort = newPort
|
||||||
|
while (!isPortFree(freePort)) {
|
||||||
|
freePort = newPort
|
||||||
|
count += 1
|
||||||
|
if (count >= maxRetries) {
|
||||||
|
throw new RuntimeException("Couldn't determine a free port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
freePort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -195,6 +195,17 @@ object AkkaBuild extends Build {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
lazy val zeroMQ = Project(
|
||||||
|
id = "akka-zeromq",
|
||||||
|
base = file("akka-zeromq"),
|
||||||
|
dependencies = Seq(actor, testkit % "test;test->test"),
|
||||||
|
settings = defaultSettings ++ Seq(
|
||||||
|
libraryDependencies ++= Dependencies.zeroMQ
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
// lazy val spring = Project(
|
// lazy val spring = Project(
|
||||||
// id = "akka-spring",
|
// id = "akka-spring",
|
||||||
// base = file("akka-spring"),
|
// base = file("akka-spring"),
|
||||||
|
|
@ -434,6 +445,8 @@ object Dependencies {
|
||||||
val tutorials = Seq(Test.scalatest, Test.junit)
|
val tutorials = Seq(Test.scalatest, Test.junit)
|
||||||
|
|
||||||
val docs = Seq(Test.scalatest, Test.junit)
|
val docs = Seq(Test.scalatest, Test.junit)
|
||||||
|
|
||||||
|
val zeroMQ = Seq(Test.scalatest, Test.junit, protobuf, Dependency.zeroMQ)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Dependency {
|
object Dependency {
|
||||||
|
|
@ -489,6 +502,7 @@ object Dependency {
|
||||||
val zkClient = "zkclient" % "zkclient" % "0.3" // ApacheV2
|
val zkClient = "zkclient" % "zkclient" % "0.3" // ApacheV2
|
||||||
val zookeeper = "org.apache.hadoop.zookeeper" % "zookeeper" % V.Zookeeper // ApacheV2
|
val zookeeper = "org.apache.hadoop.zookeeper" % "zookeeper" % V.Zookeeper // ApacheV2
|
||||||
val zookeeperLock = "org.apache.hadoop.zookeeper" % "zookeeper-recipes-lock" % V.Zookeeper // ApacheV2
|
val zookeeperLock = "org.apache.hadoop.zookeeper" % "zookeeper-recipes-lock" % V.Zookeeper // ApacheV2
|
||||||
|
val zeroMQ = "org.zeromq" %% "zeromq-scala-binding" % "0.0.3" // ApacheV2
|
||||||
|
|
||||||
// Provided
|
// Provided
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue