Asynchronous Again – Rewriting ptunnel in Rust

Asynchronous programing model is quite popular for I/0 intensive tasks – it enables you effective use of resources, while maintaining agility of and assuring scalability of the application. I myself used asynchronous programming many times –   in JavaScript (where it’s omnipresent) , Python ( mainly  in asyncio recently, but also bit in twisted, which was one of first network asynchronous libraries I met) and also in OCAML with  lwt or Core Async. The concept is always similar for all implementations –  I/O operations are returning handles to future results – they are called either  Futures, Promises, or Deferred  – and they are returned immediately.  These futures can have functions attached to them, which are executed later, when I/O result becomes available.  Asynchronous  programming is very much about functions, it requires first class functions  and anonymous functions are very useful here, that’s why asynchronous model flourishes in functional languages.  Apart of I/O deferred processing usually there are other utilities for later execution – like timeouts, pausing execution for some time (sleep), tasks synchronization (events, locks). Futures are executed in an “event loop”,   a loop that monitors various events from OS (availability of data from I/O), timers, etc. to execute futures (meaning functions attached to them), when appropriate. It’s also very common to chain futures, executing second one with result of first one , when first one is resolved and result is available and the third one with results from the second one and so on. Apart of this basic scheme languages may provide some syntactic sugar around asynchronous model like await and async keywords in Python or C#, which makes it easier to write the code.

Recently, as I’m progressing in learning of Rust,  I wondered how asynchronous programing is done in Rust. I decided to remake my old project ptunnel (written in Python) into Rust – ptunnel is a program that tunnels arbitrary connection/protocol through HTTPS proxy, so it can be used to connect IMAP, SMTP or SSH through proxy. In the rest of this article I”l share my experiences from this project.

Rust is relatively young language and asynchronous programing in Rust is even younger – now most popular library is Tokio, which is still in version 0.1, so basically in its infancy.  But site contains good documentation and guidance so it’s was relatively easy to get started.

The Long And Winding Road

I have to admit I’m still  struggling with some Rust concepts (lifetime mainly) and Tokio was quite demanding for sticking with core concepts. Tokio is using intensively futures module and all programing  and data processing is done through chaining of futures – this can create very complex types ( especially when anonymous closures are used) and thus provide very cryptic errors ( with type description over half of the screen – reminding me monstrous types in OCAML Ocsigen libraries ). In order to make various complex Future types compatible with rest of the program is quite often necessary to create trait objects – e.g. to box values. There is also one hideous features of rustc,  errors concerning lifetime of variables are reported as last – when all more ‘serious’ errors are resolved. So often it happened to me that I had removed one final error in code only to find that others pop up and I had found that I had screwed lifetime of some variables/parameters and that it couldn’t be easily fixed and had to redesign code.

Generally I noticed that while previously in other languages I was able to start and follow a single, relatively straight path from idea to final code and was able to progress steadily further, here in async Rust I had to backtrack several times, because the way lead nowhere. It looks like there are only few ways how to make it right and if you do not catch them initially you are lost. Hopefully those ways should lead to better code.

Tokio provides nice abstractions for higher level protocols (transports + protocols + services), which I believe can help in other cases,  but here I needed something rather low level –  so finally I ended up with using custom stream and  future types, after learning a bit about Tokio internals ( stressing again good documentation of Tokio) and chaining futures with map (modifying future value), and_then ( creating new future from results of previous one) and or_else (resolving error cases)

One of most attractive features of Rust is ‘zero-costs’ abstraction.  Rust provides higher level language abstractions like algebraic types, iterators etc., which are then implemented in very effective way – basically with same efficiency as if written in lower abstraction level language like C. Tokio abstractions (on lower level, where I was dwelling)  are mainly iterator like traits of Streams and Sinks and future combinators. So after finally being able to cope with these I was wondering how final code will perform and if it’ll match ‘zero-costs’ promise.

Comparison to Python Version

I have to say that old Python program was a poor contender. It’s written in very basic manner and not using any features that could improve performance ( at least thread pool, or asyncio) and it creates (and destroys) two threads for each connection.

So rather then fair benchmark it’s just a toy comparison, just to assure that Rust program is doing better. And it is doing better indeed, much better as we see below.

For quick test I used squid proxy, nginx server and ab (apache benchmark tool), all installed locally on my notebook – sending 1000 HTTP GET requests in 5 concurrent threads. So setup is like this:

ab <-> ptunnel <-> local squid <-> local nginx (default page)

And result is ( average time per request in ms):

Keep-Alive No Keep-Alive
Old Python Tool 10.51 ms 1003.75 ms
New Rust Tool 0.31 ms 3.28 ms
Without ptunnel 1.83 ms 2.55 ms

As you can see old python tool is no match – especially when no keep alive is used and thus overhead of thread creation is preposterous ( keep-alive case Python was fine on majority of requests, but few, where connection was created, time was again around 1 sec due to thread creation overhead).

For comparison there is also scenario where ab connects directly to squid (ptunnel is left out) –   there is interesting  keep-alive case, where setup with ptunnel is faster.

Even if this benchmark is simplistic it still shows that Rust program keeps to its promise and seems to perform well ( direct benchmark between ab and nginx gives 0.22ms per request).

Looking at code base Rust program is about 3 times bigger then old Python program ( ~ 600 lines vs ~ 200 lines).

The Code

The code is available on the github, where kind reader can review it, file contains instructions for installation. New Rust program functionality and CLI copies basically old python program, only new feature available is basic authentication with proxy.


Leave a Reply

Your email address will not be published. Required fields are marked *