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

Server as a Function in Kotlin - KotlinConf 2018

Server as a Function in Kotlin - KotlinConf 2018

In this talk, you'll learn about how we successfully rewrote the website of a major scientific publisher to pure Kotlin, serving millions of daily requests and in the process created the open source http4k microservice toolkit.

The talk also covers how the team migrated the stack to Continuous Delivery-based deployment into a private on-premise cloud, and how http4k helped us introduce new failure-mode and inter-service contract testing techniques.

This approach combines functional programming concepts and the versatility of Kotlin to produce applications that are simpler and more testable than most Java are accustomed to experience. And the best of it: with absolutely no magic!

Ivan Sanchez

October 04, 2018
Tweet

More Decks by Ivan Sanchez

Other Decks in Programming

Transcript

  1. Server as a Function. In Kotlin. _________________. David Denton &

    Ivan Sanchez KotlinConf - October 4th 2018
  2. The Oscar Platform •Top 1000 site globally, delivers ~10s of

    millions Req/day •Strategic journal delivery platform for a global academic publisher CD build & deploy by Deployed to Monorepo in
  3. Techrospective Issues: • Mutable HTTP model • Boilerplate around application

    setup is bloated • Magic around routing proving hard to debug • Hard to test end-to-end scenarios • Functional Java vs native Kotlin woes Action: Let’s try something in pure Kotlin
  4. BarelyMagical • Hackday project • ~40 line Kotlin wrapper for

    Utterlyidle • Use it as a library • Simple routing • Server as a function See it @ http://bit.ly/BarelyMagical
  5. Server as a Function • 2013 white paper from Marius

    Eriksen @ Twitter • Defined the composing of Services using just 2 types of asynchronous function: • Service - Represents a system boundary (symmetric) • Filter - aka middleware - Models application agnostic concerns and I/O transforms • Twitter implementation is Scala-based Finagle library • Protocol agnostic == too generic • Future-based == adds complexity
  6. Concept: Service HttpHandler “It turns an HTTP Request into an

    HTTP Response” HttpHandler: (Request) -> Response ie. it’s a function! val echo: HttpHandler = { req: Request -> Response(OK).body(req.body) } val resp: Response = echo(Request(POST, “/ echo“).body(“hello”))
  7. Concept: Filter “Provides pre and post processing on an HTTP

    operation” Filter: (HttpHandler) -> HttpHandler ie. it’s a function! val twitterFilter = Filter { next: HttpHandler -> { req: Request -> val tweet: Request = req.body(req.bodyString().take(140)) next(tweet) } } val tweet: HttpHandler = twitterFilter.then(echo)
  8. (New) Concept: Router Router: (Request) -> HttpHandler? ie. it’s a

    function! “Matches an HttpHandler against a Request” val routes: HttpHandler = routes( "/echo" bind POST to echo, "/twitter" bind routes( “/tweet” bind POST to tweet ) ) Can compose multiple routers to make an HttpHandler http4k does a depth-first search on the tree, then falls back to 404
  9. Serving HTTP val echo: HttpHandler = { r: Request ->

    Response(OK).body(r.body) } val server: Http4kServer = echo.asServer(Undertow(8000)).start() We can attach an HttpHandler to a running container
  10. Consuming HTTP HttpHandler: (Request) -> Response Can reuse the symmetric

    HttpHandler API: val client: HttpHandler = ApacheClient() val response: Response = client( Request(GET, "https://www.http4k.org/search") .query("term", “http4k is cool") )
  11. Extreme Testability Testing http4k apps is trivial because: • the

    building blocks are just functions • messages are immutable data classes! val echo: HttpHandler = { r: Request -> Response(OK).body(r.body) } class EchoTest { @Test fun `handler echoes input`() { val input: Request = Request(POST, "/echo").body("hello") val expected: Response = Response(OK).body("hello") assertThat(echo(input), equalTo(expected)) } }
  12. Extreme Testability Testing http4k apps is trivial because: • the

    building blocks are just functions • messages are immutable data classes! val echo: HttpHandler = SetHostFrom(Uri.of("http://myserver:80")) .then(ApacheClient()) class EchoTest { @Test fun `handler echoes input`() { val input: Request = Request(POST, "/echo").body("hello") val expected: Response = Response(OK).body("hello") assertThat(echo(input), equalTo(expected)) } }
  13. Typesafe HTTP Contracts • How do we enforce our incoming

    HTTP contract? • Locations: Path/Query/Header/Body/Form • Optionality - required or optional? • Marshalling + Typesafety • What about creating outbound messages? val miner: HttpHandler = routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r.path("btc")!!.toInt() + 1 Response(OK).body("""{"value":$newTotal}""") } )
  14. Concept: Lens “A Lens targets a specific part of a

    complex object to either GET or SET a value” ie. it’s a function - or more precisely 2 functions! Extract: (HttpMessage) -> X Inject: (X, HttpMessage) -> HttpMessage • these functions exist on the lens object as overloaded invoke() functions
  15. Lens example • Revisiting the earlier example… val miner: HttpHandler

    = routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r.path("btc")!!.toInt() + 1 Response(OK).body("""{"value":$newTotal}""") } ) data class BTC(val value: Int) { operator fun plus(that: BTC) = BTC(value + that.value) override fun toString() = value.toString() } • Let’s introduce a domain type to wrap our primitive val btcPath: PathLens<BTC> = Path.int().map(::BTC).of("btc") val btcBody: BiDiBodyLens<BTC> = Body.auto<BTC>().toLens() • … and create lenses to do automatic marshalling:
  16. Lens application • http4k provides Lenses targeting all parts of

    the HttpMessage • Via a CatchLensFailure filter, contract violations automatically produce a BadRequest (400) • Auto-marshalling JSON support for Jackson, GSON and Moshi val btcPath: PathLens<BTC> = Path.int().map(::BTC).of("btc") val btcBody: BiDiBodyLens<BTC> = Body.auto<BTC>().toLens() val miner: HttpHandler = CatchLensFailure.then( routes( "/mine/{btc}" bind POST to { r: Request -> val newTotal: BTC = btcPath(r) + BTC(1) btcBody(newTotal, Response(OK)) } ) )
  17. Business Abstraction Remote Client Launcher Load Environment Configuration Embedded Server

    Launch Application Stack Logging Metrics Remote Clients Application Business Abstraction Business Abstraction The Layer Cake Route Route Route Route
  18. Standardised Server & Clients fun serverStack(systemName: String, app: HttpHandler): HttpHandler

    = logTransactionFilter(“IN”, systemName) .then(recordMetricsFilter(systemName)) .then(handleErrorsFilter()) .then(app) fun clientStack(systemName: String): HttpHandler = logTransactionFilter(“OUT”, systemName) .then(recordMetricsFilter(systemName)) .then(handleErrorsFilter()) .then(ApacheClient()) Filter.then(that: Filter) -> Filter By utilising the ability to “stack” Filters, we can build reusable units of behaviour
  19. Fake Your Dependencies! Fake HTTP Service State • Leverage Body

    lenses • Simple state-based behaviour • Run in memory or as server • Easy to simulate failures
  20. 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 HttpHandlers
  21. Consumer Driven Contracts Abstract Test Contract Success scenarios Business Abstraction

    Fake Dependency Test Fake System (HttpHandler) State Failure scenarios Real Dependency Test Environment Configuration Remote Client (HttpHandler)
  22. Performance •Best performing Kotlin library •http4k + Apache server •Standard

    JVM tuning Full implementation @ http://bit.ly/techempower
  23. What did we gain? •Pure Kotlin Services •No Magic ==

    easy debugging •Boilerplate reduction •In-Memory == super quick build •End-to-End testing is easy
  24. serverless4k AWS API Gateway AWS Lambda • Http4k Apps can

    run as Lambdas by implementing a single interface • Applying Proguard shrinks binary size to 100’s of Kb • Dependencies can have a significant effect on cold start time** ** http://bit.ly/coldstartwar
  25. native4k •GraalVM is a universal VM •Can compile JVM apps

    into native binaries •Small size + quick startup •BUT: No simple reflection = hard for many libs •http4k apps with Apache-backend work out of the box! •Simple Graal http4k app = 6mb •With Docker (Alpine), 9mb
  26. http4k nanoservices “5 useful mini-apps which all fit in a

    tweet!” { ws: Websocket -> while (true) { ws.send(WsMessage( Instant.now().toString()) ) Thread.sleep(1000) } }.asServer(Jetty()).start() ProxyHost(Https) .then(JavaHttpClient()) .withChaosControls(Latency() .appliedWhen(Always)) .asServer(SunHttp()) .start() static(Directory()) .asServer(SunHttp()) .start() ProxyHost(Https) .then(RecordTo(Disk("store"))) .then(JavaHttpClient()) .asServer(SunHttp()) .start() JavaHttpClient().let { client -> Disk("store").requests() .forEach { println(it) client(it) } } http://bit.ly/http4knanoservices