akka-typed - cluster: group router, cluster-load-balancing

Posted by scnjl on Thu, 11 Jun 2020 03:02:18 +0200

Let's start with the router actor for akka-typed.route is divided into pool router and group router.Let's first look at a demonstration of using pool-router:

      val pool = Routers.pool(poolSize = 4)(
        // make sure the workers are restarted if they fail
        Behaviors.supervise(WorkerRoutee()).onFailure[Exception](SupervisorStrategy.restart))
      val router = ctx.spawn(pool, "worker-pool")

      (0 to 10).foreach { n =>
        router ! WorkerRoutee.DoLog(s"msg $n")
      }

The pool in the example above is a pool-router, meaning a routee pool with four routees.Each routee is built with WorkerRoutee(), which means there is only one type of actor in the routee pool.A pool-router is a factory method that builds (spawn) all routees directly locally (JVM).That is, all routees are children of router.

Take another look at the use of group-router:

val serviceKey = ServiceKey[Worker.Command]("log-worker")

      // this would likely happen elsewhere - if we create it locally we
      // can just as well use a pool
      val workerRoutee = ctx.spawn(WorkerRoutee(), "worker-route")
      ctx.system.receptionist ! Receptionist.Register(serviceKey, workerRoutee)

      val group = Routers.group(serviceKey)
      val router = ctx.spawn(group, "worker-group")

      // the group router will stash messages until it sees the first listing of registered
      // services from the receptionist, so it is safe to send messages right away
      (0 to 10).foreach { n =>
        router ! WorkerRoutee.DoLog(s"msg $n")
      }

group-router and pool-router have more differences:

1. routee is built outside of a router, which uses a key to get a list of actor s with the same key through Receptionist as the routee group

2. Receptionist is global.Actors on any node can send registration messages to register on Receptionist

3. No size restriction, any actor will become routee once registered on Receptionist and will be managed by router

It should be said that group-router is the most appropriate choice if you want to assign computing tasks to the nodes in the cluster and perform the load-balance effect in parallel.However, it is up to the user to decide how many routees are needed for different operation tasks, which is not as convenient as using cluster-metrics in akka-classic to automatically increase or decrease routee instances based on node load.

Receptionist: Now that we're talking about it, let's go a little deeper about Receptionist: Receptionist is cluster-wide as mentioned above.That is, actors on any node can be registered on Receptonist to form a list of actors that exist on different nodes in the cluster.If Receptionist provides this list to a user, he or she can configure the operation task on each node to implement a distributed operation mode in a sense.Receptionist is used by registering ActorRef by sending a message to the Receptionist of this node, and then getting the latest list of actor Refs by publishing a registration change message via Receptionist:

  val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker")

  ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self)

  ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter)

Receptionist registration and inventory acquisition are associated with ServiceKey.The list you get should all be one type of actor, except that their addresses may be cross-node, but they can only do the same operation.From another perspective, a task is a parallel operation of actors distributed across different nodes.

As mentioned in the previous discussion, if the publish-subscribe mechanism is between two actors, the two actors also need to work within the framework of the specified information exchange protocol: we must be aware of the message types and provide the necessary message type conversion mechanisms.Here is a Receptionist registration demonstration:

object Worker {

  val WorkerServiceKey = ServiceKey[Worker.TransformText]("Worker")

  sealed trait Command
  final case class TransformText(text: String, replyTo: ActorRef[TextTransformed]) extends Command with CborSerializable
  final case class TextTransformed(text: String) extends CborSerializable

  def apply(): Behavior[Command] =
    Behaviors.setup { ctx =>
      // each worker registers themselves with the receptionist
      ctx.log.info("Registering myself with receptionist")
      ctx.system.receptionist ! Receptionist.Register(WorkerServiceKey, ctx.self)

      Behaviors.receiveMessage {
        case TransformText(text, replyTo) =>
          replyTo ! TextTransformed(text.toUpperCase)
          Behaviors.same
      }
    }
}

Receptionist registration is straightforward: registrants do not need Receptionist to return messages, so useCtx.selfSender as message.Note that the replyTo: ActorRef[TextTransformed] for TransformText represents sender as an actor that can handle TextTransformed message types.In fact, on the sender side isCtx.askType conversion for TextTransformed is provided.

Receptionist.Subscribe Receptionist is required to return an actor list, so it is a request/response mode.The replyTo sent to Receptionist messages must be of the type that the sender can handle, as follows:

  def apply(): Behavior[Event] = Behaviors.setup { ctx =>
    Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] {
        case Worker.WorkerServiceKey.Listing(workers) =>
          WorkersUpdated(workers)
      }
      ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter)

...
    }
  

Ctx.messageAdapterAn from Receptionist.Listing Registration of conversion mechanism from return type to WorkersUpdated type: List type returned from Receptionist will be converted to WorkersUpdated type as follows:

...
   Behaviors.receiveMessage {
      case WorkersUpdated(newWorkers) =>
        ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers)

...

In addition, the TextTransformed conversion mentioned above is as follows:

          ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) {
            case Success(transformedText) => TransformCompleted(transformedText.text, text)
            case Failure(ex) => JobFailed("Processing timed out", text)
          }

Ctx.askConvert TextTransformed to TransformCompleted.The complete Behavior is defined as follows:

object Frontend {

  sealed trait Event
  private case object Tick extends Event
  private final case class WorkersUpdated(newWorkers: Set[ActorRef[Worker.TransformText]]) extends Event
  private final case class TransformCompleted(originalText: String, transformedText: String) extends Event
  private final case class JobFailed(why: String, text: String) extends Event


  def apply(): Behavior[Event] = Behaviors.setup { ctx =>
    Behaviors.withTimers { timers =>
      // subscribe to available workers
      val subscriptionAdapter = ctx.messageAdapter[Receptionist.Listing] {
        case Worker.WorkerServiceKey.Listing(workers) =>
          WorkersUpdated(workers)
      }
      ctx.system.receptionist ! Receptionist.Subscribe(Worker.WorkerServiceKey, subscriptionAdapter)

      timers.startTimerWithFixedDelay(Tick, Tick, 2.seconds)

      running(ctx, IndexedSeq.empty, jobCounter = 0)
    }
  }

  private def running(ctx: ActorContext[Event], workers: IndexedSeq[ActorRef[Worker.TransformText]], jobCounter: Int): Behavior[Event] =
    Behaviors.receiveMessage {
      case WorkersUpdated(newWorkers) =>
        ctx.log.info("List of services registered with the receptionist changed: {}", newWorkers)
        running(ctx, newWorkers.toIndexedSeq, jobCounter)
      case Tick =>
        if (workers.isEmpty) {
          ctx.log.warn("Got tick request but no workers available, not sending any work")
          Behaviors.same
        } else {
          // how much time can pass before we consider a request failed
          implicit val timeout: Timeout = 5.seconds
          val selectedWorker = workers(jobCounter % workers.size)
          ctx.log.info("Sending work for processing to {}", selectedWorker)
          val text = s"hello-$jobCounter"
          ctx.ask[Worker.TransformText,Worker.TextTransformed](selectedWorker, Worker.TransformText(text, _)) {
            case Success(transformedText) => TransformCompleted(transformedText.text, text)
            case Failure(ex) => JobFailed("Processing timed out", text)
          }
          running(ctx, workers, jobCounter + 1)
        }
      case TransformCompleted(originalText, transformedText) =>
        ctx.log.info("Got completed transform of {}: {}", originalText, transformedText)
        Behaviors.same

      case JobFailed(why, text) =>
        ctx.log.warn("Transformation of text {} failed. Because: {}", text, why)
        Behaviors.same

    }

Now we can demonstrate using group-router to implement some kind of cross-node distributed operation.Because group-router manages routees through Receptionist, which is cluster-wide, it means that if we build routees on each node and register with Receptionist, a cross-node routee ActorRef list will be formed.If you assign tasks to routees on this list, you should be able to achieve the effect of cluster node load balancing.Let's demonstrate this loadbalancer.The process is simple: build a workersRouter in one access point (serviceActor), then register three workerRoutees with Receptionist, break the received tasks into subtasks and send them to the workersRouter one by one.After each workerRoutee completes its task, it sends the results to an aggregator, Aggregator returns the summary results to the serverActor after checking that it has received all the results returned by the workerRoutee.First look at this serverActor:

object Service {
  val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router")

  sealed trait Command extends CborSerializable

  case class ProcessText(text: String) extends Command {
    require(text.nonEmpty)
  }

  case class WrappedResult(res: Aggregator.Response) extends Command

  def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val aggregator = ctx.spawn(Aggregator(), "aggregator")
    val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult)
    Behaviors.receiveMessage {
      case ProcessText(text) =>
        ctx.log.info("******************** received ProcessText command: {} ****************",text)
        val words = text.split(' ').toIndexedSeq
        aggregator ! Aggregator.CountText(words.size, aggregatorRef)
        words.foreach { word =>
          workersRouter ! WorkerRoutee.Count(word, aggregator)
        }
        Behaviors.same
      case WrappedResult(msg) =>
        msg match {
          case Aggregator.Result(res) =>
            ctx.log.info("************** mean length of words = {} **********", res)
        }
        Behaviors.same
    }
  }

  def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = {
    val singletonSettings = ClusterSingletonSettings(ctx.system)
      .withRole("front")
    SingletonActor(
      Behaviors.supervise(
        serviceBehavior(workersRouter)
      ).onFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      )
      , "singletonActor"
    ).withSettings(singletonSettings)
  }

  def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val cluster = Cluster(ctx.system)
    val workersRouter = ctx.spawn(
      Routers.group(routerServiceKey)
        .withRoundRobinRouting(),
      "workersRouter"
    )
    (0 until 3).foreach { n =>
      val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n")
      ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee)
    }
    val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter))
    Behaviors.receiveMessage {
      case job@ProcessText(text) =>
        singletonActor ! job
        Behaviors.same
    }
  }

}

The overall goup-router and routee are built in apply(), and the tasks received are forwarded to singletonActor.SingletonActor is an actor with service Behavior as its core.In the servceBehavior, the tasks received are broken down and sent to the workersRouter separately.It is worth noting that the service Behavior expects to receive responses from Aggregator, and there is a request/response mode for information exchange between them, so it needs to Aggregator.Response Type conversion mechanism to WrappedResult.Also: the subtasks are sent to a workerRoutee via the workersRoute, and we need each workerRoutee to return the result of the operation to Aggregator, so the message sent to the workersRouter contains the Actor Ref of the Aggregator, such as the workersRouter!WorkerRoutee.Count(cnt, aggregatorRef).

Aggregator is a persistent Actor, as follows:

 

object Aggregator {
  sealed trait Command
  sealed trait Event extends  CborSerializable
  sealed trait Response

  case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command
  case class MarkLength(word: String, len: Int) extends Command
  case class TextCounted(cnt: Int) extends Event
  case class LengthMarked(word: String, len: Int) extends Event
  case class Result(meanWordLength: Double) extends Response

  case class State(expectedNum: Int = 0, lens: List[Int] = Nil)

  var replyTo: ActorRef[Response] = _

  def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => {
    cmd match {
      case CountText(cnt,ref) =>
        replyTo = ref
        Effect.persist(TextCounted(cnt))
      case MarkLength(word,len) =>
        Effect.persist(LengthMarked(word,len))
    }
  }
  def eventHandler: (State, Event) => State = (st,ev) => {
    ev match {
      case TextCounted(cnt) =>
        st.copy(expectedNum = cnt, lens = Nil)
      case LengthMarked(word,len) =>
        val state = st.copy(lens = len :: st.lens)
        if (state.lens.size >= state.expectedNum) {
          val meanWordLength = state.lens.sum.toDouble / state.lens.size
          replyTo ! Result(meanWordLength)
          State()
        } else state
    }
  }
  val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => {
      if (st.lens.isEmpty) {
          if (ev.isInstanceOf[LengthMarked])
            true
          else
            false
      } else
         false
  }
  def apply(): Behavior[Command] = Behaviors.supervise(
    Behaviors.setup[Command] { ctx =>
      EventSourcedBehavior(
        persistenceId = PersistenceId("33","2333"),
        emptyState = State(),
        commandHandler = commandHandler,
        eventHandler = eventHandler
      ).onPersistFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      ).receiveSignal {
        case (state, RecoveryCompleted) =>
          ctx.log.info("**************Recovery Completed with state: {}***************",state)
        case (state, SnapshotCompleted(meta))  =>
          ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
        case (state,RecoveryFailed(err)) =>
          ctx.log.error("*************recovery failed with: {}***************",err.getMessage)
        case (state,SnapshotFailed(meta,err)) =>
          ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage)
      }.snapshotWhen(takeSnapShot)
    }
  ).onFailure(
    SupervisorStrategy
      .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
      .withMaxRestarts(3)
      .withResetBackoffAfter(10.seconds)
  )
}

Notice the takeSnapShot function: This function is EventSourcedBehavior.snapshotWhen(takeSnapShot) Called.The incoming parameter is (State,Event,seqenceNr), and we need to analyze the current value of State,Event and return true to make a snapshot.

Looking at a portion of the display shows that the task has been assigned to routee s on several nodes:

20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-15] INFO com.learn.akka.WorkerRoutee$ - ************** processing [this] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [text] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.072 [ClusterSystem-akka.actor.default-dispatcher-36] INFO com.learn.akka.WorkerRoutee$ - ************** processing [be] on akka://ClusterSystem@127.0.0.1:51182 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-16] INFO com.learn.akka.WorkerRoutee$ - ************** processing [will] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-26] INFO com.learn.akka.WorkerRoutee$ - ************** processing [is] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-13] INFO com.learn.akka.WorkerRoutee$ - ************** processing [the] on akka://ClusterSystem@127.0.0.1:51173 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [that] on akka://ClusterSystem@127.0.0.1:25251 ***********
20:06:59.236 [ClusterSystem-akka.actor.default-dispatcher-3] INFO com.learn.akka.WorkerRoutee$ - ************** processing [analyzed] on akka://ClusterSystem@127.0.0.1:25251 ***********

The source code for this example is as follows:

package com.learn.akka

import akka.actor.typed._
import akka.persistence.typed._
import akka.persistence.typed.scaladsl._
import scala.concurrent.duration._
import akka.actor.typed.receptionist._
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl._
import akka.cluster.typed.Cluster
import akka.cluster.typed.ClusterSingleton
import akka.cluster.typed.ClusterSingletonSettings
import akka.cluster.typed.SingletonActor
import com.typesafe.config.ConfigFactory

object WorkerRoutee {
  sealed trait Command extends CborSerializable
  case class Count(word: String, replyTo: ActorRef[Aggregator.Command]) extends Command

  def apply(nodeAddress: String): Behavior[Command] = Behaviors.setup {ctx =>
    Behaviors.receiveMessage[Command] {
      case Count(word,replyTo) =>
        ctx.log.info("************** processing [{}] on {} ***********",word,nodeAddress)
        replyTo ! Aggregator.MarkLength(word,word.length)
        Behaviors.same
    }
  }
}
object Aggregator {
  sealed trait Command
  sealed trait Event extends  CborSerializable
  sealed trait Response

  case class CountText(cnt: Int, replyTo: ActorRef[Response]) extends Command
  case class MarkLength(word: String, len: Int) extends Command
  case class TextCounted(cnt: Int) extends Event
  case class LengthMarked(word: String, len: Int) extends Event
  case class Result(meanWordLength: Double) extends Response

  case class State(expectedNum: Int = 0, lens: List[Int] = Nil)

  var replyTo: ActorRef[Response] = _

  def commandHandler: (State,Command) => Effect[Event,State] = (st,cmd) => {
    cmd match {
      case CountText(cnt,ref) =>
        replyTo = ref
        Effect.persist(TextCounted(cnt))
      case MarkLength(word,len) =>
        Effect.persist(LengthMarked(word,len))
    }
  }
  def eventHandler: (State, Event) => State = (st,ev) => {
    ev match {
      case TextCounted(cnt) =>
        st.copy(expectedNum = cnt, lens = Nil)
      case LengthMarked(word,len) =>
        val state = st.copy(lens = len :: st.lens)
        if (state.lens.size >= state.expectedNum) {
          val meanWordLength = state.lens.sum.toDouble / state.lens.size
          replyTo ! Result(meanWordLength)
          State()
        } else state
    }
  }
  val takeSnapShot: (State,Event,Long) => Boolean = (st,ev,seq) => {
      if (st.lens.isEmpty) {
          if (ev.isInstanceOf[LengthMarked])
            true
          else
            false
      } else
         false
  }
  def apply(): Behavior[Command] = Behaviors.supervise(
    Behaviors.setup[Command] { ctx =>
      EventSourcedBehavior(
        persistenceId = PersistenceId("33","2333"),
        emptyState = State(),
        commandHandler = commandHandler,
        eventHandler = eventHandler
      ).onPersistFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      ).receiveSignal {
        case (state, RecoveryCompleted) =>
          ctx.log.info("**************Recovery Completed with state: {}***************",state)
        case (state, SnapshotCompleted(meta))  =>
          ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
        case (state,RecoveryFailed(err)) =>
          ctx.log.error("*************recovery failed with: {}***************",err.getMessage)
        case (state,SnapshotFailed(meta,err)) =>
          ctx.log.error("***************snapshoting failed with: {}*************",err.getMessage)
      }.snapshotWhen(takeSnapShot)
    }
  ).onFailure(
    SupervisorStrategy
      .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
      .withMaxRestarts(3)
      .withResetBackoffAfter(10.seconds)
  )
}
object Service {
  val routerServiceKey = ServiceKey[WorkerRoutee.Command]("workers-router")

  sealed trait Command extends CborSerializable

  case class ProcessText(text: String) extends Command {
    require(text.nonEmpty)
  }

  case class WrappedResult(res: Aggregator.Response) extends Command

  def serviceBehavior(workersRouter: ActorRef[WorkerRoutee.Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val aggregator = ctx.spawn(Aggregator(), "aggregator")
    val aggregatorRef: ActorRef[Aggregator.Response] = ctx.messageAdapter(WrappedResult)
    Behaviors.receiveMessage {
      case ProcessText(text) =>
        ctx.log.info("******************** received ProcessText command: {} ****************",text)
        val words = text.split(' ').toIndexedSeq
        aggregator ! Aggregator.CountText(words.size, aggregatorRef)
        words.foreach { word =>
          workersRouter ! WorkerRoutee.Count(word, aggregator)
        }
        Behaviors.same
      case WrappedResult(msg) =>
        msg match {
          case Aggregator.Result(res) =>
            ctx.log.info("************** mean length of words = {} **********", res)
        }
        Behaviors.same
    }
  }

  def singletonService(ctx: ActorContext[Command], workersRouter: ActorRef[WorkerRoutee.Command]) = {
    val singletonSettings = ClusterSingletonSettings(ctx.system)
      .withRole("front")
    SingletonActor(
      Behaviors.supervise(
        serviceBehavior(workersRouter)
      ).onFailure(
        SupervisorStrategy
          .restartWithBackoff(minBackoff = 10.seconds, maxBackoff = 60.seconds, randomFactor = 0.1)
          .withMaxRestarts(3)
          .withResetBackoffAfter(10.seconds)
      )
      , "singletonActor"
    ).withSettings(singletonSettings)
  }

  def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
    val cluster = Cluster(ctx.system)
    val workersRouter = ctx.spawn(
      Routers.group(routerServiceKey)
        .withRoundRobinRouting(),
      "workersRouter"
    )
    (0 until 3).foreach { n =>
      val routee = ctx.spawn(WorkerRoutee(cluster.selfMember.address.toString), s"work-routee-$n")
      ctx.system.receptionist ! Receptionist.register(routerServiceKey, routee)
    }
    val singletonActor = ClusterSingleton(ctx.system).init(singletonService(ctx, workersRouter))
    Behaviors.receiveMessage {
      case job@ProcessText(text) =>
        singletonActor ! job
        Behaviors.same
    }
  }

}

object LoadBalance {
  def main(args: Array[String]): Unit = {
    if (args.isEmpty) {
      startup("compute", 25251)
      startup("compute", 25252)
      startup("compute", 25253)
      startup("front", 25254)
    } else {
      require(args.size == 2, "Usage: role port")
      startup(args(0), args(1).toInt)
    }
  }

  def startup(role: String, port: Int): Unit = {
    // Override the configuration of the port when specified as program argument
    val config = ConfigFactory
      .parseString(s"""
      akka.remote.artery.canonical.port=$port
      akka.cluster.roles = [$role]
      """)
      .withFallback(ConfigFactory.load("cluster-persistence"))

    val frontEnd = ActorSystem[Service.Command](Service(), "ClusterSystem", config)
    if (role == "front") {
      println("*************** sending ProcessText command  ************")
      frontEnd ! Service.ProcessText("this is the text that will be analyzed")
    }

  }

}

cluster-persistence.conf

akka.actor.allow-java-serialization = on
akka {
  loglevel = INFO
  actor {
    provider = cluster
    serialization-bindings {
      "com.learn.akka.CborSerializable" = jackson-cbor
    }
  }
 remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 0
    }
  }
  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:25251",
      "akka://ClusterSystem@127.0.0.1:25252"]
  }
  # use Cassandra to store both snapshots and the events of the persistent actors
  persistence {
    journal.plugin = "akka.persistence.cassandra.journal"
    snapshot-store.plugin = "akka.persistence.cassandra.snapshot"
  }
}
akka.persistence.cassandra {
  # don't use autocreate in production
  journal.keyspace = "poc"
  journal.keyspace-autocreate = on
  journal.tables-autocreate = on
  snapshot.keyspace = "poc_snapshot"
  snapshot.keyspace-autocreate = on
  snapshot.tables-autocreate = on
}

datastax-java-driver {
  basic.contact-points = ["192.168.11.189:9042"]
  basic.load-balancing-policy.local-datacenter = "datacenter1"
}

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.1"
scalacOptions in Compile ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlog-reflective-calls", "-Xlint")
javacOptions in Compile ++= Seq("-Xlint:unchecked", "-Xlint:deprecation")

val AkkaVersion = "2.6.5"
val AkkaPersistenceCassandraVersion = "1.0.0"


libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-cluster-sharding-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-typed" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-query" % AkkaVersion,
  "com.typesafe.akka" %% "akka-serialization-jackson" % AkkaVersion,
  "com.typesafe.akka" %% "akka-persistence-cassandra" % AkkaPersistenceCassandraVersion,
  "com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
  "ch.qos.logback"     % "logback-classic"             % "1.2.3"
)

Topics: Scala snapshot Java jvm