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. Futuristic Functions
    & How to Fake Them
    David Denton
    fintrospect.io
    Scala meet up / 23rd October 2017

    View Slide

  2. 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

    View Slide

  3. 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.

    View Slide

  4. Futuristic Functions:
    Server as a Function
    explained

    View Slide

  5. • 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

    View Slide

  6. 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)
    }

    View Slide

  7. 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()

    View Slide

  8. 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"
    }
    }

    View Slide

  9. 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

    View Slide

  10. 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!

    View Slide

  11. 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:

    View Slide

  12. Server as a Function.
    Tick.
    Now what?

    View Slide

  13. • Typesafe routing
    • Reduce validation boilerplate
    • Auto-documenting (Swagger)
    • Fault tolerance
    • Lightweight dependencies
    Wish list Mk2

    View Slide

  14. fintrospect.io
    “Fintrospect is a routing library which allows
    the API user to serve and consume typesafe
    HTTP contracts.”
    So what is it?

    View Slide

  15. RouteSpec
    Service
    (Client)
    RouteClient
    bind()
    ServerRoute
    Service
    (Server)
    bind()
    Concept: RouteSpec
    Module
    ServerRoute
    ServerRoute
    ServerRoute
    Service
    Module
    ServerRoute
    ServerRoute
    ServerRoute
    Module
    ServerRoute
    ServerRoute
    ServerRoute

    • 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")

    View Slide

  16. 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

    View Slide

  17. 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”

    View Slide

  18. 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)
    }
    }

    View Slide

  19. 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!

    View Slide

  20. 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).

    View Slide

  21. 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)
    }

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

  24. fintrospect modules

    View Slide

  25. Futuristic Functions:
    Standardised.

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. Futuristic Functions:
    How to Fake Them.

    View Slide

  29. 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

    View Slide

  30. 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)

    View Slide

  31. 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

    View Slide

  32. 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!

    View Slide

  33. Thanks!
    __________?
    @daviddenton/fintrospect
    web: www.fintrospect.io fintrospect.io

    View Slide