Why did I choose Rust? Rusts’ memory management introduces a steep learning curve. Its ecosystem isn’t developed as much as that of some other languages. Yet, Rust performs great, comes with some of the best support for web-assembly, and still manages to be an expressive language. Let’s review these properties in the context of an actual use case.
A decentralized computing infrastructure
My use case is building a decentralized, interest-based computing infrastructure. The ambition is to maximally use the hardware owned by users for the interest of their owners. The devices collectively host a global address space for typed variables. Peers may subscribe to variables of their interest, receiving both existing state and future updates. Peers can mutate data at any time and replicate changes to subscribed peers when online. Finally, peers may define functions that operate on changes and store the results as new variables.
The architecture of the network overlay and replication mechanism resembles that of IPFS PubSub. It hosts conflict-free replicated data types (CRDTs) on a semi-structured peer to peer network. CRDTs exploit monotonicity, which requires that updates inflate the data structure. Adding elements is thus allowed. Removing elements is accomplished by increasing host specific versions. Merging monotonic updates leads to consistent results across peers, making it safe to share data at any scale. Monotonic updates can also be transformed, filtered and aggregated. Lasp is an example that enables such operations for Set CRDTs. The ambition is to support a wide range of data types and provide the ability to express useful computations appropriate to those types.
I have conducted my experiments in Scala thus far, but have chosen Rust to build the system as a whole. Here’s why:
The Strengths of Rust
Strength 1: System-level performance
Rust performs close to the level of C and C++. Zero cost abstractions permit composition without loss of efficiency. A hand-optimized solution won’t outperform one that composes abstractions. Retaining efficiency is a great asset for battery and CPU constrained devices. Much of this efficiency is realized by Rusts’ deterministic memory management. This is not only a benefit for memory-constrained devices. Predictable allocation and deallocation mean predictable performance. This is a great benefit for real-time collaborative applications. To appeal to authority: Google chose Rust to develop their collaborative editor Xi.
You might wonder, why would we need to run on machines with such constrained hardware? The truth is that consumers increasingly use mobile devices. Their unstable connectivity is a challenge for the overall quality of a decentralized service. This instability can be overcome but at the cost of greater hardware utilization. We know the opposite to be desirable. Ideally, we would be able to suspend service to save battery. An efficient language aids in reducing the overhead, but isn’t enough on its own.
Efficiency does help to include more types of devices in the network. Modems and routers have constrained hardware, but stable connectivity. This makes them well suited for overlay management. Also, they enable bypassing the NAT traversal problem that complicates peer to peer system design. Another interesting type of device to include is network attached storage (NAS). Mobile devices can offload network-overlay and replication responsibilities to NASses when available. Choosing Rust thus enables the infrastructure to run on a large number of different devices.
Strength 2: Security by isolating system components
The desire for completely open systems strongly contrasts with security. The choice of language alone doesn’t solve that, but can be part of a larger security strategy.
One can distinguish a few types of risks for a p2p application hosting shared mutable state. My top three would be:
1) taking over the system in an epidemic fashion
2) impersonation of the owner(s) of a machine
3) destructive manipulation of locally stored data
These risks are mitigated by isolating system components. Each system component can run in its own sandbox, separated by securely-exposed interfaces. Of all sandboxes available, those that run scripts in web browsers are the most mature. Their widespread adoption gives maximal incentive to exploit. Eventually, sandboxes running Web Assembly (WASM) will start providing the same isolation guarantees.
Rusts’ lack of complex runtime eases compilation to WASM. Its support is already on the higher end among languages. We can expect this trend to continue, given that Mozilla is the primary player behind Rust.
Strength 3: The power of abstraction
As mentioned before, peers can subscribe to incremental updates to variables. Such changes can be transformed, filtered, aggregated and then stored as new variables. Ensuring their monotonicity is non-trivial, however. Fortunately, it is possible to abstract much of that complexity away behind dataflow operations.
Dataflow models that employ monotonicity and incrementality are explored extensively in recent years. Lasp, Differential Dataflow (developed in Rust!) and Structured Streaming all demonstrate practical data-flow APIs. Functional programming is well suited to embed dataflow languages. It enables both a declarative and an imperative style of programming. A declarative style benefits maintainability as internal complexity is hidden. An imperative style provides the control to keep complex compositions performant. Rust is not the most advanced functional programming language. But, it does offer type parameters, type classes, and higher-order functions. These are the primary mechanisms I used during my Scala experiments, so I expect Rust to be enough.
The risks in choosing Rust
So far, we have considered the strengths of Rust, how about its weaknesses? Are they a problem, and if so, acceptable? I don’t think Rust has major weaknesses, but it does have two major risks. These are of the magnitude that I wouldn’t, as a consultant, recommend the language to many of my clients. Let’s see how they apply to my use case!
Risk 1: Rust has a steep learning curve
The main selling point of Rust, its typed memory management, is also a major source of complexity. Rust introduces new concepts that will take time to get used to. You will have to postpone running your code because Rust doesn’t trust its memory safety yet. This can be a source of frustration and leave one feeling unproductive. It may feel like you’re thinking about memory management all the time instead of on a few critical paths.
However, that complexity is not incidental. It is an explicit design choice that helps prevent errors early on. Errors such as segmentation faults or garbage collection stalls are hard to prevent through tests. They are especially frustrating to diagnose and reproduce once occurring on production. My use case is challenging from a continuous deployment perspective, as I have little control over when updates take place. I cannot afford a test-on-production approach, as there is no way to recover. Preventing errors early on is a must, so Rust’s complex memory management pays for itself in my case.
Risk 2: Rust has a small ecosystem
When compared to some popular languages such as C# and Java, Rust’s ecosystem is tiny. Despite winning Stackoverflow’s “most loved language” three years in a row, Rust still has a small community. It also has little enterprise backing besides Mozilla itself. A smaller community isn’t all bad, as it promotes focused efforts on few initiatives. In larger ecosystems, there is a great deal of experimentation. This introduces the risk to choose solutions that lose community support. However, that experimentation also drives up the quality of longer-lived initiatives. Due to the above, production-ready Rusts’ libraries are small in number.
The above hints at the following general conclusion. You can choose Rust when your use case is either simple (especially from an integration perspective) or when it is exotic. In the first case, frameworks would only provide feature bloat. In the second case, a community is likely not of help anyway. The converse would be: don’t choose Rust if you want to do something complex that everybody is already doing. Don’t expect optimal productivity when writing the next multi-cloud microservices architecture in Rust.
My use case categorizes as exotic. P2P systems are rare and mature tooling is near inexistent regardless of language. For my use case, I require support for protocols (TCP, HTTP, and UDP) and serialization (JSON, Protobuf). Rust has sufficient support for these.
Conclusion
Building a decentralized mutable datastore comes with great challenges. Choosing the appropriate language helps to reduce the problem space significantly. Rust’s system’s level performance allows inclusion of a wide range of devices with different strengths. Rusts’ WASM compilation grants improved security through component isolation. Rusts’ expressivity doesn’t only help me to deal with the complexity of this domain, but also enables convenient APIs to operate on the data.
The general risks of Rust don’t apply to my use case. Rusts’ non-trivial memory management pays off when working in a highly concurrent environment. Rusts’ smaller ecosystem is hardly a problem when communicating with other peers is your main concern.
I hope you enjoyed this blog. If you did, consider joining our Rust Meetup September 18th.