Skip to content

Commit

Permalink
Update Rust cancel safe article with minor clarifications
Browse files Browse the repository at this point in the history
  • Loading branch information
nazmulidris committed Oct 3, 2024
1 parent d5967f0 commit 4b7ff24
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 49 deletions.
37 changes: 23 additions & 14 deletions _posts/2024-07-10-rust-async-cancellation-safety-tokio.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ categories:
<!-- /TOC -->

## Introduction
<a id="markdown-introduction" name="introduction"></a>

This tutorial, video, and repo are a deep dive into the concept of cancellation safety in
async code using Tokio and Rust. It affects the `tokio::select!` macro, and what happens
Expand Down Expand Up @@ -89,7 +88,6 @@ require you to call `.await`. The code it generates take care of this.
> examples below.
## What can go wrong when racing futures?
<a id="markdown-what-can-go-wrong-when-racing-futures%3F" name="what-can-go-wrong-when-racing-futures%3F"></a>

If you recall, in Rust, a `Future` is just a data structure that doesn't really do
anything until you `.await` it.
Expand Down Expand Up @@ -120,7 +118,6 @@ async streams, etc. you will be fine. However if you're maintaining state inside
happens.

## YouTube video for this article
<a id="markdown-youtube-video-for-this-article" name="youtube-video-for-this-article"></a>

This blog post has examples from this live coding video. If you like
to learn via video, please watch the companion video on the [developerlife.com YouTube
Expand All @@ -139,7 +136,6 @@ channel](https://www.youtube.com/@developerlifecom).
<br/>

## Examples of cancellation safety in async Rust using tokio::select!
<a id="markdown-examples-of-cancellation-safety-in-async-rust-using-tokio%3A%3Aselect!" name="examples-of-cancellation-safety-in-async-rust-using-tokio%3A%3Aselect!"></a>

Let's create some examples to illustrate how to use the typestate pattern in Rust. You can run
`cargo new --lib async_cancel_safe` to create a new library crate.
Expand Down Expand Up @@ -167,7 +163,6 @@ futures-util = "0.3.30"
We are going to add all the examples below as tests to the `lib.rs` file in this crate.

### Example 1: Right and wrong way to sleep, and interval
<a id="markdown-example-1%3A-right-and-wrong-way-to-sleep%2C-and-interval" name="example-1%3A-right-and-wrong-way-to-sleep%2C-and-interval"></a>

Add the following code to your `lib.rs` file. Both these examples show similar ways of using
`tokio::time::sleep(..)` incorrectly in a `tokio::select!` block.
Expand All @@ -191,7 +186,9 @@ async fn test_sleep_right_and_wrong_ways_v1() {
tokio::select! {
// Branch 1 (right way)
// This branch executes a deterministic number of times. The same
// sleep future is re-used on each iteration.
// sleep future is re-used on each iteration. Once the sleep "expires"
// it stays "expired"! This is the desired behavior:
// https://docs.rs/tokio/latest/tokio/time/struct.Sleep.html
_ = &mut sleep => {
println!("branch 1 - tick : {count}");
count -= 1;
Expand Down Expand Up @@ -258,13 +255,29 @@ You can run these tests to see what they do by running the following in your ter
They are flaky and its not possible to really make accurate assertions at the end of
each of these tests.

Let's break down `v1` first to see what is happening.
Let's break down `v1` first to see what is happening. Here's the output:

```
---- test_sleep_right_and_wrong_ways_v1 stdout ----
branch 2 - sleep : 5, elapsed: 101 ms
branch 1 - tick : 5, elapsed: 101 ms
branch 1 - tick : 4, elapsed: 101 ms
branch 1 - tick : 3, elapsed: 101 ms
branch 1 - tick : 2, elapsed: 101 ms
branch 1 - tick : 1, elapsed: 101 msk
```

- Branch 1 (right way): This branch executes a deterministic number of times. The same
sleep future is re-used on each iteration. This is achieved using the `tokio::pin!`
macro. Since futures are stateful, ensuring that the same one is re-used between
iterations of the `loop` ensures that state isn't lost when the other branch is
executed, or when this branch finishes and its future is dropped.
macro. Here are the [docs](https://docs.rs/tokio/latest/tokio/time/struct.Sleep.html) on
how to use `Sleep` in `tokio::select!` blocks. Since futures are stateful, ensuring that
the same one is re-used between iterations of the `loop` ensures that state isn't lost
when the other branch is executed, or when this branch finishes and its future is
dropped. Notice that the first time in `branch 1` the code waits for 100ms, and then the
subsequent 4 iterations of the loop do not wait at all! This is because the `sleep`
future is in a `Ready` state after the first iteration, and effectively, we only wait
100ms in this loop. For those familiar with Javascript, this is akin to `setTimeout` and
not `setInterval` semantics.
- Branch 2 (wrong way): This branch is executed a non deterministic number of times. This
is because the sleep future is not pinned. It is dropped when the other branch is
executed. Then on the next iteration, a **new** sleep future is created. This means that
Expand All @@ -280,7 +293,6 @@ Let's break down `v2` next.
- Branch 2 (wrong way): Same as before.

#### Difference between interval and sleep
<a id="markdown-difference-between-interval-and-sleep" name="difference-between-interval-and-sleep"></a>

This is the mental model that I've developed for using these.

Expand All @@ -294,7 +306,6 @@ This is the mental model that I've developed for using these.
loop. And even accumulate how many times it runs to decide when to break.

### Example 2: Safe cancel of a future using interval and mpsc channel
<a id="markdown-example-2%3A-safe-cancel-of-a-future-using-interval-and-mpsc-channel" name="example-2%3A-safe-cancel-of-a-future-using-interval-and-mpsc-channel"></a>

Add the following snippet to your `lib.rs` file.

Expand Down Expand Up @@ -365,7 +376,6 @@ Let's break down what's happening in this test.
look at the `vec` that we accumulate outside of the `loop` this contains what we expect.

### Example 3: Inducing cancellation safety issues
<a id="markdown-example-3%3A-inducing-cancellation-safety-issues" name="example-3%3A-inducing-cancellation-safety-issues"></a>

This is the example we have all been waiting for. Let's start with copying the
following snippet in your `lib.rs` file. We will create a new module here.
Expand Down Expand Up @@ -532,7 +542,6 @@ pinned `Vec` to get around this issue.
> in some buffer is dropped, then this is not a problem.
## Build with Naz video series on developerlife.com YouTube channel
<a id="markdown-build-with-naz-video-series-on-developerlife.com-youtube-channel" name="build-with-naz-video-series-on-developerlife.com-youtube-channel"></a>

> If you have comments and feedback on this content, or would like to request new content
> (articles & videos) on developerlife.com, please join our [discord
Expand Down
36 changes: 22 additions & 14 deletions docs/2024/07/10/rust-async-cancellation-safety-tokio/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ <h2 id="introduction">

</h2>

<p><a id="markdown-introduction" name="introduction"></a></p>

<p>This tutorial, video, and repo are a deep dive into the concept of cancellation safety in
async code using Tokio and Rust. It affects the <code class="language-plaintext highlighter-rouge">tokio::select!</code> macro, and what happens
Expand Down Expand Up @@ -320,7 +319,6 @@ <h2 id="what-can-go-wrong-when-racing-futures">

</h2>

<p><a id="markdown-what-can-go-wrong-when-racing-futures%3F" name="what-can-go-wrong-when-racing-futures%3F"></a></p>

<p>If you recall, in Rust, a <code class="language-plaintext highlighter-rouge">Future</code> is just a data structure that doesn’t really do
anything until you <code class="language-plaintext highlighter-rouge">.await</code> it.</p>
Expand Down Expand Up @@ -363,7 +361,6 @@ <h2 id="youtube-video-for-this-article">

</h2>

<p><a id="markdown-youtube-video-for-this-article" name="youtube-video-for-this-article"></a></p>

<p>This blog post has examples from this live coding video. If you like
to learn via video, please watch the companion video on the <a href="https://www.youtube.com/@developerlifecom">developerlife.com YouTube
Expand All @@ -382,7 +379,6 @@ <h2 id="examples-of-cancellation-safety-in-async-rust-using-tokioselect">

</h2>

<p><a id="markdown-examples-of-cancellation-safety-in-async-rust-using-tokio%3A%3Aselect!" name="examples-of-cancellation-safety-in-async-rust-using-tokio%3A%3Aselect!"></a></p>

<p>Let’s create some examples to illustrate how to use the typestate pattern in Rust. You can run
<code class="language-plaintext highlighter-rouge">cargo new --lib async_cancel_safe</code> to create a new library crate.</p>
Expand Down Expand Up @@ -417,7 +413,6 @@ <h3 id="example-1-right-and-wrong-way-to-sleep-and-interval">

</h3>

<p><a id="markdown-example-1%3A-right-and-wrong-way-to-sleep%2C-and-interval" name="example-1%3A-right-and-wrong-way-to-sleep%2C-and-interval"></a></p>

<p>Add the following code to your <code class="language-plaintext highlighter-rouge">lib.rs</code> file. Both these examples show similar ways of using
<code class="language-plaintext highlighter-rouge">tokio::time::sleep(..)</code> incorrectly in a <code class="language-plaintext highlighter-rouge">tokio::select!</code> block.</p>
Expand All @@ -440,7 +435,9 @@ <h3 id="example-1-right-and-wrong-way-to-sleep-and-interval">
<span class="nn">tokio</span><span class="p">::</span><span class="nd">select!</span> <span class="p">{</span>
<span class="c1">// Branch 1 (right way)</span>
<span class="c1">// This branch executes a deterministic number of times. The same</span>
<span class="c1">// sleep future is re-used on each iteration.</span>
<span class="c1">// sleep future is re-used on each iteration. Once the sleep "expires"</span>
<span class="c1">// it stays "expired"! This is the desired behavior:</span>
<span class="c1">// https://docs.rs/tokio/latest/tokio/time/struct.Sleep.html</span>
<span class="n">_</span> <span class="o">=</span> <span class="o">&amp;</span><span class="k">mut</span> <span class="n">sleep</span> <span class="k">=&gt;</span> <span class="p">{</span>
<span class="nd">println!</span><span class="p">(</span><span class="s">"branch 1 - tick : {count}"</span><span class="p">);</span>
<span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span><span class="p">;</span>
Expand Down Expand Up @@ -509,14 +506,29 @@ <h3 id="example-1-right-and-wrong-way-to-sleep-and-interval">
<p>They are flaky and its not possible to really make accurate assertions at the end of
each of these tests.</p>

<p>Let’s break down <code class="language-plaintext highlighter-rouge">v1</code> first to see what is happening.</p>
<p>Let’s break down <code class="language-plaintext highlighter-rouge">v1</code> first to see what is happening. Here’s the output:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---- test_sleep_right_and_wrong_ways_v1 stdout ----
branch 2 - sleep : 5, elapsed: 101 ms
branch 1 - tick : 5, elapsed: 101 ms
branch 1 - tick : 4, elapsed: 101 ms
branch 1 - tick : 3, elapsed: 101 ms
branch 1 - tick : 2, elapsed: 101 ms
branch 1 - tick : 1, elapsed: 101 msk
</code></pre></div></div>

<ul>
<li>Branch 1 (right way): This branch executes a deterministic number of times. The same
sleep future is re-used on each iteration. This is achieved using the <code class="language-plaintext highlighter-rouge">tokio::pin!</code>
macro. Since futures are stateful, ensuring that the same one is re-used between
iterations of the <code class="language-plaintext highlighter-rouge">loop</code> ensures that state isn’t lost when the other branch is
executed, or when this branch finishes and its future is dropped.</li>
macro. Here are the <a href="https://docs.rs/tokio/latest/tokio/time/struct.Sleep.html">docs</a> on
how to use <code class="language-plaintext highlighter-rouge">Sleep</code> in <code class="language-plaintext highlighter-rouge">tokio::select!</code> blocks. Since futures are stateful, ensuring that
the same one is re-used between iterations of the <code class="language-plaintext highlighter-rouge">loop</code> ensures that state isn’t lost
when the other branch is executed, or when this branch finishes and its future is
dropped. Notice that the first time in <code class="language-plaintext highlighter-rouge">branch 1</code> the code waits for 100ms, and then the
subsequent 4 iterations of the loop do not wait at all! This is because the <code class="language-plaintext highlighter-rouge">sleep</code>
future is in a <code class="language-plaintext highlighter-rouge">Ready</code> state after the first iteration, and effectively, we only wait
100ms in this loop. For those familiar with Javascript, this is akin to <code class="language-plaintext highlighter-rouge">setTimeout</code> and
not <code class="language-plaintext highlighter-rouge">setInterval</code> semantics.</li>
<li>Branch 2 (wrong way): This branch is executed a non deterministic number of times. This
is because the sleep future is not pinned. It is dropped when the other branch is
executed. Then on the next iteration, a <strong>new</strong> sleep future is created. This means that
Expand All @@ -541,7 +553,6 @@ <h4 id="difference-between-interval-and-sleep">

</h4>

<p><a id="markdown-difference-between-interval-and-sleep" name="difference-between-interval-and-sleep"></a></p>

<p>This is the mental model that I’ve developed for using these.</p>

Expand All @@ -563,7 +574,6 @@ <h3 id="example-2-safe-cancel-of-a-future-using-interval-and-mpsc-channel">

</h3>

<p><a id="markdown-example-2%3A-safe-cancel-of-a-future-using-interval-and-mpsc-channel" name="example-2%3A-safe-cancel-of-a-future-using-interval-and-mpsc-channel"></a></p>

<p>Add the following snippet to your <code class="language-plaintext highlighter-rouge">lib.rs</code> file.</p>

Expand Down Expand Up @@ -640,7 +650,6 @@ <h3 id="example-3-inducing-cancellation-safety-issues">

</h3>

<p><a id="markdown-example-3%3A-inducing-cancellation-safety-issues" name="example-3%3A-inducing-cancellation-safety-issues"></a></p>

<p>This is the example we have all been waiting for. Let’s start with copying the
following snippet in your <code class="language-plaintext highlighter-rouge">lib.rs</code> file. We will create a new module here.</p>
Expand Down Expand Up @@ -818,7 +827,6 @@ <h2 id="build-with-naz-video-series-on-developerlifecom-youtube-channel">

</h2>

<p><a id="markdown-build-with-naz-video-series-on-developerlife.com-youtube-channel" name="build-with-naz-video-series-on-developerlife.com-youtube-channel"></a></p>

<blockquote>
<p>If you have comments and feedback on this content, or would like to request new content
Expand Down
4 changes: 2 additions & 2 deletions docs/authors/nadiaidris/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<meta property="og:url" content="http://developerlife.com/authors/nadiaidris/" />
<meta property="og:site_name" content="developerlife.com" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-09-30T09:26:40-05:00" />
<meta property="article:published_time" content="2024-10-03T11:56:29-05:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="Nadiaidris" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Nazmul Idris"},"dateModified":"2024-09-30T09:26:40-05:00","datePublished":"2024-09-30T09:26:40-05:00","description":"Nadia is a product designer turned web engineer specializing in TypeScript and React.","headline":"Nadiaidris","mainEntityOfPage":{"@type":"WebPage","@id":"http://developerlife.com/authors/nadiaidris/"},"url":"http://developerlife.com/authors/nadiaidris/"}</script>
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Nazmul Idris"},"dateModified":"2024-10-03T11:56:29-05:00","datePublished":"2024-10-03T11:56:29-05:00","description":"Nadia is a product designer turned web engineer specializing in TypeScript and React.","headline":"Nadiaidris","mainEntityOfPage":{"@type":"WebPage","@id":"http://developerlife.com/authors/nadiaidris/"},"url":"http://developerlife.com/authors/nadiaidris/"}</script>
<!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/assets/main.css" />

Expand Down
4 changes: 2 additions & 2 deletions docs/authors/nazmulidris/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
<meta property="og:url" content="http://developerlife.com/authors/nazmulidris/" />
<meta property="og:site_name" content="developerlife.com" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-09-30T09:26:40-05:00" />
<meta property="article:published_time" content="2024-10-03T11:56:29-05:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="Nazmulidris" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Nazmul Idris"},"dateModified":"2024-09-30T09:26:40-05:00","datePublished":"2024-09-30T09:26:40-05:00","description":"Nazmul is a software engineer focused on Rust, TUI, Web, and Android technologies.","headline":"Nazmulidris","mainEntityOfPage":{"@type":"WebPage","@id":"http://developerlife.com/authors/nazmulidris/"},"url":"http://developerlife.com/authors/nazmulidris/"}</script>
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Nazmul Idris"},"dateModified":"2024-10-03T11:56:29-05:00","datePublished":"2024-10-03T11:56:29-05:00","description":"Nazmul is a software engineer focused on Rust, TUI, Web, and Android technologies.","headline":"Nazmulidris","mainEntityOfPage":{"@type":"WebPage","@id":"http://developerlife.com/authors/nazmulidris/"},"url":"http://developerlife.com/authors/nazmulidris/"}</script>
<!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/assets/main.css" />

Expand Down
Loading

0 comments on commit 4b7ff24

Please sign in to comment.