• Stars
    star
    151
  • Rank 246,057 (Top 5 %)
  • Language
    Rust
  • License
    GNU General Publi...
  • Created almost 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

extremely boring async function runner!

extreme!

extremely boring async function runner, written in 44 lines of 0-dependency Rust.

why?

I teach custom Rust workshops that cover a wide variety of low-level subjects. This lays bare the essential runtime complexity of Rust's async functionality for educational purposes.

documentation

/// Run a `Future`.
pub fn run<F, O>(f: F) -> O
where
    F: Future<Output = O>

implementation

use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

#[derive(Default)]
struct Park(Mutex<bool>, Condvar);

fn unpark(park: &Park) {
    *park.0.lock().unwrap() = true;
    park.1.notify_one();
}

static VTABLE: RawWakerVTable = RawWakerVTable::new(
    |clone_me| unsafe {
        let arc = Arc::from_raw(clone_me as *const Park);
        std::mem::forget(arc.clone());
        RawWaker::new(Arc::into_raw(arc) as *const (), &VTABLE)
    },
    |wake_me| unsafe { unpark(&Arc::from_raw(wake_me as *const Park)) },
    |wake_by_ref_me| unsafe { unpark(&*(wake_by_ref_me as *const Park)) },
    |drop_me| unsafe { drop(Arc::from_raw(drop_me as *const Park)) },
);

/// Run a `Future`.
pub fn run<F: std::future::Future>(mut f: F) -> F::Output {
    let mut f = unsafe { std::pin::Pin::new_unchecked(&mut f) };
    let park = Arc::new(Park::default());
    let sender = Arc::into_raw(park.clone());
    let raw_waker = RawWaker::new(sender as *const _, &VTABLE);
    let waker = unsafe { Waker::from_raw(raw_waker) };
    let mut cx = Context::from_waker(&waker);

    loop {
        match f.as_mut().poll(&mut cx) {
            Poll::Pending => {
                let mut runnable = park.0.lock().unwrap();
                while !*runnable {
                    runnable = park.1.wait(runnable).unwrap();
                }
                *runnable = false;
            }
            Poll::Ready(val) => return val,
        }
    }
}

how does it work?

Rust async blocks and functions evaluate to an implementation of the Future trait, which has one method: poll. If you want to run a Rust Future by calling its poll method, you need to have a Context that you can pass to it. This Context allows the Future to have some information about the system it is running inside of. In particular, a Context provides access to a Waker, which is essentially just a raw pointer and a RawWakerVTable which can be thought of almost like a trait implementation. The Waker allows the Future to notify the runtime at a later time, communicating that it should be polled again.

bugs encountered over time

But it didn't always look this way... Here is a mostly-complete account of the reliability-related engineering effort that went into this.

  1. use after free of thread, where the backing future could clone the waker and send a reference to the thread object that lives in the stack frame of the run function. If the backing Future wakes the waker after the run function returns, it's a use after free. caught by @stjepang
  2. use after free due to accidentally dropping an Arc in the vtable clone, caught with ASAN via the sanitizer script
  3. race condition triggered by usage of thread::park, which can race when other code may rely on thread parking, like std::sync::Once being used from the called Future's poll method. caught by @tomaka
  4. potential correctness issue in pin usage, alleviated by shadowing the input future to guarantee that it is never reused, and using Pin::as_mut in the poll loop instead of creating a new Pin in each loop. caught by @withoutboats

miri, LSAN, and TSAN have also been run on this code, although they have not found issues yet.

conclusion

Runtimes can be pretty simple. Simple is not easy. unsafe must be paired with tools like the sanitizer script in this repo, and ideally as much peer feedback as possible. None of the people mentioned above were requested to look at this code, but because it was only a few dozen lines long, they took a look and provided helpful feedback. Time spent making things look cute and short turned into peer feedback that caught real bugs.

More Repositories

1

sled

the champagne of beta embedded databases
Rust
8,111
star
2

tla-rust

writing correct lock-free and distributed stateful systems in Rust, assisted by TLA+
TLA
1,040
star
3

rio

pure rust io_uring library, built on libc, thread & async friendly, misuse resistant
Rust
934
star
4

paxos

simple CASPaxos implementation written in rust on top of a simulator for finding bugs quickly
Rust
139
star
5

loghisto

counters and logarithmically bucketed histograms for distributed systems
Go
82
star
6

rasputin

(getting to be a) hard to kill scalable linearizabe store
Rust
76
star
7

seaslug

scraps of a potential language
Rust
36
star
8

rsdb

rust database engineering toolkit
Rust
33
star
9

model

model testing sugar for testing interactions on structures over time
Rust
27
star
10

crack

building blocks and testing tools for reliable systems
Rust
27
star
11

historian

Rust
22
star
12

mesos-rs

Mesos bindings using the v1 HTTP API
Rust
17
star
13

paranoia

Rust
16
star
14

assert_panic_free

Rust
15
star
15

tx

software transactional memory in rust
Rust
13
star
16

sludge

speedy web micro-framework using sled, io_uring and SIMD
Rust
12
star
17

acid-state

rust transactional state library
Rust
11
star
18

quickcheck-tut

Rust
10
star
19

state-guide

overviews of consistency, availability and durability
8
star
20

slides

7
star
21

icefall

eventually consistent location-agnostic serverless and mobile database
7
star
22

deterministic

utilities for imposing determinism on random number generators, thread execution, etc...
Rust
7
star
23

triple

embeddable/standalone rust triple store
Rust
6
star
24

test-idioms

Rust
5
star
25

lathe

performant self-healing distributed micro-framework in rust
4
star
26

dots

Haskell
4
star
27

hardware-effects-rs

A Rust port of Kobzol/hardware-effects demonstrating performance issues
4
star
28

inlinable-box

auto-box things that fit in a usize
Rust
4
star
29

lfsr

simple linear feedback shift registers for test case generation
Rust
4
star
30

programming-in-haskell

exercises, experiments, examples
Haskell
4
star
31

llrb-rs

left-leaning red-black tree
Rust
4
star
32

idgen.go

very fast monotonic ID generator
3
star
33

dist-sys

well-tested flexible distributed systems building blocks
3
star
34

eurorack

patches for the droid eurorack modular sequencer
3
star
35

rust-reactive-log

performant transactional composable persistence
Rust
3
star
36

zk-glove

command line tools for distributed orchestration and discovery
Go
3
star
37

hemlock

transactional distributed store with SSI on mesos
C++
2
star
38

rust-srv

simple portable libresolv-backed SRV record resolution
Rust
2
star
39

rust-futurepool

simple future pool library for predictable concurrency
Rust
2
star
40

tikv-client

TiKV client powered by Tokio
Rust
2
star
41

belasitsa

a tiny green WSGI back end for Mongrel 2
Python
2
star
42

ak47

extremely reliable networked systems toolkit
2
star
43

quark

distributed liveness telemetry
Erlang
2
star
44

determine-disk-block-size

2
star
45

sitecache

transparent distributed caching proxy for distribution of static files in a datacenter
Go
2
star
46

googleapis-rs

mechanically generated rust gRPC bindings for the google APIs
Rust
2
star
47

quick

property testing with shrinking, drop-in compatible with testing/quick
Go
2
star
48

cyborg-training-camp

C
1
star
49

faultcheck

quickcheck + black box fault injection
1
star
50

disruptors

dirsuptor patterns in different languages
1
star
51

id-gen

Distributed system simulation: ID generator
Rust
1
star
52

cavemon

external process instrumentation from the terminal
1
star