This week I decided that my Pocket to Buffer via IFTTT setup wasn’t cutting it anymore. Predominantly because I use a trial account of buffer and I can only queue ten items at a time. The developer in me screamed “you can build it yourself.” Although normally I would lean away from the idea I haven’t had much time to write Rust lately and decided it would be nice to throw together a few crates and get the job done.

I reached out for cargo, got things started and pretty quickly hit the following error.

thread 'main' panicked at 'not currently running on a Tokio 0.2.x runtime.'

I had included tokio but I was running the latest version 1.15.0. The error was pretty shallow telling me it was originating on the main call which I happened to setup as a tokio::main. First thing first lets get a deeper stack trace.

root@b4c0a85e75ae:/usr/src# RUST_BACKTRACE=1 cargo run

This eventually lead me to a call being made by one of the dependencies I had included. I looked up the deps of that dependency and noticed it had pinned tokio 0.2.0. A look in the build dependencies directory shows sure enough the app built with both tokio 0.2.0 and 1.15.0.

The screen capture of the build
directory showing the same dependencies compiled with multiple versions

My assumption was that the higher pinned version would have taken precedence, and this is when I learned that Cargo supports multiple version dependencies.

If multiple packages have a common dependency with semver-incompatible versions, then Cargo will allow this, but will build two separate copies of the dependency.

This all means the dependency I brought in is locked to tokio 0.2.0 and attempting to operate in the 1.15.0 runtime. The creators of tokio have prepared for these circumstances and this is where tokio-compat comes in. As tokio progressed with breaking changes they introduced methods to encapsulate older api usage.

tokio-compat-example.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::time::{Duration, Instant};
use futures_01::future::lazy;
use tokio_compat::prelude::*;

tokio_compat::run_std(async {
    // Wait for a `tokio` 0.1 `Delay`...
    let when = Instant::now() + Duration::from_millis(10);
    tokio_01::timer::Delay::new(when)
        // convert the delay future into a `std::future` that we can `await`.
        .compat()
        .await
        .expect("tokio 0.1 timer should work!");
    println!("10 ms have elapsed");

    // Wait for a `tokio` 0.2 `Delay`...
    let when = Instant::now() + Duration::from_millis(20);
    tokio_02::timer::delay(when).await;
    println!("20 ms have elapsed");
});

To fix my particular problem I used run_std to encapsulate the external crate call.

main.rs
1
2
3
tokio_compat::run_std(async {
    let auth = CrateAuth::new(&consumer_key);
});

With the tokio_compat::run_std function and an anonymous async function we’re ready for a successful compilation, but how does it work?

Tokio relies on the runtime services that Tokio provides. These runtime services include the ability to spawn other tasks; the I/O driver, which allows tasks to be notified by the operating system's async I/O APIs, and the timer. Futures which rely on Tokio 0.1's runtime services will panic when they try to access those runtime services (such as by spawning a task or creating a timer) on the Tokio 0.2 runtime, even if they are converted to the std::future::Future trait. This is because the new runtime does not provide these services in a way compatible with Tokio 0.1's APIs.