diff --git a/README.md b/README.md index 6a912af..dc46854 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,81 @@ psql auth_password -U postgres -f ./auth-password/src/main/resources/auth_entry. psql identity_manager -U postgres -f ./identity-manager/src/main/resources/identity.sql ``` +#### Setup CORS proxy + +Use NGINX or such to proxy with CORS headers and OPTION request responses. + +Crude fast solution: + +``` +http { + server { + listen 10000; + server_name localhost; + set $true 1; + more_set_headers "Access-Control-Allow-Origin: $http_origin"; + + location /auth-codecard/ { + proxy_pass http://localhost:8005; + if ($request_method = OPTIONS ) { + add_header Access-Control-Allow-Headers "Auth-Token, Content-Type"; + add_header Access-Control-Allow-Methods "GET, OPTIONS, POST, DELETE, PUT, PATCH"; + add_header Access-Control-Allow-Credentials "true"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + + location /auth-password/ { + proxy_pass http://localhost:8002; + if ($request_method = OPTIONS ) { + add_header Access-Control-Allow-Headers "Auth-Token, Content-Type"; + add_header Access-Control-Allow-Methods "GET, OPTIONS, POST, DELETE, PUT, PATCH"; + add_header Access-Control-Allow-Credentials "true"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + location /auth-fb/ { + proxy_pass http://localhost:8001; + if ($request_method = OPTIONS ) { + add_header Access-Control-Allow-Headers "Auth-Token, Content-Type"; + add_header Access-Control-Allow-Methods "GET, OPTIONS, POST, DELETE, PUT, PATCH"; + add_header Access-Control-Allow-Credentials "true"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + location /session/ { + proxy_pass http://localhost:8011; + if ($request_method = OPTIONS ) { + add_header Access-Control-Allow-Headers "Auth-Token, Content-Type"; + add_header Access-Control-Allow-Methods "GET, OPTIONS, POST, DELETE, PUT, PATCH"; + add_header Access-Control-Allow-Credentials "true"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + location /btc/ { + proxy_pass http://localhost:8011; + if ($request_method = OPTIONS ) { + add_header Access-Control-Allow-Headers "Auth-Token, Content-Type"; + add_header Access-Control-Allow-Methods "GET, OPTIONS, POST, DELETE, PUT, PATCH, UPGRADE"; + add_header Access-Control-Allow-Credentials "true"; + add_header Content-Length 0; + add_header Content-Type text/plain; + return 200; + } + } + } +} +``` + + ## Running Before starting anything make sure you have PostgreSQL, MongoDB and Redis up and running. diff --git a/auth-codecard/src/main/scala/AuthCodeCard.scala b/auth-codecard/src/main/scala/AuthCodeCard.scala index e1c29da..00f095f 100644 --- a/auth-codecard/src/main/scala/AuthCodeCard.scala +++ b/auth-codecard/src/main/scala/AuthCodeCard.scala @@ -28,35 +28,37 @@ object AuthCodeCardCard extends App with JsonProtocols with Config { Http().bindAndHandle(interface = interface, port = port, handler = { logRequestResult("auth-codecard") { - (path("register" / "codecard" ) & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token")) { (tokenValue) => - complete { - service.register(tokenValue).map[ToResponseMarshallable] { - case Right(response) => Created -> response - case Left(errorMessage) => BadRequest -> errorMessage + pathPrefix("auth-codecard") { + (path("register") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token")) { (tokenValue) => + complete { + service.register(tokenValue).map[ToResponseMarshallable] { + case Right(response) => Created -> response + case Left(errorMessage) => BadRequest -> errorMessage + } } - } - } ~ - (path("login" / "codecard" / "activate") & pathEndOrSingleSlash & post & entity(as[ActivateCodeRequest])) { (request) => - complete { - service.activateCode(request).map[ToResponseMarshallable] { - case Right(response) => OK -> response - case Left(errorMessage) => BadRequest -> errorMessage + } ~ + (path("activate") & pathEndOrSingleSlash & post & entity(as[ActivateCodeRequest])) { (request) => + complete { + service.activateCode(request).map[ToResponseMarshallable] { + case Right(response) => OK -> response + case Left(errorMessage) => BadRequest -> errorMessage + } } - } - } ~ - (path("login" / "codecard") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[LoginRequest])) { (tokenValue, request) => - complete { - service.login(request, tokenValue).map[ToResponseMarshallable] { - case Right(response) => Created -> response - case Left(errorMessage) => BadRequest -> errorMessage + } ~ + (path("login") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[LoginRequest])) { (tokenValue, request) => + complete { + service.login(request, tokenValue).map[ToResponseMarshallable] { + case Right(response) => Created -> response + case Left(errorMessage) => BadRequest -> errorMessage + } } - } - } ~ - (path("generate" / "codecard") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[GetCodeCardRequest])) { (tokenValue, request) => - complete { - service.getCodeCard(request, tokenValue).map[ToResponseMarshallable] { - case Right(response) => OK -> response - case Left(errorMessage) => BadRequest -> errorMessage + } ~ + (path("generate") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[GetCodeCardRequest])) { (tokenValue, request) => + complete { + service.getCodeCard(request, tokenValue).map[ToResponseMarshallable] { + case Right(response) => OK -> response + case Left(errorMessage) => BadRequest -> errorMessage + } } } } diff --git a/auth-fb/src/main/scala/AuthFb.scala b/auth-fb/src/main/scala/AuthFb.scala index 89c9c12..9061646 100644 --- a/auth-fb/src/main/scala/AuthFb.scala +++ b/auth-fb/src/main/scala/AuthFb.scala @@ -23,27 +23,29 @@ object AuthFb extends App with JsonProtocols with Config { Http().bindAndHandle(interface = interface, port = port, handler = { logRequestResult("auth-fb") { - (path("register" / "fb") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => - complete { - service.register(authResponse, tokenValue) match { - case SuccessT(f) => f.map[ToResponseMarshallable] { - case Right(identity) => Created -> identity - case Left(errorMessage) => BadRequest -> errorMessage + pathPrefix("auth-fb") { + (path("register") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => + complete { + service.register(authResponse, tokenValue) match { + case SuccessT(f) => f.map[ToResponseMarshallable] { + case Right(identity) => Created -> identity + case Left(errorMessage) => BadRequest -> errorMessage + } + case FailureT(e: FacebookException) => Unauthorized -> e.getMessage + case _ => InternalServerError } - case FailureT(e: FacebookException) => Unauthorized -> e.getMessage - case _ => InternalServerError } - } - } ~ - (path("login" / "fb") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => - complete { - service.login(authResponse, tokenValue) match { - case SuccessT(f) => f.map[ToResponseMarshallable] { - case Right(token) => Created -> token - case Left(errorMessage) => BadRequest -> errorMessage + } ~ + (path("login") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => + complete { + service.login(authResponse, tokenValue) match { + case SuccessT(f) => f.map[ToResponseMarshallable] { + case Right(token) => Created -> token + case Left(errorMessage) => BadRequest -> errorMessage + } + case FailureT(e: FacebookException) => Unauthorized -> e.getMessage + case _ => InternalServerError } - case FailureT(e: FacebookException) => Unauthorized -> e.getMessage - case _ => InternalServerError } } } diff --git a/auth-password/src/main/resources/application.conf b/auth-password/src/main/resources/application.conf index 1b0c6fd..503452f 100644 --- a/auth-password/src/main/resources/application.conf +++ b/auth-password/src/main/resources/application.conf @@ -1,3 +1,4 @@ + akka { loglevel = DEBUG } @@ -10,7 +11,7 @@ http { db { url = "jdbc:postgresql://localhost:5432/auth_password" user = "postgres" - password = "postgres" + password = "" } services { @@ -23,4 +24,5 @@ services { host = "localhost" port = 8010 } -} \ No newline at end of file +} + diff --git a/auth-password/src/main/scala/AuthPassword.scala b/auth-password/src/main/scala/AuthPassword.scala index 3242250..a634665 100644 --- a/auth-password/src/main/scala/AuthPassword.scala +++ b/auth-password/src/main/scala/AuthPassword.scala @@ -24,36 +24,38 @@ object AuthPassword extends App with JsonProtocols with Config { Http().bindAndHandle(interface = interface, port = port, handler = { logRequestResult("auth-password") { - path("register" / "password") { - (pathEndOrSingleSlash & post & entity(as[PasswordRegisterRequest]) & optionalHeaderValueByName("Auth-Token")) { - (request, tokenValue) => - complete { - service.register(request, tokenValue).map[ToResponseMarshallable] { - case Right(identity) => Created -> identity - case Left(errorMessage) => BadRequest -> errorMessage - } + pathPrefix("auth-password") { + path("register") { + (pathEndOrSingleSlash & post & entity(as[PasswordRegisterRequest]) & optionalHeaderValueByName("Auth-Token")) { + (request, tokenValue) => + complete { + service.register(request, tokenValue).map[ToResponseMarshallable] { + case Right(identity) => Created -> identity + case Left(errorMessage) => BadRequest -> errorMessage + } + } } - } - } ~ - path("login" / "password") { - (pathEndOrSingleSlash & post & entity(as[PasswordLoginRequest]) & optionalHeaderValueByName("Auth-Token")) { - (request, tokenValue) => - complete { - service.login(request, tokenValue).map[ToResponseMarshallable] { - case Right(token) => Created -> token - case Left(errorMessage) => BadRequest -> errorMessage - } - } - } - } ~ - path("reset" / "password") { - (pathEndOrSingleSlash & post & entity(as[PasswordResetRequest]) & headerValueByName("Auth-Token")) { - (request, tokenValue) => - complete { - service.reset(request, tokenValue).map[ToResponseMarshallable] { - case Right(identity) => OK -> identity - case Left(errorMessage) => BadRequest -> errorMessage + } ~ + path("login") { + (pathEndOrSingleSlash & post & entity(as[PasswordLoginRequest]) & optionalHeaderValueByName("Auth-Token")) { + (request, tokenValue) => + complete { + service.login(request, tokenValue).map[ToResponseMarshallable] { + case Right(token) => Created -> token + case Left(errorMessage) => BadRequest -> errorMessage + } + } } + } ~ + path("reset") { + (pathEndOrSingleSlash & post & entity(as[PasswordResetRequest]) & headerValueByName("Auth-Token")) { + (request, tokenValue) => + complete { + service.reset(request, tokenValue).map[ToResponseMarshallable] { + case Right(identity) => OK -> identity + case Left(errorMessage) => BadRequest -> errorMessage + } + } } } } diff --git a/build.sbt b/build.sbt index 56857cc..23d4fba 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,6 @@ version := "1.0" lazy val `reactive-microservices` = (project in file(".")) -lazy val `frontend-server` = project in file("frontend-server") lazy val metricsCommon = project in file("metrics-common") @@ -42,7 +41,6 @@ val cleanAll = taskKey[Unit]("Cleans all subprojects") compileAll := { fork in compile := true - (compile in Compile in `frontend-server`).toTask.value (compile in Compile in `token-manager`).toTask.value (compile in Compile in `session-manager`).toTask.value (compile in Compile in `identity-manager`).toTask.value @@ -53,7 +51,6 @@ compileAll := { } cleanAll := { - (clean in Compile in `frontend-server`).toTask.value (clean in Compile in `token-manager`).toTask.value (clean in Compile in `session-manager`).toTask.value (clean in Compile in `identity-manager`).toTask.value @@ -65,7 +62,6 @@ cleanAll := { runAll := { - (run in Compile in `frontend-server`).evaluated (run in Compile in `token-manager`).evaluated (run in Compile in `session-manager`).evaluated (run in Compile in `identity-manager`).evaluated diff --git a/frontend-server/build.sbt b/frontend-server/build.sbt deleted file mode 100644 index 1e07a36..0000000 --- a/frontend-server/build.sbt +++ /dev/null @@ -1,22 +0,0 @@ -name := "frontend-server" - -version := "1.0" - -scalaVersion := "2.11.5" - -scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8") - -resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" - -libraryDependencies ++= { - val akkaV = "2.3.10" - val akkaStreamV = "1.0-RC2" - Seq( - "com.typesafe.akka" %% "akka-actor" % akkaV, - "com.typesafe.akka" %% "akka-stream-experimental" % akkaStreamV, - "com.typesafe.akka" %% "akka-http-core-experimental" % akkaStreamV, - "com.typesafe.akka" %% "akka-http-scala-experimental" % akkaStreamV - ) -} - -Revolver.settings diff --git a/frontend-server/project/plugins.sbt b/frontend-server/project/plugins.sbt deleted file mode 100644 index 2a01432..0000000 --- a/frontend-server/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") diff --git a/frontend-server/src/main/resources/application.conf b/frontend-server/src/main/resources/application.conf deleted file mode 100644 index cffad07..0000000 --- a/frontend-server/src/main/resources/application.conf +++ /dev/null @@ -1,12 +0,0 @@ -akka { - loglevel = DEBUG -} - -http { - interface = "0.0.0.0" - port = 8080 -} - -static { - directory = "./frontend-server/web" -} \ No newline at end of file diff --git a/frontend-server/src/main/scala/FrontendServer.scala b/frontend-server/src/main/scala/FrontendServer.scala deleted file mode 100644 index 7c24409..0000000 --- a/frontend-server/src/main/scala/FrontendServer.scala +++ /dev/null @@ -1,18 +0,0 @@ -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Directives._ -import akka.stream.ActorFlowMaterializer -import com.typesafe.config.ConfigFactory - -object FrontendServer extends App { - val config = ConfigFactory.load() - val interface = config.getString("http.interface") - val port = config.getInt("http.port") - val staticDirectory = config.getString("static.directory") - - implicit val actorSystem = ActorSystem() - implicit val materializer = ActorFlowMaterializer() - implicit val dispatcher = actorSystem.dispatcher - - Http().bindAndHandle(interface = interface, port = port, handler = getFromDirectory(staticDirectory)) -} diff --git a/tutorial/index.html b/tutorial/index.html index 87cadc3..c6831b0 100644 --- a/tutorial/index.html +++ b/tutorial/index.html @@ -5,19 +5,33 @@
Reactive microservices is an activator template completely devoted to microservices architecture. It lets you learn about microservices in general — different patterns, communication protocols and 'tastes' of microservices. All these concepts are demonstrated using Scala, Akka, Play and other tools from Scala ecosystem. For the sake of clarity, we skipped topics related to deployment and operations — that's a great subject for another big activator template.
-+
To feel comfortable while playing with this template, make sure you know basics of Akka HTTP which is a cornerstone of this project. We recently released an Akka HTTP activator template that may help you start. At least brief knowledge of Akka remoting, Akka persistence, Akka streams and Play Framework websockets is also highly recommended. Anyway, don't worry — all these technologies (and more!) will be discussed on the way but we won't dig into the details.
- -This activator template consists of 10 runnable subprojects — the microservices:
Take some time to review application.conf
files that are located in resource
subdirectory of each microservice.
; project metrics-collector; run 5001
There's no formal or widespread definition of a microservice. However usually microservices are defined as an software architectural style in which system is composed of multiple services. Those services are small (or at least smaller than in typical, monolith applications), can be independently deployed and they communicate using (lightweight) protocols. Well–defined microservice should be organized around business capabilities or to put it in Domain–Driven Design wording — should encapsulate Bounded Context. Sometimes it is said that microservices are implementation of Single Responsibility Principle in architecture.
-Applications based on a microservice architecture are basically distributed systems. This means that they can scale horizontally in a much more flexible way than monolithic systems — instead of replicating whole heavyweight process one can spawn multiple instances of services that are under load. This guarantees better hardware utilization — money savings. Another important consequence of moving from monolith to microservices is the need of designing for failure which can result in a truly reactive system. Like it or not, while designing a distributed system you have to take failure into account — otherwise you will see your system falling apart. @@ -109,15 +137,23 @@
- A shift from monolithic to microservices architecture is a serious step. Distributed systems are totally different than monolithic ones and have their own dilemmas and problems. First and foremost microservices are all about communication and protocols. One should be aware that microservices doesn't magically suppress complexity — they just move it from code to communication layer. Different communication protocols (synchronous and asynchronous) and transport guarantees will be the main subject of this tutorial (see chapters 4, 5, 6, 7). Another important subject worth mentioning while discussing microservices is a polyglot persistence — how to embrace multiple different data stores and not lose consistency and performance. You can check out this approach in our activator template in chapter 3. Very common question asked while developing microservices is 'how big is a microservice?' — we'll discuss it in chapter 3. Microservice approach requires some boilerplate; one may be tempted to share code using shared libraries which introduce coupling — why and when would you like to do that? See chapter 5. There's also a multitude of the problems which are out of the scope of this template like: testing (why, when and how to do it?), polyglot approach (is it worth the cost?), operations, contract management (what's my API, who are my collaborators, how can I contact them?), API versioning (how to stay backward compatible?), logging & debugging and security. Feel encouraged to enrich this activator template with suitable examples and tutorials — and let us know! + A shift from monolithic to microservices architecture is a serious step. Distributed systems are totally different than monolithic ones and have their own dilemmas and problems. First and foremost microservices are all about communication and protocols. One should be aware that microservices doesn't magically suppress complexity — they just move it from code to communication layer. Different communication protocols (synchronous and asynchronous) and transport guarantees will be the main subject of this tutorial (see chapters 3.1., 3.2., 3.3., 3.4.). Another important subject worth mentioning while discussing microservices is a polyglot persistence — how to embrace multiple different data stores and not lose consistency and performance. You can check out this approach in our activator template in chapter 3. Very common question asked while developing microservices is 'how big is a microservice?' — we'll discuss it in chapter 3. Microservice approach requires some boilerplate; one may be tempted to share code using shared libraries which introduce coupling — why and when would you like to do that? See chapter 3.1. There's also a multitude of the problems which are out of the scope of this template like: testing (why, when and how to do it?), polyglot approach (is it worth the cost?), operations, contract management (what's my API, who are my collaborators, how can I contact them?), API versioning (how to stay backward compatible?), logging & debugging and security. Feel encouraged to enrich this activator template with suitable examples and tutorials — and let us know!
+ +To present different concepts related to microservices we built an authentication system. The idea behind it is really simple — you can sign in/sign up using arbitrarily chosen authentication methods (currently they're email–password, Facebook Oauth, codecard). Number of used authentication methods indicates user's token strength. User that presents a valid authentication token can access business applications behind the authentication system. To test if our authentication system actually works we integrated a simple application that after singing in lets users subscribe and get notifications in real–time about bitcoin market events such as rate change, volume above/below certain level etc. @@ -142,9 +178,17 @@
Synchronous communication is a data transfer method in which receiving and sending are governed by strict timing signals. It's usually takes the form of a request–response protocol — party sends data only when it's explicitly asked for it. That's how typical HTTP service works. Synchronous protocols are very popular because they're easy to understand and analyze. However they have one significant drawback — they don't scale well. First of all, synchronous protocols introduce liveness issues — when you ask for something you have to be prepared that your call may explicitly fail or even worse you may never get any response at all (that's why you need timeouts). Secondly, usually if you ask for something, you have to wait for the response to continue processing. Several such calls and you'll end up waiting most of the time instead of doing something actually useful. Having said that, world wide web we use daily is mostly synchronous — you click 'Log in' button and you wait for 'Login successful' confirmation box. Request–response — that's how vanilla HTTP works. Synchronous communication is also useful (well, almost unavoidable) while designing microservice–based system — learn why and when.
@@ -182,16 +226,19 @@- token-manager is a focal point of our authentication system. Authentication method services gets a fresh token for logged in users from it and business services verifies tokens presented by users to check identities. token-manager is built using previously presented layered architecture (routes — service — repository) but it misses gateway as it doesn't initiate communication with other services. It's the most important service in whole project so it's equipped with custom metrics reporting — there's processing time reporting for every request and success/failure reporting for each action. You'll learn how it works under the hood in the next chapter. + token-manager is a focal point of our authentication system. Authentication method services gets a fresh token for logged in users from it and business services verifies tokens presented by users to check identities. token-manager is built using previously presented layered architecture (routes — service — repository) but it misses gateway as it doesn't initiate communication with other services. It's the most important service in whole project so it's equipped with custom metrics reporting — there's processing time reporting for every request and success/failure reporting for each action. You'll learn how it works under the hood in the next chapter.
In the microservice architecture you usually have services that offer public API accessible by clients and internal API that is being used only by other services. Sometimes service offers some features that are public and others that are strictly private. That's the case with token-manager — you want your clients to be able to log out (delete token) but you don't want them to be able to add new token without going through one of auth services. This could be easily handled by a proxy server (like HAProxy or nginx) but sometimes you may want to do more complex transformations (ex. change API). In this case you should write a proxy service. Session manager is an exemplary proxy service that changes API and hides internals.
+Asynchronous communication, unlike synchronous, is not restricted by any timing signals. Asynchronous communication is usually implemented by message passing (vs request–response) — 'telling' (vs 'asking'). Asynchronous protocols usually scales better as communicating parties don't have to wait. However asynchronous message passing is unnatural, can get really complex thus it's very often complicated and hard to debug. Nonetheless, truly reactive systems should relay on asynchronous message–passing — as stated in Reactive Manifesto.
@@ -222,20 +269,28 @@If you want to have a closer look at this schema go here.
+ +Another way to provide asynchronicity in the world of webservers are websockets. Websockets are similar to TCP sockets but additionally they provide minimal framing. This makes them ideal for asynchronous message passing. You've seen them in action in metrics-collector but this time we'll analyse more complex service.
- btc-ws is a façade service for business part of our application. It allows users, after authentication, to manage subscriptions for BTC market events and receive alerts. While one could argue that user's actions can be handled synchronously, market events are asynchronous and should be handled like that. Websockets are perfect fit for such case — otherwise we would have to use HTTP polling which is inefficient. First thing to notice in the btc-ws code is a long block of mappings needed for a websocket message — scala object translations. Websocket initialization code is really straightforward — it retrieves user's identity based on presented token and opens websocket handled by WebSocketHandler
actor. You'll learn what happens there in the next chapter.
+ btc-ws is a façade service for business part of our application. It allows users, after authentication, to manage subscriptions for BTC market events and receive alerts. While one could argue that user's actions can be handled synchronously, market events are asynchronous and should be handled like that. Websockets are perfect fit for such case — otherwise we would have to use HTTP polling which is inefficient. First thing to notice in the btc-ws code is a long block of mappings needed for a websocket message — scala object translations. Websocket initialization code is really straightforward — it retrieves user's identity based on presented token and opens websocket handled by WebSocketHandler
actor. You'll learn what happens there in the next chapter.
The 'go–to' tool when it comes to asynchronous message passing in Scala world is of course Akka. It has great capabilities and convenient interface but it comes with a cost. If you chose Akka as a communication protocol for a significant part of your project you're losing many of the benefits of polyglot approach. First of all, if you want to interoperate with Akka, you have to be on JVM and preferably use Java or Scala. Your code may also become more tightly coupled — Akka encourages usage of shared libraries and data structures. As a result, in certain cases it might be better to consider using lightweight message queues such as RabbitMQ or Kafka to avoid aforementioned drawbacks but if you're sure you won't be leaving JVM anytime soon, Akka is definitely the best choice. That was also our decision in the fully asynchronous part of our system.
@@ -253,12 +308,17 @@
btc-users is a microservice completely based on Akka. It consists of three actors types: UsersManager
, UserHandler
and DataFetcher
. UsersManager
plays a role of supervisor. It responds to requests from btc-ws to create a handler (UserHandler
actor) for user with given id. UserHandler
is where all the heavy lifting happens. It's a persistent actor that processes subscription requests and issues alarms based on ticker from BTC market. First and foremost we once again leveraged the polyglot persistence approach — we used Akka persistence to persist subscription settings. Subscribe/unsubscribe actions are a perfect cases for event sourcing and that's exactly how we implemented it — see receiveCommand
and receiveRecover
. Besides handling subscribe/unsubscribe requests, UserHandler
responses to QuerySubscriptions
and broadcasts market alarms. UserHandler
actor manages its lifetime similar to how btc-ws does — by heartbeats and timeouts. DataFetcher
is a very simple actor that every few seconds fetches BTC ticker and broadcasts it to all UserHandler
actors via Akka router. That's it — that's how subscriptions are managed and market alarms are issued.
During our tour of microservices we hopefully showed the full power that comes with mixing different approaches, techniques and tools. Scala's toolbelt (and especially Akka and Play) is, without a question, ready for building reactive microservice–based distributed systems. However before migrating all your project to MSA make sure you deeply understand all dilemmas and problems of microservices, particularly ones we had to omit to keep this activator template concise like: eventual consistency, testing, operations & deployment, contract managing, versioning, monitoring, logging & debugging and security. Good luck, have fun and let us know about your adventures on the microservice way!
+