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

Typed Services using Finch

Typed Services using Finch

Finch is an open source HTTP library built on top of Finagle, the RPC framework that powers Twitter's infrastructure. Finch is a great candidate to use when building services, and compares favourably to other popular frameworks in languages such as Ruby, Go, JavaScript, Elixir, Clojure and Haskell. This talk outlines the types of problems faced when building small services, and how using a good type system can help. I introduce & outline Finch, highlighting how it addresses these concerns. Throughout, a concrete example of an API built for a startup using Finch is used.

Tom Adams

March 02, 2016
Tweet

More Decks by Tom Adams

Other Decks in Technology

Transcript

  1. Service • We care about things like • HTTP primitives

    • Request/response encode/decode • Transport protocols • Talking to downstream services • Local data storage • But not these • Asset packaging • View rendering • JavaScript, SASS, LESS, etc.
  2. Landscape • Go: gokit, Kite • Elixir: Phoenix • Javascript:

    Node.js (TypeScript) • Clojure: Caribou, Liberator, Rook • Ruby: Rails, Sinatra, Grape, Lotus • Erlang: Leptus, Yaws • Haskell: Snap, rest, servant, Hapstack, Yesod • Java: Play, Spring, Jersey, Spark, RESTEasy, (Dropwizard) • Swift: Swifton, Vapor
  3. Service Primitives • Routing is a function • r ::

    URI → a • An “action” is a function • a :: Req → Resp • A “controller” is (scoped) a collection of actions • c :: List a • A “service” is a collection of controllers • s :: List c
  4. But wait, there’s more • HTTP primitives • Datastore •

    Metrics • Logging • JSON codec • Databinding • Configuration • Environments • HTTP clients • Failure isolation • Async primitives • Monitoring • Service discovery • Debugging/tracing • Caching • Messaging • Deployment • Testing • Live code reload • …
  5. Why Scala? • JVM support - “Better Java” • Fast,

    scalable • Deployment & runtime behaviour well understood • Library & tool support (distributed heap, debugging, etc.) • Decent (not great) static type system • Succinct - closures, type classes, type aliases, type inference, no semi-colons • Features - immutability, equational reasoning, functions, case classes, implicits, packages, mixins, currying/partial application, etc. • Library support - option, either, future, etc. • Cool stuff! scalaz, actors, higher-kinds, etc.
  6. Well used • Twitter, Pinterest, SoundCloud, Strava, Gilt, LinkedIn, Amazon,

    Tumblr, Foursquare, Box, Gigya, Simple, Localytics, LivingSocial, eHarmony, Yammer, Firebase, Disqus, Asana, Hootsuite, PagerDuty • Apple, Novell, The Guardian, Sony, BSkyB, AOL, Xerox, Siemens, VMware • REA, Seek, Skedulo, CBA, Atlassian, Ambiata, Fairfax, Data61 (NICTA), RedBalloon, Canva*, Oomph* Source: Quora, AngelList, scala-lang.org, reddit, LinkedIn, Finagle Adopters
  7. Why FP? • (Static) Types, and • Immutability, and •

    Composition, gives rise to • Equational reasoning, and • Certainty, and • Reliability
  8. Options • Karyon (Netflix) • Play (Typesafe/Lightbend) • Unfiltered (OSS)

    • Dropwizard (Yammer) • Spray (Typesafe/Lightbend) • Finagle (Twitter) / Finatra (OSS) / Finch (OSS) • Akka, Lagom (Typesafe/Lightbend) • Colossus (Tumblr)
  9. Finch Finch is a thin layer of purely functional basic

    blocks on top of Finagle for building HTTP APIs. It provides developers with simple and robust HTTP primitives, while being as close as possible to the bare metal Finagle API.
  10. Hello, World import io.finch._ import com.twitter.finagle.Http val api: Endpoint[String] =

    get("hello") { Ok("Hello, World!") } Http.serve(":8080", api.toService)
  11. Finch features • High level abstraction on top of Finagle

    (don’t need to drop down to Finagle*) • Small footprint • Flexible use (what you make of it) • Referentially transparent & compositional • Request / response decoding / encoding • Explicit async modelling
  12. Finagle A fault tolerant, protocol-agnostic, extensible RPC system for the

    JVM, used to construct high- concurrency servers. Finagle implements uniform client and server APIs for several protocols, and is designed for high performance and concurrency.
  13. Finagle Features • Connection pools (w/ throttling) • Failure detection

    • Failover strategies • Load-balancers • Back-pressure • Statistics, logs, and exception reports • Distributed tracing (Zipkin) • Service discovery (ZooKeeper) • Sharding strategies • Config
  14. TwitterServer • Lightweight server template • Command line args •

    HTTP admin server • Logging • Tracing • Metrics • System stats
  15. What does that mean for you? • Performance & scalability

    out of the box • Maturity of a battle tested framework • Fast ramp up • Won’t bottom out as you scale • Known deployment, monitoring, runtime, etc.
  16. Endpoint • A function that takes a request & returns

    a value • Automatically handles Future/async • Provides routing behaviour • Extracts/matches values from the request • Values are serialised to the HTTP response • Composable (applicative)
  17. Example val divOrFail: Endpoint[Int] = post("div" :: int :: int)

    { (a: Int, b: Int) => if (b == 0) BadRequest(new ArithmeticException("...")) else Ok(a / b) }
  18. Filter (Finagle) • Many common behaviours are service agnostic •

    Cross cutting concerns • Timeouts, retries, stats, authentication, etc. • Filters are composed over services • Alter the behaviour of a service without caring what it is
  19. Filter example val timeout: Filter[...] val auth: Filter[...] val service:

    Service[Req, Resp] val composed = timeout andThen auth andThen service
  20. Filters are Typesafe // A service that requires an authenticated

    request val service: Service[AuthReq, Resp] // Bridge with a filter val auth: Filter[HttpReq, HttpResp, AuthHttpReq, HttpResp] // Authenticate, and serve val authService: Service[HttpReq, HttpResp] = auth andThen service
  21. FUTURE • A placeholder for a value that may not

    yet exist • Long computations, network calls, disk reads, etc. • The value is supplied concurrently (executed on thread pool) • Like callbacks, but not shit • Oh, and composable (monadic)
  22. FUTURE • 3 states; empty, complete or failed • “Taints”

    the types of calling code • Easy to program against & make async explicit • Forces handling of async behaviour • Can also be blocked (if required)
  23. Future in practice val dbUser = facebook.authenticate(token).flatMap { fbUser =>

    val query = findByEmail(fbUser.email).result database.run(query).flatMap(_.headOption) } dbUser match { case Return(user) => success(user) case Throw(e) => handleError(e) }
  24. Service (Finagle) • System boundaries are represented by asynchronous functions

    called services • Symmetric and uniform API represents both clients and servers • You never (usually) write a Finagle service, Finch does that for you • Services are monadic (you’ll see this a lot…)
  25. Service object LiegeApi extends ErrorOps { private def api =

    UsersApi.usersApi() :+: RidesApi.ridesApi() def apiService: Service[Request, Response] = { val service = api.handle(errorHandler).toService RequestLoggingFilter.andThen(service) } }
  26. Databinding val ts: RequestReader[Token] = (param("t") :: param("a")).as[Token] val ts:

    RequestReader[Token] = RequestReader.derive[Token].fromParams Given a model case class Token(token: String, algorithm: String) Create a reader to parse the querystring val getToken: Endpoint[Token] = get("tokens" ? ts) { (t: Token) => ... } Automatically parse the querystring in an endpoint
  27. Databinding case class Token(token: String, algorithm: String) { "token": "CAAX...kfR",

    "algorithm": "sha1"} post("sign-in" ? body.as[Token]) { (t: Token) => ... } Given a model And incoming JSON from a POST request We can bind as
  28. “Controller” object RidesApi extends HttpOps with Logging { def ridesApi()

    = list :+: details def list: Endpoint[List[Attendance]] = get("rides" ? authorise) { u: AuthenticatedUser => ... } def details: Endpoint[Attendance] = get("rides" / string("type") / string("id") ? authorise) { (backend: String, rideId: Id, u: AuthenticatedUser) => ... } }
  29. Metrics val stats = Stats(statsReceiver) val server = Http.server.configured(stats).serve(":8081", api)

    val rides: Counter = statsReceiver.counter("rides") rides.incr() val ridesLatency: Stat = statsReceiver.stat("rides_latency") Stat.time(ridesLatency) { rides(u).map(rs => Ok(rs.map(r => Attendance(u, r)))) }
  30. HTTP Clients val client = Http.client.newService("twitter.com:8081,twitter.com:8082") val f: Future[HttpRep] =

    client(HttpReq("/")) val result: Future[String] = f.map { resp => handleResponse(resp) }
  31. When should I use it? • Complex long / lived

    system / many developers • Scale or performance requirements • Integration with downstream services • Need to run on the JVM
  32. Finch • Watch the Finch videos • https://skillsmatter.com/skillscasts/6876-finch-your-rest-api- as-a-monad •

    Examples • https://github.com/finagle/finch/tree/master/examples/src/ main/scala/io/finch • Best practices • https://github.com/finagle/finch/blob/master/docs/best- practices.md
  33. Read up on Finagle • Finagle Users Guide • Your

    function as a server (original Finagle paper) • The anatomy of a twitter microservice • Fault tolerant clients with Finagle
  34. DDL final case class User(id: Option[Int] = None, name: String,

    email: String, location: Option[String], avatarUrl: String) final class UserOps(tag: Tag) extends Table[User](tag, "users") { def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def name = column[String]("name") def email = column[String]("email") def location = column[String]("location") def * = (id.?, name, email, location.?) <>(User.tupled, User.unapply) def nameIdx = index("name_idx", name, unique = true) }
  35. DB Access object UserOps extends TableQuery(new UserOps(_)) { val findByName

    = this.findBy(_.name) val findByEmail = this.findBy(_.email) def insert(u: User) = UserOps += u def userForToken(token: UserAccessToken): Future[Option[AuthenticatedUser]] = database.run(find(token).result).map(_.headOption.flatMap(asAuthenticatedUser)) def deauthenticateUser(token: AuthToken): Future[Unit] = { val q = for {u <- UserOps if u.authToken === token.asSessionId} yield u.authToken database.run(q.update(null)).flatMap(_ => Future.Done) } }
  36. Migrations object Database { lazy val migrationDatabase = new MigrationDatabase

    { def migrate(): Unit = { val flyway = new Flyway() flyway.setDataSource(env.dbUrl, env.dbUsername, env.dbPassword) flyway.migrate() } } }
  37. JSON case class Token(token: String) object Token { val decoder:

    DecodeJson[Token] = DecodeJson { c => for { token <- (c --\ "token").as[String] } yield Token(token) } val encoder: EncodeJson[Token] = EncodeJson { t => jSingleObject("token", jString(t.token)) } }