10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LabBaseテックカレンダーAdvent Calendar 2024

Day 25

The beauty of Rust in production

Last updated at Posted at 2024-12-25

fn main()

Many companies consider using Rust in their products. While most wasting their time figuring out whether it's the right choice for them, a few actually make the jump.
Welcome to my rant about Rust! In this article I'm going to talk about the benefits of using Rust in production.

let _ = tokio::spawn(boring_introduction);

Now that all the people that don't like Rust have left the article, I can get into the real thing.
Nice to meet you everyone, I'm one of the Software Engineers at LabBase, where we mainly use Rust for our backend - and recently launced a project that uses Rust in the front as well!
This article is written from the POV of someone that mostly write http servers using Rust.

fn types(reader: User<impl permissions::Read>)

The type system in Rust is the best thing you'll ever get from using Rust in production. Rust is famously known for its memory safety and speed, but it doesn't really stop there. With an extremely flexible type system, Rust allows us define any kind of invariant into the type system which speeds up development and prevent bugs at compile time. Writing a test is nice - but when you can't compile a wrong program you simply can not get it into production! The title of this part is based on real code that runs on our servers in LabBase. By defining our permission system using the type system, a function that expects a user to have write and read permissions can simply define it in the signature:
user: User<impl permissions::Read + permissions::Write>
and now it's impossible to call this function without having the correct permissions. The part that instantiate the User struct is a small block of code that makes the correct checks against the DB, and it's the most critical part of this system. Every change in this part needs to be done carefully - but it does not need to be updaded frequently and once it's done you simple know that the rest of the system is safe.

#[cfg(test)]

The type system is powerful and it's possible to encode many invariants into it, but since code still contain logic it's important to make sure that the logic is correct. Rust provides us with a strong testing system that is built right into the language and makes it easy to write tests. My favorite part about this system is that code in documentation also turns into tests, which means that it's usually up-to-date!

Here is an example for a test that confirms that the size of a data-structure in our system fits into the limitation Cognito enforces on us.

#[test]
fn fit_in_cognito_limitation() {
    let (redacted, _) = Redacted::new(REDACTED).expect("create redacted");

    let serialized = bincode::serialize(&redacted).expect("serialize the redacted");
    assert!(
        base64::prelude::BASE64_STANDARD_NO_PAD
            .encode(serialized)
            .len()
            < MAX_COGNITO_ATTRIBUTE_SIZE
    )
}

macro_rules! generate_body

Rust has a powerful macro system that allows us define macros that save us from repeating code in places where introducting a function will either be inefficient, complex or even impossible - and repeating the code will be tedious and easily introduce bugs. A macro is easy to define, is able to catch variables that are defined before it and is type safe.
Macros are just part of normal code, I write them all the time and I find them the most useful in 3 situations:

  1. In a block where you need to terminate the program flow - for example, returning from a function - in multiple places, defining a macro that can contain a shared condition is extremely useful. for example, the following macro will continue to the next iteration pushing the current record to the list of failed records, so I can export a report of all the records that have failed the migration
    macro_rules! cont_failed {
        () => {
            failed.push(record);
            continue;
        };
    }
    
  2. In tests, I usually use macros to define some kind of a very simplified DSL that allows me to easily write the conditions I want to test. For example, here is a macro that checks all the endpoints in charge of creating/updating/sending/deleting drafts in our system, and it looks like this:
    draft!(update => "test-2", expect_success);
    draft!(update => "test-3", expect_failure);
    draft!(update => "test-4";version, expect_success);
    draft!(delete version, expect_failure);
    
    the macro will send a request to the endpoint, and check that what we expect (failure or success) is actually held correct. Defining a DSL like this makes it easy to write flexible tests that allow me to cover all of the requirements.
  3. The final is a macro that copies rust behaviour, and expands its abilities. For example, a macro that wraps around a struct to define many repetitive behaviours around it. In rust, we have procedural macros that can do the same thing, but writing them takes time and when I have a very specific case it's simply easier and simpler to write a macro_rules that will do the exact same thing!

async fn maybe()

Async Rust build around the idea of control: whether it means that we can easily race between tasks, wait for all of them to finish, move them to other threads or cancel them on specific conditions. The language itself only provides the basic idea (defined by traits, syntax sugar and helpers around it) that make it easy to write async code and code that will run it - called a runtime or a scheduler. There are some bad design coices and the need to use an external runtime means that the language is "colored", but choosing the popular Tokio runtime is just good enough for a web-server or a micro-service.

Arc::new(speed)

Rust is fast - and when you care about speed it provides with all the tools that one will need to optimize a critical path that can make the entire system feel (and become) faster, but I would not recommend overdoing it when not necessary. My rule of thumb is that if it's easy to write fast code - just do it, but when lifetimes get in the way and cloning(copying) the variable will just solve the problem, it's fine to just .clone it! There is no need to care too much about every line. Rust is already faster and more efficient than the other popular choices people use.

std::proccess::exit(ExitCode::SUCCESS)

Thank you for reading my rant about Rust! (Happy Christmas :christmas_tree:)

And for the people that still look for the bad about Rust, wait for the next part of this article

Building [=======================> ] 359/356: next-part
10
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?