The interop between these software components is somewhat unexplored territory, but they seem to work alright together. Once you figure out what the Grails conventions are for calling into Scala and where to put the Akka application.conf
file (right next to the application.properties
file in the same directory), everything starts up just fine.
The Grails dependency injection mechanism for getting references to Services doesn’t work with the way of doing things I’m about to describe. I may need to revisit this in the future, if the convenience of having that becomes greater than keeping everything actor-related in Scala. I have no desire to wrap Akka Actors in Groovy and will eventually need to create a common Java interface that Scala can use to call into the Groovy side, and I will need to pass a reference for that to the Scala instance. But that comes later.
To get the various pieces working together, the Akka dependencies and the Scala plugin for Grails need to be added to the BuildConfig.groovy
:
1
2
3
4
5
6
7
8
9
|
dependencies {
compile 'com.typesafe.akka:akka-actor_2.10:2.3.0'
compile 'com.typesafe.akka:akka-remote_2.10:2.3.0'
compile 'com.typesafe.akka:akka-cluster_2.10:2.3.0'
}
plugins {
compile ":scala:0.10.3"
}
|
BootStrap.groovy
looks something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import com.test.AkkaClusterService
class BootStrap {
def akkaClusterService
def init = { servletContext ->
akkaClusterService = new AkkaClusterService()
}
def destroy = {
}
}
|
AkkaClusterService.scala
looks something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package com.test
import akka.actor.{Props, ActorSystem, ActorLogging, Actor}
import akka.cluster.Cluster
import akka.cluster.ClusterEvent._
import akka.cluster.ClusterEvent.MemberUp
import akka.cluster.ClusterEvent.UnreachableMember
class AkkkaClusterActor extends Actor with ActorLogging {
val cluster = Cluster(context.system)
override def preStart(): Unit = {
cluster.subscribe(self, initialStateMode = InitialStateAsEvents, classOf[MemberEvent], classOf[UnreachableMember])
}
override def postStop(): Unit = {
cluster.unsubscribe(self)
}
def receive = {
case MemberUp(member) =>
log.info("Member is Up: {}", member.address)
case UnreachableMember(member) =>
log.info("Member detected as Unreachable: {}", member)
case MemberRemoved(member, previousStatus) =>
log.info("Member is Removed: {} after {}", member.address, previousStatus)
case _: MemberEvent => // ignore
}
}
class AkkaClusterService {
println("Starting AkkaClusterService.")
var system = ActorSystem("AkkaCluster")
system.actorOf(Props[AkkaClusterActor])
}
|
application.conf
looks something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
akka {
actor {
# Actors are created somewhere in the cluster.
provider = "akka.cluster.ClusterActorRefProvider"
}
remote {
log-remote-lifecycle-events = off
netty.tcp {
# Need to set this or somehow it may try to use 127.0.1.1? This was observed on Ubuntu.
hostname = "127.0.0.1"
# Remoting uses a random host port.
port = 0
}
}
cluster {
seed-nodes = [
"akka.tcp://AkkaCluster@127.0.0.1:2551",
]
auto-down-unreachable-after = 10s
}
}
|
Now as long as you have a seed node in your cluster application listening on 127.0.0.1:2551
, the cluster setup should work just fine when you fire off grails run-app
.
Starting AkkaClusterService.
[INFO] [03/25/2014 12:10:55.964] [localhost-startStop-1] [Remoting] Starting remoting
[INFO] [03/25/2014 12:10:56.379] [localhost-startStop-1] [Remoting] Remoting started; listening on addresses :[akka.tcp://AkkaCluster@127.0.0.1:53884]
[INFO] [03/25/2014 12:10:56.412] [localhost-startStop-1] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:53884] - Starting up...
[INFO] [03/25/2014 12:10:56.502] [localhost-startStop-1] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:53884] - Registered cluster JMX MBean [akka:type=Cluster]
[INFO] [03/25/2014 12:10:56.502] [localhost-startStop-1] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:53884] - Started up successfully
[INFO] [03/25/2014 12:10:56.551] [AkkaCluster-akka.actor.default-dispatcher-15] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:53884] - Metrics will be retreived from MBeans, and may be incorrect on some platforms. To increase metric accuracy add the 'sigar.jar' to the classpath and the appropriate platform-specific native libary to 'java.library.path'. Reason: java.lang.ClassNotFoundException: org.hyperic.sigar.Sigar
[INFO] [03/25/2014 12:10:56.558] [AkkaCluster-akka.actor.default-dispatcher-15] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:53884] - Metrics collection has started successfully
| Server running. Browse to http://localhost:8080/grailshelloworld
| Application loaded in interactive mode. Type 'stop-app' to shutdown.
| Enter a script name to run. Use TAB for completion:
....
If it worked, you’ll see something like:
[INFO] [03/25/2014 12:51:40.910] [AkkaCluster-akka.actor.default-dispatcher-16] [Cluster(akka://AkkaCluster)] Cluster Node [akka.tcp://AkkaCluster@127.0.0.1:56797] - Welcome from [akka.tcp://AkkaCluster@127.0.0.1:2551]
[INFO] [03/25/2014 12:51:40.948] [AkkaCluster-akka.actor.default-dispatcher-3] [akka.tcp://AkkaCluster@127.0.0.1:56797/user/$a] Member is Up: akka.tcp://AkkaCluster@127.0.0.1:2551
[INFO] [03/25/2014 12:51:41.108] [AkkaCluster-akka.actor.default-dispatcher-14] [akka.tcp://AkkaCluster@127.0.0.1:56797/user/$a] Member is Up: akka.tcp://AkkaCluster@127.0.0.1:56797
If it didn’t work, you’ll see something like:
[WARN] [03/25/2014 12:10:57.912] [AkkaCluster-akka.remote.default-remote-dispatcher-7] [akka.tcp://AkkaCluster@127.0.0.1:53884/system/endpointManager/reliableEndpointWriter-akka.tcp%3A%2F%2FAkkaCluster%40127.0.0.1%3A2551-0] Association with remote system [akka.tcp://AkkaCluster@127.0.0.1:2551] has failed, address is now gated for [5000] ms. Reason is: [Association failed with [akka.tcp://AkkaCluster@127.0.0.1:2551]].
[INFO] [03/25/2014 12:10:57.929] [AkkaCluster-akka.actor.default-dispatcher-5] [akka://AkkaCluster/deadLetters] Message [akka.cluster.InternalClusterAction$InitJoin$] from Actor[akka://AkkaCluster/system/cluster/core/daemon/joinSeedNodeProcess#877024691] to Actor[akka://AkkaCluster/deadLetters] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [03/25/2014 12:10:57.929] [AkkaCluster-akka.actor.default-dispatcher-5] [akka://AkkaCluster/system/endpointManager/reliableEndpointWriter-akka.tcp%3A%2F%2FAkkaCluster%40127.0.0.1%3A2551-0/endpointWriter] Message [akka.actor.FSM$Timer] from Actor[akka://AkkaCluster/deadLetters] to Actor[akka://AkkaCluster/system/endpointManager/reliableEndpointWriter-akka.tcp%3A%2F%2FAkkaCluster%40127.0.0.1%3A2551-0/endpointWriter#1345637125] was not delivered. [2] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
That’s it. It should now be possible to define case classes in Scala and to pass them along w/o any need to write Groovy adapters. Or, if you’re feeling adventurous, write Groovy classes using the @Scalify annotation to pass to Scala.
Wash, rinse, and repeat.