Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Futuristic Functions and how to Fake them

Futuristic Functions and how to Fake them

A journey story about how we re-engineered a set of high-traffic services at a major Academic Publisher to use Finagle - the Server as a Function framework from Twitter.

I'll cover the concepts behind Finagle, then how we extended the application of those concepts to develop Fintrospect.io, a typesafe routing library that sits atop it. Finally, we'll see how the combination of the 2 technologies reinvented how we implemented microservice testing strategies, and allowed the applications to be fully Continuously Deployed.

Presented at the London Scala User Group.

David Denton

October 23, 2017
Tweet

More Decks by David Denton

Other Decks in Programming

Transcript

  1. Domain: Academic Publishing •Backbone app delivering metadata search services and

    asset XML for branded academic research content: •Serves 20-30 million requests per day CD build & deploy by Deployed to Monorepo in
  2. Migration == Opportunity! Issues we’d like to address: • Separation

    of route logic hard to debug • No typesafety around routing - validation boilerplate! • Loads and loads and loads of dependencies. • Hard to test end-to-end scenarios. • Network failure cases hard to simulate. • Swagger support not integrated.
  3. • 2013 Whitepaper from Marius Eriksen @ Twitter Server as

    a Function • Defined 2 major concepts: • Service represents System Boundaries • Filter used to apply I/O transformation and stateful effects • Protocol agnostic RPC system • In production @ Twitter • Scala + Netty • Fault tolerant by design • Basis for LinkerD ServiceMesh
  4. Concept: Service “It turns an Request into an (eventual) Response”

    Service: Request => Future[Response] ie. it’s a function! val service: Service[Request, Response] = Service.mk { r => val response = Response(Status.Ok) response.content = r.content Future(response) }
  5. Concept: Filter “Provides pre and post processing on a remote

    operation” Filter[ReqIn, RespIn, ReqOut, RespOut] = (ReqIn, Service[ReqOut, RespOut]) => Future[RespIn] ie. it’s a function! val addType: Filter[Request, Response, Request, Response] = Filter.mk { (req, next) => next(req).map(resp => { resp.headerMap("Content-type") = "application/json" resp }) } Filters compose with other Filters/Services using andThen()
  6. Testing Services This is trivially easy, because Services are just

    functions! val echoPath = Service.mk[Request, Response] { req => val response = Response(Status.Ok) response.contentString = req.uri Future(response) } class EchoPathTest extends FunSpec with Matchers { it("echoes path”) { Await.result(echoPath(Request("hello"))).contentString shouldBe "/hello" } }
  7. Functions on a network! val service: Service[Request, Response] = Service.mk

    { req: Request => Future(Response(Ok)) } val server: ListeningServer = Http.serve(":80", service) We can mount a Service into a running Finagle Server
  8. HTTP Clients “It turns an Request into an (eventual) Response”

    val client: Service[Request, Response] = Http.newService(”www.google.com:80”) val response: Future[Response] = client( Request(GET, "/search?query=hello") ) Service: Request => Future[Response] ie. it’s a Service!
  9. Fault tolerance through composition Resilience features are built into the

    Protocols: Load balancing, Retrying, Dead-node detection (circuits) val backOff = Stream(fromSeconds(1), fromSeconds(3), fromSeconds(5)) val shouldRetry: PartialFunction[(Request, Try[Response]), Boolean] = { case (_, Return(rep)) => rep.status.code != 200 } val resilientHttpClient = RetryFilter(backOff)(shouldRetry) .andThen( Http.newService("localhost:8000,localhost:8001")) Below example provides LB & custom retrying logic with backoff:
  10. • Typesafe routing • Reduce validation boilerplate • Auto-documenting (Swagger)

    • Fault tolerance • Lightweight dependencies Wish list Mk2
  11. fintrospect.io “Fintrospect is a routing library which allows the API

    user to serve and consume typesafe HTTP contracts.” So what is it?
  12. RouteSpec Service (Client) RouteClient bind() ServerRoute Service (Server) bind() Concept:

    RouteSpec Module ServerRoute ServerRoute ServerRoute Service Module ServerRoute ServerRoute ServerRoute Module ServerRoute ServerRoute ServerRoute <exports> • RouteSpec defines contract: • Path/Queries/Headers/Body • Content types • Responses RouteSpec("a post endpoint") .consuming(APPLICATION_ATOM_XML) .producing(APPLICATION_JSON) .taking(Query.required.int("qName")) .taking(Header.optional.boolean("name")) .body( Body.json("the body of the message", obj("anObject" -> obj("notAStringField" -> number(123))) ) ) .at(Post) / "echo" / Path.string("message")
  13. Concept: ServerRoute “Binds the RouteSpec to a typesafe Http Endpoint”

    val route: ServerRoute[Request, Response] = RouteSpec().at(Post) / Path.int("code") bindTo { code: Int => Service.mk { req: Request => Future(Response(Status(code))) } } • Path variables are passed to a function which builds the Service
  14. Concept: Module val renderer = Swagger2dot0Json(ApiInfo("my great api", "v1")) val

    module: Module = RouteModule(Root / "context", renderer) .withRoute(serverRoute1) .withRoute(serverRoute2) val svc: Service[Request, Response] = module.toService • Models a set of Routes • Provides auto-validation and Documentation • If no matching service found, falls back to a 404 “Matches a Request to a bound Service”
  15. Typesafe HTTP Contracts • How do we enforce our incoming

    HTTP contract? • Locations:Query/Header/Body/Form • Optionality - required or optional? • Marshalling + Typesafety • What about creating outbound messages? RouteSpec().at(Post) / "mine" bindTo { Service.mk { r: Request => val newBtc: Int = r.params("btc").toInt + 1 val response = Response(Ok) response.contentString = s”""{"balance":$newBtc}""" Future(response) } }
  16. Concept: Lens “A Lens targets a specific part of a

    complex object to either GET or SET a value” Extract (or <—-): (Message) => X Inject (or —->): (X) => (Message) => Message ie. it’s a function - or more precisely 2 functions!
  17. Lens example 1/2 val btc = Query.required.int("btc") RouteSpec().taking(btc).at(Post) bindTo Service.mk

    { r: Request => val newBtc: Int = (btc <-- r) + 1 val response = Response(Ok) response.contentString = s”""{"balance":$newBtc}""" Future(response) } } • Use lens extraction to provide a typed value. • Violations result in an HTTP 400 (Bad Request).
  18. Lens example 2/2 • We can also “map()” the lens

    into domain types: case class BTC(value: Int) { def +(that: BTC) = BTC(value + that.value) override def toString() = value.toString } val btcSpec = ParameterSpec.int().map(BTC, (i: BTC) => i.value) val btc = Query.required(btcSpec, "btc") RouteSpec().taking(btc).at(Post) bindTo Service.mk { r: Request => val newBtc = (btc <-- r) + BTC(1) val response = Response(Ok) response.contentString = s”""{"balance":$newBtc}""" Future(response) }
  19. Typesafe responses • Typesafe builders for creating Responses • Encode

    native API JSON objects import io.fintrospect.formats.Jackson.JsonFormat._ import io.fintrospect.formats.Jackson.ResponseBuilder._ val jsonResponse: Response = Ok(obj("balance" -> number(234))) case class Wallet(balance: Int) val encodedResponse: Response = NotFound(encode(Wallet(234))) • Or leverage auto-marshalling to convert Scala objects… import io.fintrospect.formats.Jackson.bodySpec val body: Body[Wallet] = Body.of(bodySpec[Wallet]()) val wallet: Wallet = body <-- encodedResponse … then extract the response using a lens
  20. Concept: RouteClient “Binds the RouteSpec to a typesafe Http Client”

    val client = Http.newService("localhost:80") val code = Path.int("code") val routeClient: RouteClient = RouteSpec().at(Post) / code bindToClient client val r: Future[Response] = routeClient(code --> 404) • Use lense “injection” to pass type safe bindings to client
  21. Business Abstraction Remote Client Launcher Load Environment Configuration Embedded Server

    Launch Application Stack Logging Metrics Remote Clients Application/Module(s) Business Abstraction Business Abstraction The Layer Cake Route Route Route Route
  22. Standardised Server & Clients def serverStack(host: String, app: Service[Request, Response]):

    Service[Request, Response] = logTransactionFilter(“IN”, host) .andThen(recordMetricsFilter(server)) .andThen(handleErrorsFilter()) .andThen(app) def clientStack(host: String): Service[Request, Response] = logTransactionFilter(“OUT”, systemName) .andThen(recordMetricsFilter(host)) .andThen(handleErrorsFilter()) .andThen(Http.newService(s"$host:80")) Filter.andThen(that: Filter) => Filter By utilising the ability to “stack” Filters, we can build reusable units of behaviour
  23. Fake Dependencies! Fake HTTP Dependency State • Define RouteSpecs for

    remote services, then mount them into a Fake Service • Leverage Body lenses & Response Builders • Simple state-based behaviour • Run in memory or as server • Easy to simulate failures
  24. Consumer Driven Contracts Abstract Test Contract Success scenarios Business Abstraction

    Fake Dependency Test Fake System (Service) State Failure scenarios Real Dependency Test Environment Configuration Remote Client (Service)
  25. Test Environment Configuration Application Testing • We can simply inject

    apps into each other in order to build an offline environment • Using fakes, we can inject failure to test particular scenarios • All internal and external applications and clients are Services
  26. Advantages of the new stack •Reduced vaildation/error boilerplate •No Magic

    == easy debugging •Compiler validated Auto-documentation •In-Memory testing == quicker build •CDC testing models allow for full CD •Light dependencies == small artefacts •Finagle services runs in 50-100Meg Heap + very performant!