Making a Scala microservice more lively using REPL
Do you have a need to interact with a running Scala microservice using REPL?
Introduction
This post takes an existing Akka application and adds REPL interactivity into it. It is also assumed that the application is built using sbt.
The reason Akka is chosen here is because
1. Akka supports concurrency and this would be a requirement when REPL needs to run while the application performs its regular operations.
2. Akka Cluster / Remoting supports mechanism to Serialize data over the wire. Hence, this article can focus on the REPL side of things.
What is REPL?
Read-Evaluate-Print-Loop is an interactive shell tool for evaluating expressions in Scala.
Ammonite is a Scala REPL+ that we use to make a microservice interactable.
Why REPL?
Generally a microservice is an Event Loop system in itself, where it may be awaiting for an action to be performed for a state transition with-in. Most common interactability of a microservice is using HTTP. But with REPL, richer actions can be performed and state transitions are visible more transparently.
Depending on the microservices built in Scala, there may be a need to interact with the application to
a. Tune the current running characteristics of an application
b. To look at state of the system
How REPL?
The first step to turn Ammonite REPL into application is by adding dependency in sbt.
libraryDependencies += "com.lihaoyi" % "ammonite" % "1.6.9" % Test cross CrossVersion.full
💡Note: Unlike a scala biased dependency that uses %%
in its artifact_id
, here exact minor version dependency is required so, the cross CrossVersion.full
is used.
Also, note that we are only adding as Test
dependency. This way, once the REPL is found to be useful, it can be moved into a separate project. Hence, the current dependencies can safely added be added to the application (as test like).
The second step is to have a mechanism to launch the REPL from sbt.
Add the following to source/test/scala/amm.scala
:
object amm {
def main(args: Array[String]): Unit = {
val arr = Array("--predef", "src/test/scala/pre_def.sc")
ammonite.Main.main(arr)
}
}
Create a file src/test/scala/pre_def.sc
that contains Gateway code to access the actorsystem running in remote.
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl._
import akka.util.Timeout
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._var context: ActorContext[Any] = null
val config: Config = ConfigFactory.parseString(
"""akka{
| actor{
| provider = "cluster"
| }
| remote {
| enabled-transports = ["akka.remote.netty.tcp"]
| netty.tcp {
| port = 0
| hostname = "127.0.0.1"
| }
| }
| cluster {
| seed-nodes = ["akka.tcp://your_actor_system_name@ip_address_of_remote_system:port"]
| }
| }
""".stripMargin)object TestActor {
def apply(): Behavior[Any] = Behaviors.setup[Any] { ctx =>
implicit val timeout = Timeout(5 minutes)
ctx.system.receptionist ! Receptionist.subscribe(AppMainActor.Key, ctx.self) //Discover my gateway actor that can manage all remote messages.Behaviors.receiveMessage {
case (AppMainActor.Key.Listing(listings)) if listings.nonEmpty =>
ready(listings.head) //keep handle to the gateway actor for future invocations.case _ =>
Behaviors.same
}
}private def ready(peer: ActorRef[RemoteMessage]): Behavior[Any] = Behaviors.setup { ctx =>
ctx.log.info("Ready!")
context = ctxBehaviors.receiveMessage {
case message: RemoteMessage => //redirect any messages to the gateway actor
peer ! message
Behaviors.samecase message =>
ctx.log.info(message.toString)
Behaviors.same
}
}
}val system = ActorSystem(TestActor(), "token_auth", config)/*sys.addShutdownHook {
system.terminate()
}*/object MyShortcutObject{
private implicit val timeout: Timeout = 10.seconds
private implicit val scheduler: Scheduler = system.scheduler
import akka.actor.typed.scaladsl.AskPattern._def create(param1: String): RemoteMessage1 = {
val response: Future[RemoteMessage1] = system.ask(ref => RemoteMessage1Request(param1, ref))
Await.result(response, timeout.duration)
}
}
I shall try to put together a Hello World
application for demonstrating this and update this article to reflect the same. Till then.
⛑ Suggestions / Feedback ! 😃