Added support for server-initiated remote actors with clients getting a dummy handle to the remote actor
This commit is contained in:
parent
8fb281f4b0
commit
41766bef22
6 changed files with 321 additions and 80 deletions
|
|
@ -435,7 +435,7 @@ trait Actor extends TransactionManagement {
|
|||
_isShutDown = true
|
||||
shutdown
|
||||
ActorRegistry.unregister(this)
|
||||
// _remoteAddress.foreach(address => RemoteClient.unregister(address.getHostName, address.getPort, uuid))
|
||||
_remoteAddress.foreach(address => RemoteClient.unregister(address.getHostName, address.getPort, uuid))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -483,8 +483,7 @@ trait Actor extends TransactionManagement {
|
|||
def send(message: Any) = {
|
||||
if (_isKilled) throw new ActorKilledException("Actor [" + toString + "] has been killed, can't respond to messages")
|
||||
if (_isRunning) postMessageToMailbox(message, None)
|
||||
else throw new IllegalStateException(
|
||||
"Actor has not been started, you need to invoke 'actor.start' before using it")
|
||||
else throw new IllegalStateException("Actor has not been started, you need to invoke 'actor.start' before using it")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -784,7 +783,7 @@ trait Actor extends TransactionManagement {
|
|||
actor
|
||||
}
|
||||
|
||||
private def postMessageToMailbox(message: Any, sender: Option[Actor]): Unit = {
|
||||
protected[akka] def postMessageToMailbox(message: Any, sender: Option[Actor]): Unit = {
|
||||
if (_remoteAddress.isDefined) {
|
||||
val requestBuilder = RemoteRequest.newBuilder
|
||||
.setId(RemoteRequestIdFactory.nextId)
|
||||
|
|
@ -826,7 +825,7 @@ trait Actor extends TransactionManagement {
|
|||
}
|
||||
}
|
||||
|
||||
private def postMessageToMailboxAndCreateFutureResultWithTimeout(
|
||||
protected[akka] def postMessageToMailboxAndCreateFutureResultWithTimeout(
|
||||
message: Any,
|
||||
timeout: Long,
|
||||
senderFuture: Option[CompletableFutureResult]): CompletableFutureResult = {
|
||||
|
|
|
|||
|
|
@ -26,12 +26,62 @@ import java.util.concurrent.atomic.AtomicLong
|
|||
|
||||
import scala.collection.mutable.{HashSet, HashMap}
|
||||
|
||||
/*
|
||||
class RemoteActorHandle(id: String, className: String, timeout: Long, hostname: String, port: Int) extends Actor {
|
||||
start
|
||||
val remoteClient = RemoteClient.clientFor(hostname, port)
|
||||
|
||||
override def postMessageToMailbox(message: Any, sender: Option[Actor]): Unit = {
|
||||
val requestBuilder = RemoteRequest.newBuilder
|
||||
.setId(RemoteRequestIdFactory.nextId)
|
||||
.setTarget(className)
|
||||
.setTimeout(timeout)
|
||||
.setUuid(id)
|
||||
.setIsActor(true)
|
||||
.setIsOneWay(true)
|
||||
.setIsEscaped(false)
|
||||
if (sender.isDefined) {
|
||||
val s = sender.get
|
||||
requestBuilder.setSourceTarget(s.getClass.getName)
|
||||
requestBuilder.setSourceUuid(s.uuid)
|
||||
val (host, port) = s._replyToAddress.map(a => (a.getHostName, a.getPort)).getOrElse((Actor.HOSTNAME, Actor.PORT))
|
||||
requestBuilder.setSourceHostname(host)
|
||||
requestBuilder.setSourcePort(port)
|
||||
}
|
||||
RemoteProtocolBuilder.setMessage(message, requestBuilder)
|
||||
remoteClient.send(requestBuilder.build, None)
|
||||
}
|
||||
|
||||
override def postMessageToMailboxAndCreateFutureResultWithTimeout(
|
||||
message: Any,
|
||||
timeout: Long,
|
||||
senderFuture: Option[CompletableFutureResult]): CompletableFutureResult = {
|
||||
val requestBuilder = RemoteRequest.newBuilder
|
||||
.setId(RemoteRequestIdFactory.nextId)
|
||||
.setTarget(className)
|
||||
.setTimeout(timeout)
|
||||
.setUuid(id)
|
||||
.setIsActor(true)
|
||||
.setIsOneWay(false)
|
||||
.setIsEscaped(false)
|
||||
RemoteProtocolBuilder.setMessage(message, requestBuilder)
|
||||
val future = remoteClient.send(requestBuilder.build, senderFuture)
|
||||
if (future.isDefined) future.get
|
||||
else throw new IllegalStateException("Expected a future from remote call to actor " + toString)
|
||||
}
|
||||
|
||||
def receive = { case _ => {} }
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* @author <a href="http://jonasboner.com">Jonas Bonér</a>
|
||||
*/
|
||||
object RemoteRequestIdFactory {
|
||||
private val nodeId = UUID.newUuid
|
||||
private val id = new AtomicLong
|
||||
|
||||
def nextId: Long = id.getAndIncrement + nodeId
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +95,65 @@ object RemoteClient extends Logging {
|
|||
private val remoteClients = new HashMap[String, RemoteClient]
|
||||
private val remoteActors = new HashMap[RemoteServer.Address, HashSet[String]]
|
||||
|
||||
// FIXME: simplify overloaded methods when we have Scala 2.8
|
||||
/*
|
||||
def actorFor(className: String, hostname: String, port: Int): Actor =
|
||||
actorFor(className, className, 5000, hostname, port)
|
||||
|
||||
def actorFor(actorId: String, className: String, hostname: String, port: Int): Actor =
|
||||
actorFor(actorId, className, 5000, hostname, port)
|
||||
*/
|
||||
def actorFor(className: String, timeout: Long, hostname: String, port: Int): Actor =
|
||||
actorFor(className, className, timeout, hostname, port)
|
||||
|
||||
def actorFor(actorId: String, className: String, timeout: Long, hostname: String, port: Int): Actor = {
|
||||
new Actor {
|
||||
start
|
||||
val remoteClient = RemoteClient.clientFor(hostname, port)
|
||||
|
||||
override def postMessageToMailbox(message: Any, sender: Option[Actor]): Unit = {
|
||||
val requestBuilder = RemoteRequest.newBuilder
|
||||
.setId(RemoteRequestIdFactory.nextId)
|
||||
.setTarget(className)
|
||||
.setTimeout(timeout)
|
||||
.setUuid(actorId)
|
||||
.setIsActor(true)
|
||||
.setIsOneWay(true)
|
||||
.setIsEscaped(false)
|
||||
if (sender.isDefined) {
|
||||
val s = sender.get
|
||||
requestBuilder.setSourceTarget(s.getClass.getName)
|
||||
requestBuilder.setSourceUuid(s.uuid)
|
||||
val (host, port) = s._replyToAddress.map(a => (a.getHostName, a.getPort)).getOrElse((Actor.HOSTNAME, Actor.PORT))
|
||||
requestBuilder.setSourceHostname(host)
|
||||
requestBuilder.setSourcePort(port)
|
||||
}
|
||||
RemoteProtocolBuilder.setMessage(message, requestBuilder)
|
||||
remoteClient.send(requestBuilder.build, None)
|
||||
}
|
||||
|
||||
override def postMessageToMailboxAndCreateFutureResultWithTimeout(
|
||||
message: Any,
|
||||
timeout: Long,
|
||||
senderFuture: Option[CompletableFutureResult]): CompletableFutureResult = {
|
||||
val requestBuilder = RemoteRequest.newBuilder
|
||||
.setId(RemoteRequestIdFactory.nextId)
|
||||
.setTarget(className)
|
||||
.setTimeout(timeout)
|
||||
.setUuid(actorId)
|
||||
.setIsActor(true)
|
||||
.setIsOneWay(false)
|
||||
.setIsEscaped(false)
|
||||
RemoteProtocolBuilder.setMessage(message, requestBuilder)
|
||||
val future = remoteClient.send(requestBuilder.build, senderFuture)
|
||||
if (future.isDefined) future.get
|
||||
else throw new IllegalStateException("Expected a future from remote call to actor " + toString)
|
||||
}
|
||||
|
||||
def receive = {case _ => {}}
|
||||
}
|
||||
}
|
||||
|
||||
def clientFor(hostname: String, port: Int): RemoteClient = clientFor(new InetSocketAddress(hostname, port))
|
||||
|
||||
def clientFor(address: InetSocketAddress): RemoteClient = synchronized {
|
||||
|
|
|
|||
|
|
@ -182,13 +182,19 @@ class RemoteServer extends Logging {
|
|||
}
|
||||
}
|
||||
|
||||
def shutdown = {
|
||||
def shutdown = if (isRunning) {
|
||||
RemoteServer.unregister(hostname, port)
|
||||
openChannels.disconnect
|
||||
openChannels.close.awaitUninterruptibly
|
||||
bootstrap.releaseExternalResources
|
||||
Cluster.deregisterLocalNode(hostname, port)
|
||||
}
|
||||
|
||||
// TODO: register active object in RemoteServer as well
|
||||
def register(actor: Actor) = if (isRunning) {
|
||||
log.info("Registering server side remote actor [%s] with id [%s]", actor.getClass.getName, actor.id)
|
||||
RemoteServer.actorsFor(RemoteServer.Address(hostname, port)).actors.put(actor.id, actor)
|
||||
}
|
||||
}
|
||||
|
||||
case class Codec(encoder : ChannelHandler, decoder : ChannelHandler)
|
||||
|
|
@ -256,8 +262,7 @@ class RemoteServerHandler(
|
|||
|
||||
override def messageReceived(ctx: ChannelHandlerContext, event: MessageEvent) = {
|
||||
val message = event.getMessage
|
||||
if (message eq null) throw new IllegalStateException(
|
||||
"Message in remote MessageEvent is null: " + event)
|
||||
if (message eq null) throw new IllegalStateException("Message in remote MessageEvent is null: " + event)
|
||||
if (message.isInstanceOf[RemoteRequest]) {
|
||||
handleRemoteRequest(message.asInstanceOf[RemoteRequest], event.getChannel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import junit.framework.Test
|
|||
import junit.framework.TestCase
|
||||
import junit.framework.TestSuite
|
||||
|
||||
import se.scalablesolutions.akka.actor.{RemoteActorTest, InMemoryActorTest, ThreadBasedActorTest, SupervisorTest, RemoteSupervisorTest, SchedulerTest}
|
||||
import se.scalablesolutions.akka.actor.{ClientInitiatedRemoteActorTest, InMemoryActorTest, ThreadBasedActorTest, SupervisorTest, RemoteSupervisorTest, SchedulerTest}
|
||||
|
||||
object AllTest extends TestCase {
|
||||
def suite(): Test = {
|
||||
|
|
@ -16,7 +16,7 @@ object AllTest extends TestCase {
|
|||
suite.addTestSuite(classOf[ThreadBasedActorTest])
|
||||
suite.addTestSuite(classOf[ReactorBasedSingleThreadEventDrivenDispatcherTest])
|
||||
suite.addTestSuite(classOf[ReactorBasedThreadPoolEventDrivenDispatcherTest])
|
||||
suite.addTestSuite(classOf[RemoteActorTest])
|
||||
suite.addTestSuite(classOf[ClientInitiatedRemoteActorTest])
|
||||
suite.addTestSuite(classOf[InMemoryActorTest])
|
||||
suite.addTestSuite(classOf[SchedulerTest])
|
||||
//suite.addTestSuite(classOf[TransactionClasherTest])
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import org.junit.{Test, Before, After}
|
|||
import se.scalablesolutions.akka.remote.{RemoteServer, RemoteClient}
|
||||
import se.scalablesolutions.akka.dispatch.Dispatchers
|
||||
|
||||
object ClientInitiatedRemoteActorTest {
|
||||
object Global {
|
||||
var oneWay = "nada"
|
||||
var remoteReply = "nada"
|
||||
}
|
||||
case class Send(actor: Actor)
|
||||
|
||||
class RemoteActorSpecActorUnidirectional extends Actor {
|
||||
dispatcher = Dispatchers.newThreadBasedDispatcher(this)
|
||||
|
||||
|
|
@ -31,8 +34,6 @@ class RemoteActorSpecActorBidirectional extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
case class Send(actor: Actor)
|
||||
|
||||
class RemoteActorSpecActorAsyncSender extends Actor {
|
||||
def receive = {
|
||||
case Send(actor: Actor) =>
|
||||
|
|
@ -45,10 +46,10 @@ class RemoteActorSpecActorAsyncSender extends Actor {
|
|||
this ! Send(actor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteActorTest extends JUnitSuite {
|
||||
import Actor.Sender.Self
|
||||
|
||||
class ClientInitiatedRemoteActorTest extends JUnitSuite {
|
||||
import ClientInitiatedRemoteActorTest._
|
||||
akka.Config.config
|
||||
|
||||
val HOSTNAME = "localhost"
|
||||
|
|
@ -57,6 +58,8 @@ class RemoteActorTest extends JUnitSuite {
|
|||
var s1: RemoteServer = null
|
||||
var s2: RemoteServer = null
|
||||
|
||||
import Actor.Sender.Self
|
||||
|
||||
@Before
|
||||
def init() {
|
||||
s1 = new RemoteServer()
|
||||
|
|
@ -116,26 +119,7 @@ class RemoteActorTest extends JUnitSuite {
|
|||
actor.stop
|
||||
}
|
||||
|
||||
/*
|
||||
This test does not throw an exception since the
|
||||
_contactAddress is always defined via the
|
||||
global configuration if not set explicitly.
|
||||
|
||||
@Test
|
||||
def shouldSendRemoteReplyException = {
|
||||
implicit val timeout = 500000000L
|
||||
val actor = new RemoteActorSpecActorBidirectional
|
||||
actor.makeRemote(HOSTNAME, PORT1)
|
||||
actor.start
|
||||
|
||||
val sender = new RemoteActorSpecActorAsyncSender
|
||||
sender.start
|
||||
sender.send(actor)
|
||||
Thread.sleep(500)
|
||||
assert("exception" === Global.remoteReply)
|
||||
actor.stop
|
||||
}
|
||||
*/
|
||||
@Test
|
||||
def shouldSendReceiveException = {
|
||||
implicit val timeout = 500000000L
|
||||
144
akka-core/src/test/scala/ServerInitiatedRemoteActorTest.scala
Normal file
144
akka-core/src/test/scala/ServerInitiatedRemoteActorTest.scala
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package se.scalablesolutions.akka.actor
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import org.scalatest.junit.JUnitSuite
|
||||
import org.junit.{Test, Before, After}
|
||||
|
||||
import se.scalablesolutions.akka.remote.{RemoteServer, RemoteClient}
|
||||
import se.scalablesolutions.akka.dispatch.Dispatchers
|
||||
|
||||
object ServerInitiatedRemoteActorTest {
|
||||
val HOSTNAME = "localhost"
|
||||
val PORT = 9990
|
||||
var server: RemoteServer = null
|
||||
|
||||
object Global {
|
||||
var oneWay = "nada"
|
||||
var remoteReply = "nada"
|
||||
}
|
||||
|
||||
class RemoteActorSpecActorUnidirectional extends Actor {
|
||||
dispatcher = Dispatchers.newThreadBasedDispatcher(this)
|
||||
start
|
||||
|
||||
def receive = {
|
||||
case "OneWay" =>
|
||||
println("================== ONEWAY")
|
||||
Global.oneWay = "received"
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteActorSpecActorBidirectional extends Actor {
|
||||
start
|
||||
def receive = {
|
||||
case "Hello" =>
|
||||
reply("World")
|
||||
case "Failure" =>
|
||||
throw new RuntimeException("expected")
|
||||
}
|
||||
}
|
||||
|
||||
case class Send(actor: Actor)
|
||||
|
||||
class RemoteActorSpecActorAsyncSender extends Actor {
|
||||
start
|
||||
def receive = {
|
||||
case Send(actor: Actor) =>
|
||||
actor ! "Hello"
|
||||
case "World" =>
|
||||
Global.remoteReply = "replied"
|
||||
}
|
||||
|
||||
def send(actor: Actor) {
|
||||
this ! Send(actor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ServerInitiatedRemoteActorTest extends JUnitSuite {
|
||||
import ServerInitiatedRemoteActorTest._
|
||||
|
||||
import Actor.Sender.Self
|
||||
akka.Config.config
|
||||
|
||||
private val unit = TimeUnit.MILLISECONDS
|
||||
|
||||
@Before
|
||||
def init() {
|
||||
server = new RemoteServer()
|
||||
|
||||
server.start(HOSTNAME, PORT)
|
||||
|
||||
server.register(new RemoteActorSpecActorUnidirectional)
|
||||
server.register(new RemoteActorSpecActorBidirectional)
|
||||
server.register(new RemoteActorSpecActorAsyncSender)
|
||||
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
|
||||
// make sure the servers shutdown cleanly after the test has finished
|
||||
@After
|
||||
def finished() {
|
||||
server.shutdown
|
||||
RemoteClient.shutdownAll
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
|
||||
@Test
|
||||
def shouldSendOneWay = {
|
||||
val actor = RemoteClient.actorFor(
|
||||
"se.scalablesolutions.akka.actor.ServerInitiatedRemoteActorTest$RemoteActorSpecActorUnidirectional",
|
||||
5000L,
|
||||
HOSTNAME, PORT)
|
||||
val result = actor ! "OneWay"
|
||||
Thread.sleep(1000)
|
||||
assert("received" === Global.oneWay)
|
||||
actor.stop
|
||||
}
|
||||
|
||||
@Test
|
||||
def shouldSendReplyAsync = {
|
||||
val actor = RemoteClient.actorFor(
|
||||
"se.scalablesolutions.akka.actor.ServerInitiatedRemoteActorTest$RemoteActorSpecActorBidirectional",
|
||||
5000L,
|
||||
HOSTNAME, PORT)
|
||||
val result = actor !! "Hello"
|
||||
assert("World" === result.get.asInstanceOf[String])
|
||||
actor.stop
|
||||
}
|
||||
|
||||
@Test
|
||||
def shouldSendRemoteReply = {
|
||||
implicit val timeout = 500000000L
|
||||
val actor = RemoteClient.actorFor(
|
||||
"se.scalablesolutions.akka.actor.ServerInitiatedRemoteActorTest$RemoteActorSpecActorBidirectional",
|
||||
timeout,
|
||||
HOSTNAME, PORT)
|
||||
|
||||
val sender = new RemoteActorSpecActorAsyncSender
|
||||
sender.setReplyToAddress(HOSTNAME, PORT)
|
||||
sender.start
|
||||
sender.send(actor)
|
||||
Thread.sleep(1000)
|
||||
assert("replied" === Global.remoteReply)
|
||||
actor.stop
|
||||
}
|
||||
|
||||
@Test
|
||||
def shouldSendReceiveException = {
|
||||
implicit val timeout = 500000000L
|
||||
val actor = RemoteClient.actorFor(
|
||||
"se.scalablesolutions.akka.actor.ServerInitiatedRemoteActorTest$RemoteActorSpecActorBidirectional",
|
||||
timeout,
|
||||
HOSTNAME, PORT)
|
||||
try {
|
||||
actor !! "Failure"
|
||||
fail("Should have thrown an exception")
|
||||
} catch {
|
||||
case e =>
|
||||
assert("expected" === e.getMessage())
|
||||
}
|
||||
actor.stop
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue