Server as a Function. In Kotlin. _________________. KotlinConf - October 4th 2018 David Denton & Ivan Sanchez
The Oscar Platform • Strategic journal delivery platform for a global academic publisher • Top 1000 site globally, delivers ~10s of millions Req/day Monorepo in CD build & deploy by Deployed to
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
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
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
Server as a Function. In Kotlin. ______ _. Distilled
Concept: Service HttpHandler “It turns an HTTP Request into an HTTP Response” ie. it’s a function! HttpHandler: (Request) -> Response val echo: HttpHandler = { req: Request -> Response( OK ).body(req.body) } val resp: Response = echo(Request(POST, “/ echo“).body(“hello”))
Concept: Filter “Provides pre and post processing on an HTTP operation” ie. it’s a function! Filter: (HttpHandler) -> HttpHandler val twitterFilter = Filter { next: HttpHandler -> { req: Request -> val tweet: Request = req.body(req.bodyString(). take (140)) next(tweet) } } val tweet: HttpHandler = twitterFilter. then (echo)
(New) Concept: Router “Matches an HttpHandler against a Request” ie. it’s a function! Router: (Request) -> HttpHandler? Can compose multiple routers to make an HttpHandler val routes: HttpHandler = routes ( "/echo" bind POST to echo, "/twitter" bind routes ( “/tweet” bind POST to tweet ) ) http4k does a depth-first search on the tree, then falls back to 404
Serving HTTP We can attach an HttpHandler to a running container val echo : HttpHandler = { r: Request -> Response( OK ).body(r.body) } val server : Http4kServer = echo . asServer (Undertow(8000)).start()
Consuming HTTP Can reuse the symmetric HttpHandler API: HttpHandler: (Request) -> Response val client: HttpHandler = ApacheClient() val response: Response = client( Request(GET, "https://www.http4k.org/search") .query("term", “http4k is cool") )
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)) } }
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)) } }
Typesafe HTTP Contracts val miner: HttpHandler = routes ( "/mine/{btc}" bind POST to { r: Request -> val newTotal: Int = r. path ("btc")!!. toInt () + 1 Response( OK ).body("""{"value":$newTotal}""") } ) • 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?
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
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}""") } ) • Let’s introduce a domain type to wrap our primitive data class BTC(val value: Int) { operator fun plus(that: BTC) = BTC(value + that.value) override fun toString() = value.toString() } • … and create lenses to do automatic marshalling: val btcPath: PathLens<BTC> = Path. int ().map(::BTC).of("btc") val btcBody: BiDiBodyLens<BTC> = Body. auto <BTC>().toLens()
Lens application 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 )) } ) ) • 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
Server as a Function. In Kotlin. _______________. Dogfood Edition
The Layer Cake Launcher Application Stack Business Abstraction Load Logging Application Environment Remote Client Configuration Business Route Abstraction Metrics Route Route Business Embedded Remote Abstraction Server Route Clients Launch
Standardised Server & Clients By utilising the ability to “stack” Filters, we can build reusable units of behaviour Filter.then(that: Filter) -> Filter 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())
Fake Your Dependencies! • Leverage Body lenses Fake HTTP Service • Simple state-based behaviour State • Run in memory or as server • Easy to simulate failures
Application Testing Test • All internal and external applications and clients are HttpHandlers Environment Configuration • We can simply inject apps into each other in order to build an o ffl ine environment • Using fakes, we can inject failure to test particular scenarios
Consumer Driven Contracts Abstract Test Contract Business Abstraction Success scenarios Fake Dependency Test Real Dependency Test Fake Failure System Remote scenarios (HttpHandler) Environment Client Configuration (HttpHandler) State
Performance • Best performing Kotlin library • http4k + Apache server • Standard JVM tuning Full implementation @ http://bit.ly/techempower
What did we gain? • Pure Kotlin Services • No Magic == easy debugging • Boilerplate reduction • In-Memory == super quick build • End-to-End testing is easy
D E M O T I M E
… all this in 70 lines of Kotlin! CORE https://http4kbox.http4k.org Auth: http4kbox:http4kbox http://bit.ly/http4kbox
Server as a Function. In Kotlin. _________________. Without the Server.
serverless4k • Http4k Apps can run as Lambdas by implementing a single interface AWS API Gateway AWS Lambda • Applying Proguard shrinks binary size to 100’s of Kb • Dependencies can have a significant e ff ect on cold start time** ** http://bit.ly/coldstartwar
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
ecosystem HamKrest CORE
http4k nanoservices “5 useful mini-apps which all fit in a tweet!” static (Directory() { ws: Websocket -> . asServer (SunHttp()) while (true) { .start() ws.send(WsMessage( Instant.now().toString()) ProxyHost(Https) ) . then (JavaHttpClient()) Thread.sleep(1000) . withChaosControls (Latency() } . appliedWhen (Always)) } . asServer (Jetty()).start() . asServer (SunHttp()) .start() JavaHttpClient(). let { client -> Disk("store").requests() ProxyHost(Https) . forEach { . then (RecordTo(Disk("store"))) println ( it ) . then (JavaHttpClient()) client( it ) . asServer (SunHttp()) } .start() } http://bit.ly/http4knanoservices
@daviddenton @s4nchez david@http4k.org ivan@http4k.org Thanks! __________? @http4k quickstart: start.http4k.org web: www.http4k.org slack: #http @ kotlinlang
Recommend
More recommend