Rust based load balancing proxy server with async I/O
In my previous Rust post, I built a simple echo server that spun a whole new thread for each connection. In this one, I want to do this in an async manner. Rust doesn’t have the notion of async/await, or something similar to Go green threads (it seems that it used to, and it was removed as costly abstraction for low level system language).
I’m going to use Tokio.rs to do that, but sadly enough, the example on the front page is about doing an async echo server. That kinda killed the mood for me there, since I wanted to deal with actually implementing it from scratch. Because of that, I decided to do something different and build an async Rust based TCP level proxy server.
Expected usage:
cargo run live-test.ravendb.net:80 localhost:8080
Which should print the port that this proxy runs on and then route the connection to one of those endpoints.
This led to something pretty strange, check out the following code:
Can you figure out what the type of addr is? It is inferred, but from what? The addr definition line does not have enough detail to figure it out. Therefor, the compiler actually goes down and see that we are passing it to the bind() method, which takes a std::net::SocketAddr value. So it figures out that the value must be a std::net::SocketAddr.
This seems to be utterly backward and fragile to me. For example, I added this:
And the compiler was very upset with me:
I’m not used to the variable type being impacted by its usage. It seems very odd and awkward. It also seems to be pretty hard to actually figure out what the type of a variable is from just looking at the code. And there isn’t an easy way to get it short of causing an intentional compiler error that would reveal those details.
The final code looks like this:
At the same time, there is a lot going on here and this is very simple.
Lines 1 – 15 are really not interesting. Lines 17 – 29 are about parsing the user’s input, but the fun stuff begins from line 30 and onward.
I use fun cautiously, it wasn’t very fun to work with, to be honest. On lines 30 & 31 I setup the event loop handlers. And then bind them to a TCP listener.
On lines 40 – 62 I’m building the server (more on that later) and on line 64 I’m actually running the event loop.
The crazy stuff is all in the server handling. The incoming().for_each() call will call the method for each connected client, passing the stream and the remote IP. I then split the TCP stream into a read half and a write half, and select a node to load balance to.
Following that, I’m doing an async connect to that node, and if it is successful I’m splitting the server and then reverse them using the copy methods. Basically attaching the input and output of each to the other side. Finally, I’m joining the two together, so we’ll have a future that will only be done when both sending and receiving is done, and then I’m sending it back to the event loop.
Note that when I’m accepting a new TCP connection, I’m not actually pausing to connect to the remote server. Instead, I’m going to setup the call and then pass the next stage to the event loop ( the spawn ) method.
This was crazy hard to do and generated a lot of compilation errors along the way. Why? See line 57, where we erase the types?
The type of send_data without this line is something like Future<Result<(u64,u64), Error>>. But the map & map_err turn it into just a Future. If you don’t do that? Well, the compiler errors are generally very good, but it seems that inference can take you into la-la land, see this compiler error. That reminds me of trying to make sense of C++ template errors in 1999.
Now, here is the definition of the spawn method:
And I didn’t understand this syntax at all. Future is a trait, and it has associated types, but I’m thinking about generics as only the stuff inside the <>, so that was pretty confusing.
Basically, the problem was that I was passing a future that was returning values, while the spawn method expected one that was expecting none.
I also tried to change the and_then to just then, but at that point I got:
At which point I just out.
However, just looking at the code on its own, it is quite nicely done, and it expresses exactly what I want it to. My problem is that every single change that I make there has repercussions down the line, which is hard for me to predict.
Comments
Can you also comment a bit on whether you think the language is productive or not? Do all of those mechanisms help or hurt in the end (to someone who is not totally new to the language)?
tobi, That is still way too early to let you know. I can tell that it is frustrating to use, and the tooling support (even with all the extensions I can install) is very much below what I'm used to have.
Hey Ayende,
I had played sometimes with Rust and in most of the time I got hit by the compiler too. After your tests, it should be nice if you publish some comparison with Go or another languages that you tried. And for helping you with it, there is a Rust plugin for rust to IDEA (https://github.com/intellij-rust/intellij-rust), you can get the nightly version. It's still going on but it can help you a little.
Daniel, Thanks, I installed that and will give it a try.
Hi,
Is it possible to set the source IP in this example? I want to forward the request but with a particular source IP and I couldn't find anything talking about that in the Tokio or Rust documentation…
Thanks!
Thibaud, I'm afraid that I'm not the person to ask
Comment preview