TIL: 23rd July 2023

tokio::select! branches never run simultaneously§

One of the Rust's popular asynchronous runtimes — tokio has two methods of creating asynchronous tasks: tokio::task::spawn function and tokio::select! macro.

Spawning a task implies it will start being executed in the background immediately, which depending on a runtime choice, might be on the same or a different thread. This means it might run in parallel with the caller thread or other task threads.

Using the select! macro guarantees that all its branches will run multiplexed on the same task, meaning they will never run simultaneously while still running concurrently. This has the benefit of multiple branches immutably borrowing data or a single branch borrowing it mutably, while a spawned task must always own its data.

tracing is really easy with Rust§

Continuing to read the tokio docs, one of the topics that caught my attention was introducing tracing to your asynchronous code.

I got excited quickly due to how easy it is with the tracing package in Rust. Just add an #[tracing::instrument] attribute to your function an it will automatically emit a tracing span when the function is called, which can be customised as needed. It also provides structured logging using the tracing::event! macro that you can call anywhere.

use tracing::{instrument, event, Level}

fn hello(greeting: &str) {
    println!("Hello, {}", greeting);
    event!(Level::DEBUG, "greeting was printed", greeting = greeting)

Moreover, it's possible to instrument any code (e.g. a call in the third-party library) by placing it into a span's scope using the tracing::Span::in_scope function.

The package is extremely flexible and instrumentation could be integrated with a number of systems, e.g. there's a package for OpenTelemetry.