Service composition helps you create results by combining dependent information from underlying services. You can compose well or poorly, depending on how you manage the underlying services and calls to those services.
A few libraries have been created to help you compose calls to services and combine the results. Generally, the more recent service composition libraries are based on simple ideas such as managing the creation of the request separate from executing the requests. Of course, this is the basic idea behind the IO monad found in haskell and a variety of other languages that allow you to control evaluation. We are also concerned about how to catch and manage error information once errors do occur.
First we can address service composition using fetch. It is interesting to note that the scala async macro helps you compose Future's in simple scenarios that match what fetch does when you use Future's with fetch.
fetch
Fetch composes service calls and combines the results together. Here's the main documentation site. It handles batching, request combining and caching. While there may be better ways to handle caching, it allows you to address caching at the fetch library level versus an infrastructure level.
First, we need to enhance our example-server to serve up some data. You can easily create a contrived customer micro-service with akka-http:
~ pathPrefix("customers") { path(IntNumber) { n => get { onSuccess(customers.find(n)) {case Some(customer) => complete(customer)case None => complete(StatusCodes.NotFound) } } } ~ path("posts-from"/ IntNumber) { n => get { onSuccess(msgs.getMsgsFrom(n)) { m => complete(m) } } } ~ path("posts-to"/ IntNumber) { n => get { onSuccess(msgs.getMsgsTo(n)) { m => complete(m) } } } } }
which is powered by some new modules:
package exampleimport scala.annotation.implicitNotFoundimport scala.annotation.migrationimport scala.concurrent.Futureimport scala.language._import spray.json.DefaultJsonProtocolcaseclass Customer(id: Int, name: String, address: String)// I use sleep which probably does not do what I want// exactly given how actors work but is good enough for this example.trait CustomerRespository extends DefaultJsonProtocol {privateval r = scala.util.Randomprivateval customers = Map(1-> Customer(1, "John", "123 Play St."),2-> Customer(2, "Mary", "495 Norm St."),3-> Customer(3, "Beth", "391 Happy St.")) /** Get a customer, pretend it has latency by using a Future ... */deffind(id: Int)(implicit ec: concurrent.ExecutionContext) = Future { Thread.sleep(r.nextInt(4000)) customers.get(id) } /** Return all the IDs known in the customer database ... */defids(implicit ec: concurrent.ExecutionContext) = Future { Thread.sleep(r.nextInt(2000)) customers.keys.toList }implicitval customerFormat = jsonFormat3(Customer)implicitval customerListFormt = immSeqFormat[Customer]}caseclass Msg(from: Int, to: Int, content: String)trait MsgRepository extends DefaultJsonProtocol {privateval r = scala.util.Randomprivateval msgs = collection.immutable.Seq( Msg(1, 3, "yt?"), Msg(1, 1, "hi me!"), Msg(2, 1, "can't talk now"), Msg(2, 3, "how's it going?"), Msg(2, 3, "let's do it!")) /** Return a list of posts mode by a customer. */defgetMsgsFrom(id: Int)(implicit ec: concurrent.ExecutionContext) = Future { Thread.sleep(r.nextInt(3000)) msgs.filter(_.from == id) } /** Return a list of posts from a customer. */defgetMsgsTo(id: Int)(implicit ec: concurrent.ExecutionContext) = Future { Thread.sleep(r.nextInt(3000)) msgs.filter(_.to == id) }implicitval msgsFormat = jsonFormat3(Msg)implicitval msgListFormat = immSeqFormat[Msg]}
This allows us to call the service, say, using curl:
and we add the modules to the main program file. Notice the use of spray json, which makes it easy to convert a string to a JSON object to a scala JVM object.
The last import brings in implicit converts that take the json reader/writers/formats and changes them into marshallers and unmarshallers suitable for use in akka routes. akka has a robust marshaller and unmarshaller framework.
Now we can use fetch to fetch results and combine them together. Since dispatch returns a scala Future, we need to use the Future monad composition in fetch. fetch's user documentation is here.
Since dispatch returns scala Futures, we need to hook this into fetch's asynchronous support:
This allows us to get a customer object using the Fetch API. Personally, I think the fetch API will evolve so as to not be so heavy on objects and traits, but that's another day.
We can now create a fetch object, re-use as much as we want, and get a customer object.
val fetchCustomer1 = fetchCustomer(1)val result1 = Fetch.run[Future](fetchCustomer) result1 onComplete println println("Hit ENTER to end program...") scala.io.StdIn.readLine
The result prints after the prompt because we artificially inserted a small delay into the server response on the server side.
Now we can compose our customer fetches together and use a slightly different syntax to run the Fetch object (e.g. .runA[Future] vs Fetch.run[Future]):