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::current
s 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
.