Implementing Web Clients
Thundering Web Requests: Part 1
This is the first post in a series of posts exploring web services related technologies. It documents my observations from using different technologies to implement web clients.
Recently, I wanted to explore web service technologies. I decided to do this exploration by simulating and handling a thundering herd of web requests.
As part of this exercise, one of the tasks was to
Implement a web client that takes a URL and a positive integer N as command-line arguments and issues N concurrent HTTP GET requests to the URL.
The client also reports the time taken (latency) for each HTTP GET request to URL U along with the breakdown of how many of the N requests succeeded/failed.
Choice of Technologies
I chose four technologies to implement four variants of the client.
Two common reasons influenced these choices:
- Explore the support to develop web service related solutions.
- Explore the support to program concurrency.
Besides these, the choices were influenced by few minor reasons specific to the technologies.
- Erlang: I had not used Erlang and I wanted to try it out.
- Elixir + HTTPoison: After I built the client with Erlang using its builtin libraries, I was tempted to see if using Elixir would be different from using Erlang in terms of development experience and execution performance. The choice of HTTPoison was rather arbitrary.
- Go: I had used Go in the past and I wanted to do more with it.
- Kotlin + Vert.x: I had used both these technologies in the past and I wanted to have a client based on a commonly used technology, i.e., JVM.
I wanted to keep the source artifacts and the process to use the source artifacts simple and easy while jumping thru necessary “hurdles” in using a technology. So, given the simplicity of the client, I approached developing the variants as hacking exercise with minimal use of software engineering practices outside code. Specifically, whenever the above conditions were not violated,
- A variant was coded as a single file without adhering to community practices, e.g., packages, layout of folders.
For example, the Go client was coded into a main package that was stored in the top-level folder. Kotlin client was similarly embedded neither in a package nor in folders.
- If a variant could be coded up as a script and executed in compiled mode, then it was coded up as a script without using build scripts.
For example, the Erlang client was coded as a single script file that could be compiled and executed using Erlang’s escript tool. Similarly, the Kotlin client was coded as a single script file that could be compiled and executed using KScript.
- If a variant required dependencies, then a tool was used to acquire the dependencies.
For example, since the Elixir client depended on HTTPoison library, Mix build tool was used. The dependence was specified via mix.exs (build) file and Mix was used to download and compile the dependency before building the client.
Coding in Erlang for the first time was interesting. Function declaration in Erlang seamlessly embraces pattern matching to do value-based case splitting (as opposed to relying on flag/option-based conditionals). It also supports the use of pattern matching to deconstruct (project) compound values such as structs and lists into their components with the ability to pick only components of interest. I think this is both elegant and intuitive. Pattern matching can be used to similar effect in function declaration in Elixir as well.
Since both Erlang and Go had builtin core features to easily spawn concurrent computations and use message passing communication, dispatching concurrent requests was easy. This was also true of Elixir as it is targets Erlang’s VM.
As for Vert.x-based Kotlin variant, this was even easier as its API accepts a callback that will be invoked when the request completes.
Libraries and Documentation
Erlang’s builtin library was pretty self-sufficient for this exercise. I ended up using the core httpc library. Since this was my first time with Erlang, I took some time to come to grips with Erlang’s documentation scheme. While additional hyperlinks to terms used in documentation can ease navigation and searching, I really liked the structure of Erlang’s documentation as it mimics the structure of code/APIs and data in Erlang.
While I could have used Erlang’s httpc library with Elixir, I decided not to just for the sake of exploring an Elixir library. While the docs for HTTPoison were good, I felt the inclusion of a complete example would have been more helpful. This limitation may have been due to the fact that I was a Elixir newbie. In general, I liked the idea of hosting documentation of Elixir libraries (hex packages) at a central location (https://hexdocs.pm).
Like Erlang, Go’s builtin library was pretty self-sufficient for this exercise. I ended up using core http package. I really liked the Examples section of the documentation as it helps quickly grok the capabilities of a package and how to combine these capabilities.
While Vert.x was self-sufficient in terms of capabilities, I found its documentation (like other libraries in JVM ecosystem) to be limiting. Specifically, while the documentation about “how to use capability X” was good, it seemed to be “detached” from the documentation of the involved API. Also, given the number of different APIs and modules (e.g., core vs web), identifying the appropriate API while searching for it in the API documentation based on names was neither simple nor easy. I suppose this has more to do with javadoc-style documentation. [If you are curious, compare how
httpc package and
httpc::request methods are documented in Erlang vs how
Web module and
sendXXX methods are documented in Vert.x.]
Independent of the language, I was pleasantly surprised with the support available to easily work with web services. There were lots of options in terms of libraries and most of them were pretty easy to use. I suppose this could be a reason for the boom in Microservices :)
While the tooling to build the client was simple in Go (e.g.,
go build), I really liked the support in Kotlin (via KScript) and Erlang (via escript) for executing scripts in compiled mode. KScript supports the management of dependencies via the use of annotations. This removes the burden of build files in many situations that employ scripts characterized by adjectives such as simple, quick-n-dirty, and short-lived. I did not find similar support in escript. So, if I had used external libraries in the Erlang client, then my experience with Erlang would have been different.
In contrast, since the Elixir client depended on HTTPoison, I used the Mix tool along with a “build” file that captured the dependencies. While the file is not an issue by itself, the use of Mix entails more work as with any other build tool, e.g., create the project, understand the relevance of various build-related files, make appropriate changes to these files; more on this in the next post. I think this is an unnecessary hurdle in simple situations.
[If there is a way to use escript to both manage dependencies and execute the script (like KScript), then please do tell me how.]
In terms of LOC, Kotlin was the shortest implementation followed by Erlang, Go, and Elixir. While I had expected Go client to be long, I was surprised that the Elixir client was almost equally long. I am not sure if this is an artifact of how I implemented the Elixir client or the design and ecosystem of Elixir.
All of the clients were rather simple due to the inherent simplicity of the task and the rich library support to dispatch HTTP requests.
The code for all clients is available on GitHub. I will discuss the implementation differences in the clients in a future post focused on performance evaluation of the clients.
Observations from implementing web services using different technologies.
Aug-20–2019: Uncovering http client issue in Erlang
During evaluation of the servers using the clients, the erlang client was at times hanging. While the implementation handled success and failure of HTTP requests and the abnormal failure of spawned processes issuing HTTP requests, it seems there was a condition that was not being covered. Thanks to input from the Erlang community (via Slack), I extended the client with a catch-all handler. Interestingly, this handler was not triggered during the hanging runs.
So, at this point, I think either the httpc library in Erlang is broken in some rare cases or the library needs to be configured appropriately to handle situations where the client may issues numerous concurrent requests, e.g., 3200. Either ways, I will not use Erlang client to evaluate the servers.