Server-side Swift: Making Canopy (2/6)
Last week I talked about how I found Swift to be an excellent choice for server-side development. This week I’ll be talking about platform choices.
Open Sourcing Canopy
Firstly I wanted to reiterate my promise to open source the client and server sides of Canopy provided my Patreon goals are met. I want to be a full-time open source developer, Canopy’s monetization is one way I want to get there, but if I don’t need I’d rather give my work to the community.
The Many Options
- An SSL-capable web-server
- An APNs engine to communicate with Apple’s push notification service
- A database
- General cryptographic capabilities
The options I considered were:
I chose Perfect. Let’s talk about each one.
Reinventing the wheel
A webserver isn’t that complicated. However the edge cases are. I’m not against reinventing wheels, often it’s quite fun and often you learn one-hell-of a lot in the process. The best reason to roll your own for me is that when things go wrong, you know the code, so fixing things is easier. When things go wrong with a black-box fixing it yourself can be really hard.
Still, I never really considered this option for Canopy: I wanted to build a robust and reliable solution; making my own web-server would surely bite me in the ass and it would surely happen at the worst possible time during production.
Thus my decision would come down to:
- Maturity of the stack
- The amount of material out there in blogs, platform-documentation and the relative usefulness of existing stackoverflow answers
A mature stack is one that is hopefully already stable and relatively bug free.
A stack that has a good developer presence on GitHub is one where if I have issues I can hope that I will get help.
High quantities of blog posts etc. about the stack indicates other people have already tested the waters and the stack probably works in real world use. Good numbers of questions and crucially answers and StackOverflow indicates I will find help when I need it during development and for issues in production.
Perfect is a mature solution ported to Swift from Java and has been around for years. I found a good deal of information online
Vapor is a pure Swift solution. This directly correlates with an easier build-system life. Anyone with any experience with dependency graphs is also familiar with the fact that the bits that go wrong are the “native” extensions. Where the tool has to integrate with a C-library or some other library that isn’t that language. As I said last week though, this is quite painless in Swift relative to eg. Ruby. I still can’t build a bunch of gems on Mojave because they need libxml or whatever and Mojave moved where various C headers live.
Vapor however is quite new and in a state of flux.
But I love that Vapor is a really vibrant open source project, actively and passionately maintained.
It also has a beautiful and very thoughtful API.
Kitura is backed by the might of IBM, they use it themselves and people are paid to work on it. Since Swift was released IBM have committed to Swift in a very inspiring way.
However I wasn’t super keen on the APIs available, and Kitura was about as mature as Vapor.
Why I chose Perfect
Performance of all the solutions is pretty great. So it wasn’t a primary concern (though Perfect wins the linked comparison).
Perfect’s APIs were idiomatic enough and made sense to me, though they are not very “Swifty” and in many places seem quite thoughtless. Overall the maturity of the platform won me over. Since this was my first Swift project on the server I wanted as little hassle as possible in the lower levels, and it had the most third party documentation out there which would help me during development.
So how did it go? Not perfectly.
It’s a solid system, I’ve had very few issues (with Perfect itself). I found it easy enough to figure out how to use it and to find the features I needed. It performs well. Out the box it supported everything I needed (SSL, APNs, WebSockets, Cryptography etc.).
I also liked that there were no black-boxes. You write a
main.swift, you control the entry point of your server and to run it you execute the binary SwiftPM makes for you. Vapor can be the same, but their tutorials guide you to their automated execution model where you write a YAML file (ugh) and execute their binary to start the server.
The less magic, the easier it is to debug issues. Plain and simple.
They also provide a whole org of examples, making getting started much simpler. I require such things from new tooling. Examples are just what I find easiest. Tools that provide a generated API reference and that’s it don’t get tried out by me at least.
API Not Great
The API is not great, but it is serviceable.
Little things are grating. The API in general uses
[UIint8] rather than
Data which is strange. I think probably it is because Perfect was released for Swift very soon after it was open sourced and Foundation at that point was mostly unimplemented. But converting between byte arrays and
Data is a continuous annoyance since
Codable and well, everything requires
The APIs have conveniences for decoding the incoming data into a
Decodable however they require
Codable rather than
Decodable. This seems (I may not see the actual reason so forgive me if so) like poor attention to detail since it is incoming data I only need it to decode. Marking it Codable means Swift must generate the decoding as well as the encoding machinery for my types bloating the final binary size (maybe the optimizer removes it). But for me more importantly it meant I was confused about the API, I kept looking around in case I misunderstood it, marking the API
Decodable gives the user a hint as to its purpose: it is for receiving and explicitly not for sending.
A simple perfect server might be:
Thus once built (
swift build) and run (
.build/debug/server_name) you can go to http://localhost/foo and it will run the above route. Note you have to run servers for port 80 (the default) as
root, and Perfect is advanced enough to provide API so you can downgrade to another user after executing, which you should definitely do in production. Preferably create a new Linux user with minimal permissions.
As you can see Perfect has some nicities, like an enum for HTTP response codes. You can use the integer codes if you know them by heart, but you shouldn't obviously, but you have the options and flexibility to do both.
It’s easy to write bad routes
As the above example shows you respond by calling functions on the
HTTPResponse object (2nd parameter in the above closure, Perfect’s examples never use inline closures for handlers, I didn’t either and you shouldn’t either: put routes in their own files).
I frequently had paths where I (accidentally) did not complete the request, if we were to use the language to its fullest we would get Swift itself to refuse to compile code that did not “complete” the request. Since we live in an asynchronous world I wrote my own
Route extension that required you to return a
Promise. Notably this is what Vapor does.
Thus it became a compile error to not finish any requests rather than a hidden production bug. Ideally Perfect would do this.
The APNs (push notification) system Perfect provides is not production ready. I was thrilled they provided one, so I used it. But quickly I found issues. Some back and forth with the maintainers while initially great because they fixed my issues but after a bit more testing ended up with stagnant tickets. So I looked into fixing the Perfect implementation, but in the end realized this would be a large endeavor and I knew enough to write my own for my specific and limited needs.
I know this is not how open source should work, but I did try, and the process of studying their code and the Apple docs got me to the point where there was going to be more value for me to write my own limited to my needs implementation. Sometimes that is how it goes.
As I said last time, Swift allows easy integration with c libraries, so I jumped on using libcurl since it has mature HTTPS2 support, which APNs requires. Writing my own was fun and took about two days,
Unfortunately this left a bug that I only discovered in production. I was not closing the connections properly and eventually the kernel terminated my server due too many file descriptors being open. At that time I didn’t have systemd configured to control my server so it didn’t restart. I am now using systemd, and I fixed the bug too.
I was initially intimidated by the idea of communicating with APNs myself, after all, it seems like it would be a horribly opaque system that should only be tackled by the likes of “super engineers”, but the process of writing my own layer made me remember that usually that feeling of intimidation is just because you haven’t tried. Read the docs and give it a go. Apple’s system is pretty simply actually, and to boot I know now a lot about HTTP2, which incidentally is a very neat thing.
Writing my own led to the first build problems I’d had with Swift because I needed a newer libcurl than the system provided. As I said above this is a plus for Vapor, it is pure Swift, a smaller dependency graph that is entirely in its control is a plus. But ultimately this was simple to solve because SwiftPM is well designed in this area.
Not Perfect’s fault but I had pain with URLSession having some edge-case behaviors unimplemented. Specifically for me when I was validating in-app-purchase receipts with Apple their server would do something that caused URLSession to need to seek-within request (not really sure what was going on TBH), however in Linux Foundation this fatals due to being unimplemented.
Forunately Perfect provide
CURLRequest (a more “Swifty” wrapper to libcurl) which was easy enough to use and worked fine, so I switched to this everywhere I needed to do additional networking in my routes. The API on this class is pretty obnoxious TBH, but I commend Perfect yet again for having solutions when I needed them.
The value of a minimal layer
Perfect has minimal “magic” and as a result I knew I could use
NSFileManager, manipulate the file system as I saw fit, integrate any database system I liked etc.
You can pick more involved systems like eg. Rails, but it takes years with such systems to know for sure if you can do simple things like writing to a file and that doing such things won’t break something or is not “good practice” for some reason or another.
Each to their own, but I appreciated this aspect. I knew that every route that is executed with Perfect is just a
DispatchQueue.async on a concurrent queue. Everything going on was using machinery I already knew well from years of development on Apple platforms. And also — I think importantly—using well tested, well designed systems dog-fooded for years by Apple.
I think next time I would pick Vapor, the community is really active and the quality of its code is high.