├── project └── plugins.sbt ├── src ├── main │ ├── scala │ │ └── com │ │ │ └── sap1ens │ │ │ ├── MicroServiceSystem.scala │ │ │ ├── utils │ │ │ └── ConfigHolder.scala │ │ │ ├── package.scala │ │ │ ├── api │ │ │ ├── HealthCheckRoutes.scala │ │ │ ├── KVRoutes.scala │ │ │ └── Api.scala │ │ │ ├── ConsulAPI.scala │ │ │ ├── KVStorageService.scala │ │ │ └── Core.scala │ └── resources │ │ ├── testing.conf │ │ ├── logback.xml │ │ └── application.conf └── test │ └── scala │ └── com │ └── sap1ens │ ├── BaseSpec.scala │ ├── BaseActorSpec.scala │ └── KVStorageServiceSpec.scala ├── Dockerfile ├── .gitignore ├── LICENSE ├── README.md ├── run.sh └── docker-compose.yml /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "spray repo" at "http://repo.spray.io" 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2") -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/MicroServiceSystem.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import com.sap1ens.api.Api 4 | 5 | object MicroServiceSystem extends App with ClusteredBootedCore with Api -------------------------------------------------------------------------------- /src/test/scala/com/sap1ens/BaseSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import org.scalatest._ 4 | 5 | trait BaseSpec extends FlatSpecLike with Matchers with BeforeAndAfter with BeforeAndAfterAll 6 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/utils/ConfigHolder.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens.utils 2 | 3 | import com.typesafe.config.ConfigFactory 4 | 5 | trait ConfigHolder { 6 | val config = ConfigFactory.load() 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/testing.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | } 5 | 6 | hostname = "localhost" 7 | port = "8878" 8 | 9 | origin.domain = "http://localhost" 10 | -------------------------------------------------------------------------------- /src/test/scala/com/sap1ens/BaseActorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import akka.testkit.{ImplicitSender, TestKit} 4 | import akka.actor.ActorSystem 5 | 6 | abstract class BaseActorSpec extends TestKit(ActorSystem()) with BaseSpec with Core with ImplicitSender -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jre 2 | 3 | ADD run.sh /app/run.sh 4 | 5 | ADD target/scala-2.11/akka-microservice-assembly-1.0.jar /app/app.jar 6 | 7 | EXPOSE 80 81 82 2551 2552 2553 8 | 9 | ENV SERVICE_NAME akka-cluster-demo 10 | 11 | ENTRYPOINT ["app/run.sh"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/package.scala: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import akka.actor.ActorRef 4 | import scala.collection.immutable.Map 5 | 6 | package object sap1ens { 7 | type Services = Map[String, ActorRef] 8 | object Services { 9 | def empty[A, B]: Map[A, B] = Map.empty 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/api/HealthCheckRoutes.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens.api 2 | 3 | import spray.http.StatusCodes 4 | import spray.routing._ 5 | 6 | class HealthCheckRoutes extends ApiRoute { 7 | 8 | val route: Route = 9 | path("health") { 10 | get { 11 | complete(StatusCodes.OK) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/scala/com/sap1ens/KVStorageServiceSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | class KVStorageServiceSpec extends BaseActorSpec { 4 | 5 | import com.sap1ens.KVStorageService._ 6 | 7 | behavior of "KV Storage Service" 8 | 9 | it should "get, set and delete elements" in { 10 | val exampleService = system.actorOf(KVStorageService.props(), "KVStorageService") 11 | 12 | exampleService ! Get("test") 13 | 14 | expectMsg(Result(None)) 15 | 16 | exampleService ! Set("test", "12345") 17 | 18 | expectMsg(Updated("test")) 19 | 20 | exampleService ! Get("test") 21 | 22 | expectMsg(Result(Some("12345"))) 23 | 24 | exampleService ! Delete("test") 25 | 26 | expectMsg(Deleted("test")) 27 | 28 | exampleService ! Get("test") 29 | 30 | expectMsg(Result(None)) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %date [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | ./logs/log.%d{yyyy-MM-dd}.log 8 | 30 9 | 10 | 11 | 12 | 13 | 14 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/ConsulAPI.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import akka.actor.{ActorSystem, Address} 4 | import com.orbitz.consul.option.{ConsistencyMode, ImmutableQueryOptions} 5 | import com.sap1ens.utils.ConfigHolder 6 | 7 | import scala.collection.JavaConversions._ 8 | 9 | object ConsulAPI extends ConfigHolder { 10 | 11 | val consul = com.orbitz.consul.Consul.builder().withUrl(s"http://${config.getString("consul.host")}:8500").build() 12 | 13 | def getServiceAddresses(implicit actorSystem: ActorSystem): List[Address] = { 14 | val serviceName = config.getString("service.name") 15 | 16 | val queryOpts = ImmutableQueryOptions 17 | .builder() 18 | .consistencyMode(ConsistencyMode.CONSISTENT) 19 | .build() 20 | val serviceNodes = consul.healthClient().getHealthyServiceInstances(serviceName, queryOpts) 21 | 22 | serviceNodes.getResponse.toList map { node => 23 | Address("akka.tcp", actorSystem.name, node.getService.getAddress, node.getService.getPort) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yaroslav Tkachenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/KVStorageService.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import akka.actor.{Actor, ActorLogging, Props} 4 | 5 | object KVStorageService { 6 | sealed trait KVCommand 7 | case class Get(key: String) extends KVCommand 8 | case class Set(key: String, value: String) extends KVCommand 9 | case class Delete(key: String) extends KVCommand 10 | 11 | sealed trait KVResult 12 | case class Result(value: Option[String]) extends KVResult 13 | case class Updated(key: String) extends KVResult 14 | case class Deleted(key: String) extends KVResult 15 | 16 | case object End 17 | 18 | def props() = Props(classOf[KVStorageService]) 19 | } 20 | 21 | class KVStorageService extends Actor with ActorLogging { 22 | import KVStorageService._ 23 | 24 | private var data: Map[String, String] = Map.empty 25 | 26 | def receive = { 27 | case Get(key) => { 28 | log.info(s"Looking for key: $key") 29 | 30 | sender() ! Result(data.get(key)) 31 | } 32 | 33 | case Set(key, value) => { 34 | log.info(s"Updating key: $key with value: $value") 35 | 36 | data = data.updated(key, value) 37 | 38 | sender() ! Updated(key) 39 | } 40 | 41 | case Delete(key) => { 42 | log.info(s"Deleting key: $key") 43 | 44 | data = data - key 45 | 46 | sender() ! Deleted(key) 47 | } 48 | 49 | case _ => 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrapping Akka Cluster With Consul 2 | 3 | If you want to start using Akka Cluster you should solve cluster bootstrapping first. Almost every tutorial on the internet (including the [official one](http://doc.akka.io/docs/akka/2.4/scala/cluster-usage.html#Joining_to_Seed_Nodes)) tells you to use seed nodes. It looks something like this: 4 | 5 | ``` 6 | akka.cluster.seed-nodes = [ 7 | "akka.tcp://ClusterSystem@host1:2552", 8 | "akka.tcp://ClusterSystem@host2:2552" 9 | ] 10 | ``` 11 | 12 | but wait… Hardcoding nodes manually? Now when we have Continuous Delivery, Immutable Infrastructure, tools like CloudFormation and Terraform, and of course Containers?! 13 | 14 | Well, Akka Cluster also provides programmatic API for bootstrapping: 15 | 16 | ```scala 17 | def joinSeedNodes(seedNodes: Seq[Address]): Unit 18 | ``` 19 | 20 | So, instead of defining seed nodes manually we’re going to use service discovery with Consul to register all nodes after startup and use provided API to create a cluster programmatically. Let’s do it! 21 | 22 | ## TL;DR 23 | 24 | It’s very easy to try (with Docker): 25 | 26 | ``` 27 | $ git clone https://github.com/sap1ens/akka-cluster-consul.git 28 | $ docker-compose up 29 | ``` 30 | 31 | Docker Compose will start 6 containers: 32 | 33 | - 3 for Consul Cluster 34 | - 3 for Akka Cluster 35 | 36 | Everything should just work and in about 15 seconds after startup you should see a few `Cluster is ready!` messages in logs - it worked! 37 | 38 | More details with explanations can be found here: [http://sap1ens.com/blog/2016/11/12/bootstrapping-akka-cluster-with-consul/](http://sap1ens.com/blog/2016/11/12/bootstrapping-akka-cluster-with-consul/). 39 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | 5 | actor { 6 | provider = "akka.cluster.ClusterActorRefProvider" 7 | } 8 | 9 | extensions = ["akka.cluster.metrics.ClusterMetricsExtension"] 10 | 11 | remote { 12 | log-remote-lifecycle-events = off 13 | log-sent-messages = off 14 | log-received-messages = off 15 | 16 | netty.tcp { 17 | # in production should be replaced with PUBLIC ip/hostname 18 | hostname = ${HOST_INTERNAL_IP} 19 | port = 2551 20 | # Temporary, only for running real cluster locally with Docker 21 | port = ${?SERVICE_AKKA_PORT} 22 | 23 | bind-hostname = ${HOST_INTERNAL_IP} 24 | bind-port = 2551 25 | # Temporary, only for running real cluster locally with Docker 26 | bind-port = ${?SERVICE_AKKA_PORT} 27 | } 28 | } 29 | 30 | cluster { 31 | #Minimum number of nodes to start broadcasting Up message, should be 3 minimum unless it's local 32 | min-nr-of-members = 3 33 | use-dispatcher = cluster-dispatcher 34 | metrics { 35 | # Disable legacy metrics in akka-cluster. 36 | enabled = off 37 | # Sigar native library extract location during tests. 38 | # Note: use per-jvm-instance folder when running multiple jvm on one host. 39 | native-library-extract-folder = ${user.dir}/target/native 40 | } 41 | } 42 | } 43 | 44 | cluster-dispatcher { 45 | type = "Dispatcher" 46 | executor = "fork-join-executor" 47 | fork-join-executor { 48 | parallelism-min = 2 49 | parallelism-max = 4 50 | } 51 | } 52 | 53 | service.name = "akka-cluster-demo" 54 | 55 | hostname = "0.0.0.0" 56 | port = "80" 57 | 58 | origin.domain = "http://localhost" 59 | 60 | consul { 61 | enabled = true 62 | host = 127.0.0.1 63 | host = ${?CONSUL_HOST} 64 | } 65 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | 6 | __curl_with_retries () { 7 | local url="$1" 8 | local payload="$2" 9 | local success_message="$3" 10 | local error_message="$4" 11 | local method="${5:--XPUT}" 12 | 13 | # retry strategy required since curl fails completely on 'connection refused'. 14 | for i in {1..50}; do 15 | curl --retry 20 --max-time 20 --fail --silent "$method" "$url" --data "$payload" && \ 16 | _info "$success_message" && break || _warn "$error_message Retry: $i." && sleep "$i" 17 | done || _error "$error_message. Retries exhausted." 18 | } 19 | 20 | _error () { 21 | local message="$1" 22 | echo "[ERROR] $message" 23 | } 24 | 25 | _warn () { 26 | local message="$1" 27 | echo "[WARN] $message" 28 | } 29 | 30 | _info () { 31 | local message="$1" 32 | echo "[INFO] $message" 33 | } 34 | 35 | _consul_url () { 36 | echo "http://consul1:8500" 37 | } 38 | 39 | _internal_ip () { 40 | echo "$(ip addr show eth0 | awk -F'[/ ]' '/inet / {print $6}' | head -n 1)" 41 | } 42 | 43 | _consul_register_service () { 44 | local name="${1:-$SERVICE_NAME}" 45 | local port="${2:-${SERVICE_PORT:-80}}" 46 | 47 | local register_url="$(_consul_url)/v1/catalog/register" 48 | 49 | local payload='{ 50 | "Node": "akka-cluster-demo-'$(_internal_ip)'", 51 | "Address": "'"$(_internal_ip)"'", 52 | "Service": { 53 | "Service": "'"$name"'", 54 | "Address": "'"$(_internal_ip)"'", 55 | "Port": '"$port"' 56 | }, 57 | "Check": { 58 | "CheckID": "service:'"$name"'", 59 | "Status": "passing" 60 | } 61 | }' 62 | 63 | _info "Attempting to register service: $payload" 64 | local success_message="Service: '$name' registered!" 65 | local error_message="Failed to register service: '$id' with local consul agent." 66 | 67 | __curl_with_retries "$register_url" "$payload" "$success_message" "$error_message" & 68 | } 69 | 70 | _consul_register_service 71 | 72 | export HOST_INTERNAL_IP=$(_internal_ip) 73 | 74 | java -jar /app/app.jar -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | consul1: 4 | image: consul 5 | command: consul agent -server -client=0.0.0.0 -bootstrap-expect=3 -data-dir=/tmp/consul -ui 6 | hostname: consul1 7 | ports: 8 | - "8301:8300" 9 | - "8401:8400" 10 | - "8501:8500" 11 | - "8601:8600" 12 | - "9001:9001" 13 | 14 | consul2: 15 | image: consul 16 | command: consul agent -server -client=0.0.0.0 -bootstrap-expect=3 -rejoin -retry-join=consul1 -data-dir=/tmp/consul -ui 17 | hostname: consul2 18 | links: 19 | - consul1 20 | ports: 21 | - "8302:8300" 22 | - "8402:8400" 23 | - "8502:8500" 24 | - "8602:8600" 25 | - "9002:9001" 26 | 27 | consul3: 28 | image: consul 29 | command: consul agent -server -client=0.0.0.0 -bootstrap-expect=3 -rejoin -retry-join=consul1 -data-dir=/tmp/consul -ui 30 | hostname: consul3 31 | links: 32 | - consul1 33 | ports: 34 | - "8303:8300" 35 | - "8403:8400" 36 | - "8503:8500" 37 | - "8603:8600" 38 | - "9003:9001" 39 | 40 | akka-cluster-demo1: 41 | image: sap1ens/akka-cluster-demo 42 | hostname: akka-cluster-demo1 43 | links: 44 | - consul1 45 | - akka-cluster-demo2 46 | - akka-cluster-demo3 47 | ports: 48 | - "80:80" 49 | - "2551:2551" 50 | environment: 51 | - CONSUL_HOST=consul1 52 | - SERVICE_PORT=2551 53 | 54 | akka-cluster-demo2: 55 | image: sap1ens/akka-cluster-demo 56 | hostname: akka-cluster-demo2 57 | links: 58 | - consul1 59 | - akka-cluster-demo3 60 | ports: 61 | - "81:80" 62 | - "2552:2552" 63 | environment: 64 | - CONSUL_HOST=consul1 65 | - SERVICE_AKKA_PORT=2552 66 | - SERVICE_PORT=2552 67 | 68 | akka-cluster-demo3: 69 | image: sap1ens/akka-cluster-demo 70 | hostname: akka-cluster-demo3 71 | links: 72 | - consul1 73 | ports: 74 | - "82:80" 75 | - "2553:2553" 76 | environment: 77 | - CONSUL_HOST=consul1 78 | - SERVICE_AKKA_PORT=2553 79 | - SERVICE_PORT=2553 80 | 81 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/api/KVRoutes.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens.api 2 | 3 | import scala.concurrent.ExecutionContext 4 | import spray.util.LoggingContext 5 | import spray.json.DefaultJsonProtocol 6 | import spray.routing._ 7 | import spray.http.StatusCodes 8 | import akka.actor.ActorRef 9 | import akka.pattern.ask 10 | 11 | import akka.util.Timeout 12 | import com.sap1ens.KVStorageService.Result 13 | 14 | import scala.concurrent.duration._ 15 | 16 | import scala.util.Success 17 | import scala.util.Failure 18 | 19 | object KVRoutes { 20 | case class TestAPIObject(thing: String) 21 | 22 | object KVRoutesProtocol extends DefaultJsonProtocol { 23 | implicit val resultFormat = jsonFormat1(Result) 24 | } 25 | } 26 | 27 | class KVRoutes(kvService: () => ActorRef)(implicit ec: ExecutionContext, log: LoggingContext) extends ApiRoute { 28 | 29 | import KVRoutes._ 30 | import KVRoutesProtocol._ 31 | import com.sap1ens.api.ApiRoute._ 32 | import com.sap1ens.KVStorageService._ 33 | import ApiRouteProtocol._ 34 | 35 | implicit val timeout = Timeout(10 seconds) 36 | 37 | val route: Route = 38 | path("kv" / Segment) { (key) => 39 | get { 40 | val future = (kvService() ? Get(key)).mapTo[KVResult] 41 | 42 | onComplete(future) { 43 | case Success(result @ Result(_: Some[String])) => 44 | complete(result) 45 | 46 | case Success(Result(None)) => 47 | complete(StatusCodes.NotFound) 48 | 49 | case Failure(e) => 50 | log.error(s"Error: ${e.toString}") 51 | complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException)) 52 | } 53 | } 54 | } ~ 55 | path("kv" / Segment / Segment) { (key, value) => 56 | put { 57 | val future = (kvService() ? Set(key, value)).mapTo[KVResult] 58 | 59 | onComplete(future) { 60 | case Success(_) => 61 | complete(StatusCodes.OK) 62 | 63 | case Failure(e) => 64 | log.error(s"Error: ${e.toString}") 65 | complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException)) 66 | } 67 | } 68 | } ~ 69 | path("kv" / Segment) { (key) => 70 | delete { 71 | val future = (kvService() ? Delete(key)).mapTo[KVResult] 72 | 73 | onComplete(future) { 74 | case Success(_) => 75 | complete(StatusCodes.OK) 76 | 77 | case Failure(e) => 78 | log.error(s"Error: ${e.toString}") 79 | complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException)) 80 | } 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/api/Api.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens.api 2 | 3 | import spray.routing._ 4 | import akka.actor.{ActorLogging, ActorRef, Props} 5 | import akka.io.IO 6 | 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | import spray.can.Http 9 | import spray.json.DefaultJsonProtocol 10 | import spray.util.LoggingContext 11 | import spray.httpx.SprayJsonSupport 12 | import com.sap1ens.utils.ConfigHolder 13 | import com.sap1ens.{ClusteredBootedCore, Core, CoreActors, Services} 14 | import spray.http.HttpHeaders.{`Access-Control-Allow-Credentials`, `Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Origin`} 15 | import spray.http.HttpMethods._ 16 | import spray.http.{HttpOrigin, SomeOrigins, StatusCodes} 17 | 18 | trait CORSSupport extends Directives { 19 | private val CORSHeaders = List( 20 | `Access-Control-Allow-Methods`(GET, POST, PUT, DELETE, OPTIONS), 21 | `Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent"), 22 | `Access-Control-Allow-Credentials`(true) 23 | ) 24 | 25 | def respondWithCORS(origin: String)(routes: => Route) = { 26 | val originHeader = `Access-Control-Allow-Origin`(SomeOrigins(Seq(HttpOrigin(origin)))) 27 | 28 | respondWithHeaders(originHeader :: CORSHeaders) { 29 | routes ~ options { complete(StatusCodes.OK) } 30 | } 31 | } 32 | } 33 | 34 | trait Api extends Directives with RouteConcatenation with CORSSupport with ConfigHolder { 35 | this: ClusteredBootedCore => 36 | 37 | val routes = 38 | respondWithCORS(config.getString("origin.domain")) { 39 | pathPrefix("api") { 40 | new HealthCheckRoutes().route ~ 41 | new KVRoutes(getKVService).route 42 | } 43 | } 44 | 45 | val rootService = system.actorOf(ApiService.props(config.getString("hostname"), config.getInt("port"), routes)) 46 | } 47 | 48 | object ApiService { 49 | def props(hostname: String, port: Int, routes: Route) = Props(classOf[ApiService], hostname, port, routes) 50 | } 51 | 52 | class ApiService(hostname: String, port: Int, route: Route) extends HttpServiceActor with ActorLogging { 53 | IO(Http)(context.system) ! Http.Bind(self, hostname, port) 54 | 55 | def receive: Receive = runRoute(route) 56 | } 57 | 58 | object ApiRoute { 59 | case class Message(message: String) 60 | 61 | object ApiRouteProtocol extends DefaultJsonProtocol { 62 | implicit val messageFormat = jsonFormat1(Message) 63 | } 64 | 65 | object ApiMessages { 66 | val UnknownException = "Unknown exception" 67 | } 68 | } 69 | 70 | abstract class ApiRoute extends Directives with SprayJsonSupport 71 | -------------------------------------------------------------------------------- /src/main/scala/com/sap1ens/Core.scala: -------------------------------------------------------------------------------- 1 | package com.sap1ens 2 | 3 | import akka.actor.{ActorRef, ActorSystem, Cancellable} 4 | import akka.cluster.Cluster 5 | import akka.cluster.singleton.{ClusterSingletonManager, ClusterSingletonManagerSettings, ClusterSingletonProxy, ClusterSingletonProxySettings} 6 | import com.sap1ens.utils.ConfigHolder 7 | import com.typesafe.scalalogging.LazyLogging 8 | 9 | import scala.concurrent.duration._ 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | 12 | trait Core { 13 | implicit def system: ActorSystem 14 | } 15 | 16 | trait BootedCore extends Core { 17 | implicit lazy val system = ActorSystem("microservice-system") 18 | 19 | sys.addShutdownHook(system.terminate()) 20 | } 21 | 22 | trait CoreActors extends ConfigHolder { 23 | this: Core => 24 | } 25 | 26 | trait ClusteredBootedCore extends BootedCore with CoreActors with LazyLogging { 27 | 28 | var kvService: ActorRef = _ 29 | val getKVService = () => kvService 30 | 31 | /** 32 | * Creating Cluster Singleton for KVStorageService 33 | */ 34 | def init() = { 35 | system.actorOf(ClusterSingletonManager.props( 36 | singletonProps = KVStorageService.props(), 37 | terminationMessage = KVStorageService.End, 38 | settings = ClusterSingletonManagerSettings(system)), 39 | name = "kvService") 40 | 41 | kvService = system.actorOf(ClusterSingletonProxy.props( 42 | singletonManagerPath = s"/user/kvService", 43 | settings = ClusterSingletonProxySettings(system)), 44 | name = "kvServiceProxy") 45 | } 46 | 47 | val cluster = Cluster(system) 48 | val isConsulEnabled = config.getBoolean("consul.enabled") 49 | 50 | // retrying cluster join until success 51 | val scheduler: Cancellable = system.scheduler.schedule(10 seconds, 30 seconds, new Runnable { 52 | override def run(): Unit = { 53 | val selfAddress = cluster.selfAddress 54 | logger.debug(s"Cluster bootstrap, self address: $selfAddress") 55 | 56 | val serviceSeeds = if (isConsulEnabled) { 57 | val serviceAddresses = ConsulAPI.getServiceAddresses 58 | logger.debug(s"Cluster bootstrap, service addresses: $serviceAddresses") 59 | 60 | // http://doc.akka.io/docs/akka/2.4.4/scala/cluster-usage.html 61 | // 62 | // When using joinSeedNodes you should not include the node itself except for the node 63 | // that is supposed to be the first seed node, and that should be placed first 64 | // in parameter to joinSeedNodes. 65 | serviceAddresses filter { address => 66 | address != selfAddress || address == serviceAddresses.head 67 | } 68 | } else { 69 | List(selfAddress) 70 | } 71 | 72 | logger.debug(s"Cluster bootstrap, found service seeds: $serviceSeeds") 73 | 74 | cluster.joinSeedNodes(serviceSeeds) 75 | } 76 | }) 77 | 78 | cluster registerOnMemberUp { 79 | logger.info("Cluster is ready!") 80 | 81 | scheduler.cancel() 82 | 83 | init() 84 | } 85 | } --------------------------------------------------------------------------------