Service API

In order to define our Cook as a service we need to implement a special interface called a service descriptor. The service descriptor defines two aspects of a Lagom service:

  • The service signature: How the service should be called and its return type
  • The service meta data: How the service call is mapped to the transport layer, for example to the REST call

The service descriptor extends Lagom's Service trait and in its simplest form just needs to override the descriptor method. This is what it looks like in our Cook definition, which we place into the cook-api module:

import com.lightbend.lagom.scaladsl.api._
import ch15.model._

trait CookService extends Service {
def cook: ServiceCall[Dough, RawCookies]

override def descriptor: Descriptor = {
import Service._
named("CookService").withCalls(call(cook))
}
}

Here we define a descriptor that connects the single call of the service, the cook method, to the service identifier "CookService" which will be needed for the routing later. For the call, we use the simplest identifier which just takes the name of the method. The configuration will result in the call mapped to the /cook REST URL.

The call itself is defined to be of the ServiceCall[Dough, RawCookies] type. Let's take a look at ServiceCall in more detail. The definition in the Lagom's source code looks like this:

trait ServiceCall[Request, Response] {
def invoke(request: Request): Future[Response]
}

ServiceCall is typed by the request and response and can be invoked at the moment the request is issued by the client producing a response asynchronously.

The request and response types in Lagom can be strict or streamed. There are four possible combinations ranging from both sides being strict to both sides being streamed. Strict means that the request or response is fully buffered in the memory at the method boundary. The combination of both the strict request and response results in the synchronous semantics of the call. A streamed request or response is of the Source type, which is known to us from Chapter 13, Basics of Akka Streams where we looked at Akka streams. For the streaming calls, Lagom will try its best to choose the appropriate semantics. Typically, this will be WebSockets, and we will see how it works later in this chapter.

Our Cook instance is very quick so it is appropriate to define the service in synchronous terms.

The implementation of Cook goes in another module, cook-impl. This separation is essential in order to give microservices a possibility to refer to the definitions of each other without having any knowledge about implementation details.

The implementation is somewhat more involving, but not because of the service definition itself. The code should be very familiar by now:

package ch15

import ch15.model._
import com.lightbend.lagom.scaladsl.api._

import scala.concurrent.Future

class CookServiceImpl extends CookService {
override def cook = ServiceCall { dough =>
Future.successful(RawCookies(makeCookies(dough.weight)))
}
private val cookieWeight = 60
private def makeCookies(weight: Int): Int = weight / cookieWeight
}

The only new part here is the definition of the service call wrapper. It is done by using the constructor defined in the ServiceCall companion object:

 def apply[Request, Response](call: Request => Future[Response]): ServiceCall[Request, Response]

We provide a function that converts Dough (request) into Future[RawCookies] (response) and the constructor builds a proper ServiceCall from it.

The previously mentioned complexity is related to the fact that we also need to wire together and start our service. For those who read Chapter 14, Project 1 - Building Microservices with Scala, the approach will look very much like a combination of both approaches we looked at there: mixing traits and providing concrete implementations for abstract members and passing dependencies as constructor parameters. But this time, we'll get the Lagom's help for this task. First, we define LagomApplication:

abstract class CookApplication(context: LagomApplicationContext)
extends LagomApplication(context) {
override lazy val lagomServer: LagomServer = serverFor[CookService](wire[CookServiceImpl])
}

The application extends LagomApplication and needs LagomApplicationContext, which is just passed over via a constructor. No doubt you recognize the thin-cake pattern we used to connect together the components of our Akka-HTTP example. lagomServer is an overridden method, which is used by Lagom to provide correct wiring for the service calls. Another wiring happening here is the binding of CookServiceImpl to CookService with the help of Macwire.

Macwire (https://github.com/adamw/macwire) is a dependency injection framework that implements constructor-based DI. It does so by generating calls to the class constructors with appropriate parameters found in scope. In a sense, it provides proper constructor calls behind the scenes the same way Circe or Play provide proper mapping to JSON structures. It would be very useful in projects of significant size.

Now our application can be used in the application loader, which does the real work of starting the service in a development or production environment:

class CookLoader extends LagomApplicationLoader {

override def load(context: LagomApplicationContext) =
new CookApplication(context) with AhcWSComponents {
override def serviceLocator: ServiceLocator = NoServiceLocator
}

override def loadDevMode(context: LagomApplicationContext) =
new CookApplication(context) with AhcWSComponents with LagomDevModeComponents
}

CookLoader can be started by Lagom as needed. It overrides two load methods for respective environments. Please note how we extended CookApplication with AhcWSComponents. The latter is needed in order to provide wsClient, which in turn is required by LagomApplication we defined as a base class for our CookApplication. For the development mode, we also mix in LagomDevModeComponents, which gives us a development mode service locator. 

Now we need to configure the application loader by providing a corresponding play setting in the well-known by now application.conf:

play.application.loader = ch15.CookLoader

And that is it—now we are ready to start our application. The easiest way to do this is by using Lagom's runAll command in the SBT console. It will try to start all of the services we've defined so far as well as the components of the underlying infrastructure—the development mode service locator, Cassandra database, and Kafka message broker:

sbt:bakery> runAll
[info] Starting Kafka
[info] Starting Cassandra
[info] Cassandra server running at 127.0.0.1:4000
[info] Service locator is running at http://localhost:9008
[info] Service gateway is running at http://localhost:9000
[info] Service cook-impl listening for HTTP on localhost:57733
[info] Service baker-impl listening for HTTP on localhost:50764
[info] Service manager-impl listening for HTTP on localhost:63552
[info] Service chef-impl listening for HTTP on localhost:56263
[info] Service boy-impl listening for HTTP on localhost:64127

The logs witness that the logging is working alongside other infrastructure components.

At this stage, the log will contain a lots of stacktraces (not shown here) because of missing loader configurations for all but the boy-impl modules. We will fix this during this chapter, as we will implement the services one after another.

We can also see that our service is running on port 57733 and can try to communicate with it:

slasch@void:~$ curl -X POST http://localhost:57733/cook -d '{ "weight": 100 }'
{"count":1}

Congratulations, we just talked to our first Lagom microservice!

We communicated to the service directly without using a service registry and service locator. It is safe to put the port number into the code listing for reference because despite their random appearance, ports are assigned to the services by Lagom in a deterministic manner (basically by using a hash of a project name). Hence, the services are assigned the same port numbers (with respect to port conflicts) in any environment.

Now we can move on to the implementation of the Boy service, which is similarly simple in its functionality. It is expected to forward incoming shopping lists to external services and forward groceries it will get in return to the initial caller.

The definition of the service should look familiar, except that we're using the namedCall method to map the shop call to the go-shopping name in order to have a nicer URL:

trait BoyService extends Service {
def shop: ServiceCall[ShoppingList, Groceries]

override def descriptor: Descriptor =
named("BoyService").withCalls(namedCall("go-shopping", shop))
}

The implementation is a bit more complex then the Cook service because the Boy service needs to call an external HTTP service to make an order. The following template should not raise any questions:

class BoyServiceImpl extends BoyService {
override def shop = ServiceCall(callExternalApi)
private val callExternalApi: ShoppingList => Future[Groceries] = ???
}

How do we call the external API though? We could, of course, use an HTTP client library and do the call the same way as before, issuing the HTTP request, getting the HTTP response, and handling marshalling and unmarshalling. But this would lower the abstraction level of our solution and hard-wire the implementation to the external service's location.

We will do the following instead. First, we will register our externally running service with the service locator by adding the service's URL to build.sbt:

lagomUnmanagedServices in ThisBuild := Map("GroceryShop" -> "http://localhost:8080")

Then, we will define an API for the grocery store as if we were about to implement it:

trait ShopService extends Service {
def order: ServiceCall[Order, Purchase]
override def descriptor: Descriptor = {
named("GroceryShop").withCalls(restCall(Method.POST, "/purchase", order))
}
}

Here, we specify a service descriptor for the service with the same name as we just registered with the service locator. The API call is registered with a restCall descriptor to be sure that both the HTTP method and the path are correctly mapped to the existing service. We also need to wrap and unwrap ShoppingList and Groceries into proper Order and Purchase as expected with the existing shop service. Luckily, the JSON representation of our case classes is the same as for Map[String, Int] so we can safely just reuse the existing model along with serializers and add the wrappers on top of it:

object ShopService {
final case class Order(order: ShoppingList)
final case class Purchase(order: Groceries)
implicit val purchase: Format[Purchase] = Json.format
implicit val order: Format[Order] = Json.format
}

We don't need to provide an implementation for ShopService; we just want Lagom to apply all existing machinery to represent an existing REST service as if it were one made with Lagom.

The shop service is ready to use with Boy now:

class BoyServiceImpl(shopService: ShopService)                    
(implicit ec: ExecutionContext) extends BoyService {
override def shop: ServiceCall[ShoppingList, Groceries] = ServiceCall(callExtApi)

private val callExtApi: ShoppingList => Future[Groceries] = list =>
shopService.order.invoke(Order(list)).map(_.order).recover {
case _ => Groceries(0, 0, 0, 0)
}
}

Note that we provide the shopService client and an execution context. The latter will be used to transform the future result we'll get from the service invocation. The callExtApi function shows how this is done: we refer to the order method from the ShopService definition, which returns ServiceCall, which we happily invoke with Order we created from the shopping list. The result is Future[Purchase] so we unwrap an order out of it. Finally, we define that, if anything wrong happens with the external service, for example, the service is not available or there is insufficient inventory to fulfill the order, the Boy should just return back with empty hands.

Now Boy is able to communicate with ShopService we built in Chapter 14Project 1 - Building Microservices with Scala using Akka-HTTP.

The shop service must be running and must have sufficient inventory in order for further examples from this chapter to work properly.

Our Boy and Cook services are stateless. The Cook service just returns the result immediately so there is no point having any state in it. Boy is unsophisticated and just comes back for instructions if anything unexpected happens. But Chef and Baker are different because they are supposed to represent processes taking some time. For this reason we can't implement them in a synchronous manner.

The Baker has m:n semantics in the sense that it can respond with zero, one, or many response messages to a single incoming message. Let's use Lagom's possibility to define asynchronous services to implement it. This will allow us to reuse the flow definition for Baker we constructed in Chapter 13, Basics of Akka Streams.

We first need to define the service as we already did, but this time with asynchronous semantics:

import akka.NotUsed
import akka.stream.scaladsl.Source

trait BakerService extends Service {
def bake: ServiceCall[Source[RawCookies, NotUsed],
Source[ReadyCookies, NotUsed]]

override def descriptor: Descriptor = named("BakerService").withCalls(call(bake))
}

Here, we define BakerService to have a request of the Source[RawCookies, NotUsed] type and the response of the Future[Source[ReadyCookies, NotUsed]] type. This should allow us to just write RawCookies at the moment they are available and get ReadyCookies back after they are baked.

The implementation is straightforward because it is literally wrapping the flow taken from Chapter 13Basics of Akka Streams:

import play.api.Logger

class BakerServiceImpl extends BakerService {

private val logger = Logger("Baker")

override def bake: ServiceCall[Source[RawCookies, NotUsed],
Source[ReadyCookies, NotUsed]] =
ServiceCall { dough =>
logger.info(s"Baking: $dough")
Future.successful(dough.via(bakerFlow))
}

private val bakerFlow: Flow[RawCookies, ReadyCookies, NotUsed] =
Baker.bakeFlow.join(Oven.bakeFlow)
}

We reuse the definition of the Baker and Oven flows and return the combined flow as the result of the call. In this snippet, we also demonstrate the use of Logger available from the underlying Play framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.98.207