Why is tokio loading up on implicit global state while futures is getting rid of it?

futures 0.2 got rid of the implicit task context for a number of reasons that are well-described in the RFC[0]. So why is tokio adding the equivalent implicit state with things like tokio::spawn and Timer[1]? IMO everything in the futures RFC applies just as much to tokio.

It feels weird that two crates that are relatively coupled are pulling in opposite directions on fundamental API design like this.

[0] https://github.com/rust-lang-nursery/futures-rfcs/blob/master/task-context.md
[1] And similar, see "global state" at https://tokio.rs/docs/going-deeper/building-runtime/

1 Like

These are two very different pairs of shoes. Task context is not quite global, it's semi-global per task. This means there needs to be maintained by every executor and would be a mandatory feature.

Tokio defaults to global state because managing to have a whole application running on a number of cores mapped to threads is a surprisingly difficult problem. It means passing around a runtime handle to quite a number of components and also threading handles through the whole application.

3 Likes

Something else, too. It sounds like a surprising contradiction, but it's not a far comparison, because the two crates are being considered from a different perspective in your subject.

Futures is adding explicit state under the hood that's relevant to those implementing their own futures at a low level directly on top of a poll and wakeup mechanism (there are plenty of ways to implement higher-level futures that derive from lower-level ones). The change is largely invisible to "end users" who will manipulate futures using the combinators and IntoFuture trait.

Tokio is adding (optional) implicit runtime state for the convenience of end users, so that they don't (in many case) have to deal with the extra boilerplate of creating an executor and context and passing it around all the time. It's still there under the hood if you need to do something clever.

So, from a different perspective, it's possible to see these recent changes as the two libraries becoming more consistent, rather than heading in opposite directions.

1 Like

Thanks for the replies. I think the comments about user convenience sound good but they fall down once you start thinking about what this means for libraries. Some more thoughts from trying to port my little tokio-core-based library[0] to tokio follow:

Implicit state is not actually optional, because using explicit state requires your entire call chain to use explicit state

There is no way to usefully retrieve any of the three[1] implicit state components. (No, DefaultExecutor and Handle::current don't count, see below). This means it's not possible to transition from the implicit API to the explicit one somewhere along a call chain. So, if I as a library author choose to require explicit handles, everything that consumes my library must use explicit handles as well.

And tokio::run is entirely implicit

Combined with the above, this means that if I want my library to be used from tokio::run (which is now the recommended way to use tokio) I must use the implicit state.

An API that did handle both the implicit and explicit states would be awful

Ok, that's all fixable, we can add getters for the explicit state, or change run to

fn tokio::run<C, F>(C)
    where C: FnOnce(reactor::Handle, executor::Executor, timer::Handle) -> F
          F: Future<Item = (), Error = ()> + Send + 'static

and now you have your explicit state. What's the problem?

An API that handles both cases would look something like

fn do_server(reactor: Option<&reactor::Handle>,
             executor: Option<&executor::Executor>,
             timer: Option<&timer::Handle>)
    -> impl Future<Item = (), Error = ()> + Send + 'static
{
    let std_tcp_listener = configuration();

    let handle = match reactor {
        Some(handle) => handle.clone(),
        None => {
            // Gee I hope this handle works.
            reactor::Handle::current()
        }
    };

    let tcp_listener = TcpListener::from_std(std_tcp_listener, &handle);

    let executor = executor.cloned();
    let timer = timer.cloned();

    let server = tcp_listener.incoming().for_each(|tcp| {
        let database_query_to_run = magic(tcp);
        let time_limit = Instant::now() + TIMEOUT_DURATION;
        let timeout = if let Some(timer) {
            timer.delay(time_limit)
        } else {
            // Gee I sure hope there'll be a timer running when we need
            // it or this database query might go on forever.
            Delay::new(time_limit)
        };
        let f = database_query_to_run.select2(timeout)
            .then(|r| {
                match r {
                    // ...
                }
            });
        if let Some(executor) = executor {
            executor.spawn(Box::new(f));
        } else {
            // Gee I hope there's an executor set or we're going to panic.
            tokio::spawn(f);
        }

        Ok(())
    });

    server
}

This is the worst of both worlds. I have even more API surface than the explicit case would require (because everything is optional). And internally I have to handle all three cases: where the handle is passed explicitly, where it's not passed explicitly and is present, and where it's not passed explicitly and is not present.

There's also the incredibly fun question of what should happen if both the explicit and implicit states are present and they're different.

Relying on implicit state is very difficult to reason about

This was IMO the most compelling argument from the futures task::Context RFC and it is even more compelling here. Since there are now three different tokio components of varying optionality, how do you tell which code requires which component? If I have a library that requires timers to function properly, how does a consumer know that? Do I have to test every entrypoint to my library on a runtime without timers and make sure it returns the right error?

And why would we check all this when the compiler could just do it for us?

Passing around explicit arguments is not that bad

It's really not. And with some mildly clever trait magic there only has to be one argument. e.g.

trait HasReactor {
  fn reactor(&self) -> &reactor::Handle;
}

trait HasExecutor {
  fn executor(&self) -> &executor::Executor;
}

trait HasTimer {
  fn timer(&self) -> &timer::Handle;
}

impl HasReactor for (reactor::Handle, executor::Executor) {
  ...
}
impl HasExecutor for (reactor::Handle, executor::Executor) {
  ...
}

fn do_io<H>(handles: H, arguments ...) -> impl Future<...>
  where H: HasReactor + HasExecutor
{
  ...
}

fn do_io_with_timeout<H>(handles: H, arguments ...) -> impl Future<...>
  where H: HasReactor + HasExecutor + HasTimer
{
  ...
}

fn main() {
  let arguments = ...;
  let handles = (myReactor, myExecutor);
  do_io(handles, arguments); // Compiles
  do_io_with_timeout(handles, arguments); // Fails to compile.
}

This is also totally extensible, so when somebody writes tokio-rocket-launcher you can do

fn go_to_space<H>(handles: H, rocket: Rocket)
  where H: HasExecutor + HasTimer + HasRocketLauncher
{
  let launch_moment = Instant::now() + LAUNCH_COUNTDOWN_DELAY;
  let launcher = handles.launcher().clone();
  let countdown = handles.timer().delay(launch_moment)
    .and_then(move |_| {
      launcher.launch(rocket)
    })
  handles.executor().spawn(countdown);
}

and be guaranteed that the rocket-launcher component is running, rather than creating this ecosystem-wide assumption that things will be passed in TLS.

What's wrong with DefaultExecutor and Handle::current?

DefaultExecutor is not "the current value of the global Executor", it's "a pointer to the current value of the global Executor". It does not get you a permanent reference to whatever is in the TLS slot, and it is basically a way to call tokio::spawn and get an Error instead of a panic.

Both Handle::currents always returns a Handle. But if there's no current reactor/timer it just gives you a Handle that won't actually work, and there's no way to tell this apart from a working Handle except trying to use it.

[0] https://github.com/khuey/bb8

[1] The tokio::reactor::Reactor, the tokio::executor::Executor implementation, and the tokio-timer::timer::Timer, all of which were previously accessible in spirit through tokio-core::reactor::Handle.

1 Like