Rust is Not so Hairy

TL;DR

  • I decided to learn Rust on my nth attempt.
  • Writing small programs helped me get stuff done.
  • I converted a Java gRPC service into Rust for comparison
  • I'm super-impressed with Rust's low CPU and memory footprint

Hi, I'm Neville (nevi-me), a self-taught developer/[insert other things] residing in JHB, South Africa. This is my short account of learning Rust by replacing a Java service with a Rust one.

I've wanted to learn Rust for over 2 years now (Rust and Go, actually), but there's always been something shinier like Kotlin, Dart, Pony, etc.

A few weeks ago I saw A Gentle Introduction to Rust by Steve Donovan (ps. if you read this Steve, I'd like to buy you lunch to say thanks, I live in Sunninghill/Roodepoort). This introduction was very gentle, and got me hooked to the language.

Why the Title?

Rust looks scary. Borrow-checkers, mutable references, etc. It's the primary reason why I never went beyond Hello World over the past 2 years.
I chose the title because it fits with my experience so far. Rust is not so hairy.

Learning by Doing

I believe that a good way to learn a programming language (beyond syntax and basic toolchain) is to try write some small piece of software with it.

After playing with the Gentle Intro, I revisited stepancheg's grpc & protobuf libraries. It took a few attempts for me to understand how to compile my protobuf files and get gRPC fired up. While doing that, I created rust-grpc-shorty, which is a small program that connects to our custom URL shortener, and returns a shortened URL.

You can call shorty a POC, because when I got it working, I gained the confidence to build something slightly larger in Rust. I've dived a bit deeper into the internals of grpc in Rust, and have also played with Actix.

Networking & Data

Unless you're building a CLI or small cmdlet that does one small thing, you'll find yourself needing to either connect to the Internet, or to handle some files on the system.
I had to deal with the former, so I had to find a good way to be able to manipulate data. JSON is often the de-facto standard, but I started preferring protobufs when I learnt Kotlin. It just makes life easier having a structured data format to work with.

I've relied (and will in the future) on Stepan Koltsov's protobuf and grpc libraries. I've found them to be the most mature of the implementations that I looked at, and he's very friendly on Github.

Using protobufs meant deferring learning the various serde's of Rust, which has helped me to focus on getting something done.

Rust, The Language

I'm a beginner, so I'll spare lots of details.

  • I'm picking portions of Rust as I go along.
  • I've found it easier to focus on one library/crate at a time, until I can do the minimum that I need.
    • For example, I've touched rustracing (OpenTracing in Rust), actix (Actor-based web framework), grpc and protobuf (networking, serialisation), redis.
  • Rust documentation is decent-to-good, but some libraries aren't well documented.

With the above said, one thing that stood out for me is this:

The borrow-checker and mutability aren't scary if you reason about memory. What helped me was treating an object in memory as a physical item. If I don't ask for a reference, I take/move the value.
The Rust compiler is very helpful, as most Rustaceans would agree.

Rust Performance

The most exciting thing for me about my Rust experience, was discovering how much faster it was/is than a similar service that I wrote in Kotlin.

Problem Statement

I run Moving Gauteng, which is a public transit information website for the Gauteng Province. One of the more interesting things about the service/website, is the mixture of real-time and simulated vehicle locations.

We have a service that generates a stream of vehicle locations every 5 seconds, both based on scheduled location, as well as real-time information that we crowdsource. An example of what I mean is movinggauteng.co.za/explore, which I haven't had time to complete. This webpage will show you all the vehicles that we have info on.

moving-gauteng-explore

The gold/yellow vehicles (best viewed during the GMT+2 weekday) are Gautrain buses, which I've been granted permission to stream. I'm unfortunately only allowed to display the stream. I therefore am forbidden from mapping the buses to their timetables so I can offer bus delays and other predictions. It sucks, but it's not important for this post.

The vehicles (other than the Gautrain ones) are streamed to a central pub-sub stream thing that I built in Java. It's pretty intelligent, you tell it which vehicles you're interested in, and what frequency of updates you want, and it serves those updates through a streaming gRPC endpoint.

The RPC extract is below. I've removed other detail about frequency of updates. Maybe I'll talk about them some other day.

service Transit {
    rpc StreamTripUpdates (Scope) returns (stream TripUpdate) {}
}

message Scope {
    Level level = 1;
    repeated string stop = 2;
    repeated string trip = 3;
    repeated string mode = 4;
    repeated string route = 5;
    repeated string agency = 6;
    repeated string line = 7;
}

message TripUpdate {
    string _id = 1;
    Scope scope = 2;
    Trip trip = 3;
    repeated Stop_time stop_time = 4;
    uint32 weekday = 5;
    uint32 week = 6;
    uint32 year = 7;
    uint64 timestamp = 8;
    VehiclePosition vehiclePosition = 9;
    bool live = 10;
    string label = 11;
}

Anyways, the above gets streamed by a NodeJS service, and is piped to websockets from the webpage. The beauty about this is that if you're looking at one route, you get to see only the vehicles active on that route.

I've been working on a real-time geofencing problem, and wanted to build something on top of Tile38. I'll explain it in the next section.

Fancy Geofences

Imagine that you're waiting for a bus/train, and you'd like to be notified when the vehicle is x meters/yards away. What if you could subscribe to a service that would send you a notification/SMS when the vehicle is near you?

This is a geofencing problem, which Tile38 solves nicely. We could solve this problem by keeping a real-time collection of vehicles (with relevant attributes), and add geofence subscriptions based on your conditions.
When a vehicle enters a geofence, we notify you. Simple, right? Yes!

Geofancy

I prototyped a solution using Tile38 a few months back, and started working on a Java client that would abstract the Redis API away from me. I built Geofancy-Java shortly thereafter.

Geofancy exposes a gRPC endpoint, which I send the latest vehicles. It then writes those vehicles to Tile38. It also handles subscribing to webhooks. That's all it does!

Porting Geofancy to Rust

To be honest, Geofancy was the next smallest thing that I could port to Rust, given its simplicity. I built it in a few hours, and you can find it at geofancy-rs.

Performance Comparison

The main interesting thing about it is the performance that I'm seeing in prod.

I configured my http2 proxy to hit both geofancy-java and geofancy-rs endpoints with the same request, so both would perform the same task.

I stopped the Java impl, and started both when I deployed the Rust version. So far, I'm seeing 1/3 CPU usage, and 7MB vs 240MB RAM utilisation at off-peak. At peak, the Rust version barely touches 10MB, while the Java version will use up to 500MB.

geofancy-in-prod-1

Off-peak RAM util

The third service on the list, geofancy-vehicle-conduit is also a Rust service, that streams the rpc StreamTripUpdates (Scope) returns (stream TripUpdate) {} service with an infinite loop. It gets vehicles, and sends them to both geofancy services.

Summary

I've enjoyed my adventures with Rust so far, and with a very helpful community; I hope to build more with Rust.

I have a monolith Java service that generates those vehicle feeds that I talked about. At peak, it can consume over 30GB of RAM. I'm planning on breaking up the service and rewriting the performance-critical parts in Rust. That's a lower priority for now, but I'm excited about it.