Stockfighter is a CTF (short for Capture the Flag) game that I first heard about at Microconf 2015, but haven’t gotten a chance to play up until very recently. I plan on posting more about my impressions of the game later, but very shortly: it is a series of programming challenges based on the concept of stock exchanges and ways to manipulate stock exchanges. Along with the web UI, a public json API is exposed as a mechanism for interacting with the game. There did not seem to be any Scala clients floating around, so I took this as a chance to play around with akka-http.
¶Scaffolding
After some quick googling and reading of tutorials, it looked like the basic structure of an http client would be something like this:
1 | package stockfighter.client |
A couple of things to note here. First off, akka-http
is a part of the (increasingly pervasive) akka ecosystem, so obviously actors have to be involved. The tutorials either had the ActorSystem
at the top level or had the client itself be an Actor
, but I felt like a regular class would suffice so I compromised by passing the system in as a parameter with a default. The ActorMaterializer
is completely new to me, since I am coming from spray
~1.1 and have missed out on a lot of the latest reactive-buzzwordy developments.
I’m still not sure I grok it completely, but my understanding is that akka-http is backed completely by reactive streams, which the client constructs as lazy descriptions of computations. When the computations are run, the ActorMaterializer
spins up the actors to do the actual work. In any case, I thought about putting the Materializer in the constructor as well, but the fact that it takes an implicit ActorSystem
as an argument makes it fairly awkward to have both ActorSystem
and ActorMaterializer
live as constructor params with defaults. I can think of a few ways to deal with this, but for a quickie client I decided to just in-line the materializer and move on.
¶Making a request
The Stockfighter API ships with a heartbeat/status endpoint, i.e., “is the service up?” The endpoint lives at https://api.stockfighter.io/ob/api/heartbeat and returns a response in the following format:
1 | { |
This seemed like as good a starting point as any, in that it’s a fairly simple endpoint with a simple response type, but still complex enough to test a full request flow with some common functionality like serialization/deserialization.
As it turns out, it took a decent amount of time and a lot of reading to get to a basic implementation:
1 | import akka.http.scaladsl.Http |
Executive summary: akka-http
leverages the concept of reactive streams that seems to be the new hot thing lately. Streams are essentially fancied up functions, and consist of three parts: the Source
, the Flow
, and the Sink
. The Source
describes the input, which can be single element (Source.single
), an iterator (Source.iterator
), a Future (Source.fromFuture
), etc. The Flow
is the description of a computation to run on the data from the Source
. The Sink
describes what to do with data after it has been run through the flow: push it into a queue
(Sink.queue[T]
), fold
over it (Sink.fold
), and so on. You can combine things in all sorts of different ways–the above uses via
and runWith
, but there’s also viaMat
, run
, and any number of other fancy combinators.
What it boils down to here is that the Source
is an http request, the Flow
describes how to send that request, and runWith(Sink.head)
runs the flow and returns a future of the response. Phew…
¶Serialization/Deserialization
For serialization/deserialization, akka-http
provides its own Marshal/Unmarshal
. For json, the default option is to lean on akka-http
’s predecessor, spray
–Or more specifically, spray-json
:
1 | // ADT describing the response |
The Serialization trait is normal procedure for spray-json
, and the SprayJsonSupport
provided by akka-http
just provides an implicit conversion that links the Unmarshal(r.entity)
together with the jsonFormat
for the entity. The big wart here is actually the mapAsync(parallelism = 1)
, which is a result of Unmarshall(...).to[T]
returning a Future[T]
. I didn’t dig too deeply into this, but based on some quick googling the general consensus seems to be that the use of Future
here is a way of handling lazy/streaming responses. Whatever the case, I could not find an alternative API for this so mapAsync(1)
seemed to be the least of the evils–another choice would have been something like .map { r => Await.result(Unmarshal(r.entity).to[ApiStatus], Duration.Inf) }
but that seems even clunkier.
¶Error handling
The above code still has the flaw that if the server responds with e.g. 404, it will throw an exception and the client will be SOL. This is not so much an issue for the heartbeat endpoint, but Stockfighter is nice enough to enumerate a bunch of its common errors for us so why not add in some minimal handling via Either?
1 | // Type alias for readability's sake |
apiIsUp
should now return an Either[ApiError, ApiStatus]
unless something really bad (dare I say, exceptional?) happens.
¶TODOs
The above is a nice start, but a few big TODOs stand out to me before I go on and toss this onto github.
First and foremost… Tests! Testing libraries like this is always tricky since they’re essentially all integration-y glue code, but I have always been a big fan of the vcr gem in Ruby. As far as I know the closest thing in Scala is betamax, which I have not used but would like to. (I know, I know–Not writing test firsts? What about TDD? BAD DEVELOPER! rolls up newspaper)
Another big thing for me is domain modeling. The built-in json deserialization is fine for working with row-level data, but the plain case class format leaves a bit to be desired as far as robust data modeling. As a simple example:
1 | // A simplistic "order request" |
1 | // naive translation: |
I haven’t decided whether it would be better to add another step to the pipeline (e.g., mapJsonToDomainObject
) or to roll custom spray JsonFormat
s to do this.
Lastly: websockets. In theory websockets are supported, but the documentation is even sparser than for http clients and I haven’t quite figured it out yet–especially since the deserialization provided in SprayJsonSupport
does not seem to work with the types used in the websocket API.
¶Overall Impressions
So far, my impression of akka-http
is by and large the same as my impression of spray
. Actors (and now reactive streams) provide a lot of power and performance in exchange for non-trivial complexity. In my experience this tradeoff is generally worth it for server-side/business application code, but lugging around an ActorSystem/etc ends up feeling very clunky for a simple http client. It doesn’t help that the -client libraries seem to be the red-headed step-children of both ecosystems.
The documentation feels consistent with the general API design. That is: it tries to look simple for the most basic use-cases, but in reality there is a lot of implicit stuff floating around. It was basically a pre-requisite for me to go digging for not only how streams worked conceptually but all the varied APIs that need to be used to link everything together before I could unpack the examples in the client tutorials. For example, while playing with the websocket tutorial I tried to switch the Sink.foreach
with a Sink.queue
and got the following:
1 | [error] (...)/TradingApiClient.scala:118: type mismatch; |
It’s not the end of the world as I worked out the need for a type parameter (i.e., Sink.queue[Message]
) but there a lot of examples like this where the errors and tutorials are not exactly intuitive. I can see this being a huge deterrent to folks who are new to the ecosystem, to the concepts, or to Scala in general who will hit a wall and think, “Wow, all this and I can’t even open up a websocket/execute a json POST
/etc?” Or even worse–the example code will be cargo-culted in by a harried developer on a deadline and carried on as the software version of the five monkeys. (This is not to say I could do any better. Documentation and API design are some of the most underrated hard problems in software today, IMO. 😃
All my nitpicking aside, there is a lot to like about akka-http
. In exchange for all the effort involved in learning about reactive streams and how to work with them, they provide a nice construct for abstracting away concerns like back-pressure management. This frees up developers to concentrate on the actual flow of the data. The resulting code is also quite clean and generally easy to follow, despite the time it took to actually get to that point. In other words, it trades off learning curve and ease of intuition for API comprehensiveness and composability. akka-http
is especially nice on the server, where performance is a bigger concern. I’ve built a couple of internal webservices with spray
previously, and it’s always been fairly performant without excessive tuning on my part. In addition, I’ve found the concept of Directives and the server-side routing DSL to be quite nice to work with in the past.
Overall I would recommend akka-http
unreservedly for writing web services and business applications. My experience with it on the server side has been quite good. I would also use it again on the client side, mostly because there don’t seem to be any better options. I had looked into some alternatives, but e.g. play-ws has the same overloaded baggage problem and dispatch is like the poster-child of unintelligible symbolic operators (and seems unmaintained to boot). So until a better http client surfaces in the Scala ecosystem, one could do a lot worse.