You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 30, 2019. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 30, 2019. It is now read-only.
As currently proposed, futures-core 0.3 will not build in task-local data. The idea is to instead address needs here through external libraries via scoped thread-local storage.
For any robust task-local data, however, we want the ability to hook into the task spawning process, so that data inheritance schemes can be set up.
If #56 proceeds and spawning is removed from task::Context, I expect the value of having a good story around task-local data to increase. In general, I don't really want my executor-generic framework code to stash a Box<Executor> in all the places that need spawning.
I don't really want to stash a Box<Executor> in all the places that need spawning.
I think most places that need spawning are application code, which can just reference an appropriate executor via globals or handles or whatever it likes directly, as done today in futures 0.1.
For task-local storage, what is needed is to run code before each top-level future poll (and potentially, if using scoped-tls, code after too).
One solution is to add a hook that allows wrapping every future that comes in the executor into some wrapper future type: this way the wrapper future would be able to execute code around the wrapped future.
Actually, this could even be done by consuming the executor and returning a wrapping executor that first wraps the future with the task-local-wrapper, and then forwards the wrapped future to the wrapped executor.
However, I'm not sure this “consuming the executor and returning it” would actually work: what I actually want is tokio::spawn to use the wrapping executor -- which, AFAIU, wouldn't happen with the consume-and-wrap design.
I messed around with @mitsuhiko's execution-context crate yesterday. It's futures-agnostic and works via TLS, and I made it work with futures exactly as @Ekleog said: a wrapper future:
/// Returns a future that executes within the scope of the current [ExecutionContext].pubfncontext_propagating<F:Future>(future:F) -> implFuture<Output = F::Output>{ContextFuture{
future,context:ExecutionContext::capture(),}}/// A future that executes within a specific [ExecutionContext].structContextFuture<F>{future:F,context:ExecutionContext,}impl<F>FutureforContextFuture<F>whereF:Future,{typeOutput = F::Output;fnpoll(self:PinMut<Self>,cx:&mut task::Context) -> Poll<F::Output>{let me = unsafe{PinMut::get_mut_unchecked(self)};let future = unsafe{PinMut::new_unchecked(&mut me.future)};
me.context.run(|| future.poll(cx))}}
This works quite well! Like @Ekleog, I definitely see the value in hooking into the spawn mechanism, because wrapping every future in context_propagating before spawning is error-prone. Empirically, I've seen context propagation issues in production servers more times than I can count. Such a hook would have to be opt-in, because not every application needs or can use task-local data via TLS.
A portable mechanism for wrapping all top-level poll calls might have broader applications as well: for example, task-level profiling, or just logging warnings when a poll call blocks for unreasonably long, would call for something similar.
I guess the first design decision is: do we want to add hooks to modify top-level poll calls into an existing Executor, or do we want to wrap Executors into other, different-top-level-poll-call-behaviour Executors?
I lean for the first option, because it'd work nicely with eg. tokio::spawn, while wrapping Executors into other Executors would mean the top-level executor would need to be changed.
@tikue@Ekleog A question regarding these executor-level hooks: it seems like if you ever use a library that creates its own executor internally, you would have no way to instrument it with hooks. And of course you could forget to do so on your own executor. Both of which would lead to propagation failures.
Or were you thinking of providing a global hook mechanism of some kind?
Actually, the more I think about this, would it be for task_local or for handling time (timeout shouldn't require forcing the executor, it's generic enough), the more I think that futures should look like this (written in the github comment box, please forgive shallowness of reflexion):
traitTaskLocalExecutor:Executorwhere <SelfasExecutor>::Context:TaskLocalContext,{}traitTaskLocalContext{fnget_task_local(ctx:&mut <SelfasExecutor>::Context) -> &mutTaskLocalMap;}// some magic using TaskLocalMap to make it available as a task-local through some macro
And
traitTimeoutExecutor:Executorwhere <SelfasExecutor>::Context:TimeoutContext,{typeTimeoutFuture:Future<Output = ()>;fntimeout_future(ctx:&mut <SelfasExecutor>::Context,d:Duration) -> TimeoutFuture;}// Some macro to make it easily usable from async/await
and [etc.] (actually, even Waker could be moved to specific executors, not all futures require that the executor is able to wake them and some executors may not support waking but just run each future in a round-robin fashion… though that'd maybe be over-doing it)
Assuming that async/await is able to infer the bounds to set to its Executor depending on its contents, this sounds like a minor inconvenience when writing futures manually (a bit more boilerplate) for much more flexibility and portability (task locals, timeouts, and likely other things that will be implemented by lots of executors and that we've not yet thought about)
I guess this has already been discussed somewhere… could someone point me to where, so I understand the discussion around it?
Oh, forgot to mention: it would also allow experimenting with cross-executor task_locals / timeouts outside of std, given that std would only need to provide the Executor trait, and libraries could define TaskLocalExecutor, TimeoutExecutor, etc.
@Ekleogrust-lang/futures-rs#1196 has some similar prior discussion. The place where I've always thought this would become far too tedious is in propagating the bounds everywhere it's needed (i.e. when you have a function generic over F: Future you now need to have E: Executor, F: Future<E> and f: impl Future<Output = Foo> becomes f: impl Future<impl Executor, Output = Foo>). Maybe that just won't be a common enough issue to really matter.
It's worth noting that tokio constructs its executor(s) lazily, so there is adequate opportunity to hook into them during startup without forcing ugly global state into standard APIs.
Activity
tikue commentedon Sep 20, 2018
If #56 proceeds and spawning is removed from task::Context, I expect the value of having a good story around task-local data to increase. In general, I don't really want my executor-generic framework code to stash a
Box<Executor>in all the places that need spawning.Things task-local data would be helpful for:
Ralith commentedon Sep 20, 2018
I think most places that need spawning are application code, which can just reference an appropriate executor via globals or handles or whatever it likes directly, as done today in futures 0.1.
tikue commentedon Sep 20, 2018
Yes, that's certainly true. I've updated my comment to clarify I'm talking about executor-generic framework code.
tikue commentedon Sep 20, 2018
Prior related discussion: rust-lang/futures-rs#937
Ekleog commentedon Sep 20, 2018
For task-local storage, what is needed is to run code before each top-level future poll (and potentially, if using scoped-tls, code after too).
One solution is to add a hook that allows wrapping every future that comes in the executor into some wrapper future type: this way the wrapper future would be able to execute code around the wrapped future.
Actually, this could even be done by consuming the executor and returning a wrapping executor that first wraps the future with the task-local-wrapper, and then forwards the wrapped future to the wrapped executor.
However, I'm not sure this “consuming the executor and returning it” would actually work: what I actually want is
tokio::spawnto use the wrapping executor -- which, AFAIU, wouldn't happen with the consume-and-wrap design.tikue commentedon Sep 20, 2018
I messed around with @mitsuhiko's execution-context crate yesterday. It's futures-agnostic and works via TLS, and I made it work with futures exactly as @Ekleog said: a wrapper future:
This works quite well! Like @Ekleog, I definitely see the value in hooking into the spawn mechanism, because wrapping every future in
context_propagatingbefore spawning is error-prone. Empirically, I've seen context propagation issues in production servers more times than I can count. Such a hook would have to be opt-in, because not every application needs or can use task-local data via TLS.Ralith commentedon Sep 20, 2018
A portable mechanism for wrapping all top-level
pollcalls might have broader applications as well: for example, task-level profiling, or just logging warnings when a poll call blocks for unreasonably long, would call for something similar.Ekleog commentedon Sep 21, 2018
I guess the first design decision is: do we want to add hooks to modify top-level
pollcalls into an existingExecutor, or do we want to wrapExecutors into other, different-top-level-poll-call-behaviourExecutors?I lean for the first option, because it'd work nicely with eg.
tokio::spawn, while wrappingExecutors into otherExecutors would mean the top-level executor would need to be changed.aturon commentedon Nov 15, 2018
@tikue @Ekleog A question regarding these executor-level hooks: it seems like if you ever use a library that creates its own executor internally, you would have no way to instrument it with hooks. And of course you could forget to do so on your own executor. Both of which would lead to propagation failures.
Or were you thinking of providing a global hook mechanism of some kind?
tikue commentedon Nov 15, 2018
@aturon I hadn't considered the multi-executor scenario at all. Do you have any thoughts on a global hook mechanism?
Ekleog commentedon Nov 15, 2018
Actually, the more I think about this, would it be for
task_localor for handling time (timeoutshouldn't require forcing the executor, it's generic enough), the more I think that futures should look like this (written in the github comment box, please forgive shallowness of reflexion):This could be expanded with:
And
and [etc.] (actually, even
Wakercould be moved to specific executors, not all futures require that the executor is able to wake them and some executors may not support waking but just run each future in a round-robin fashion… though that'd maybe be over-doing it)A
Futurewould then be written like:And a future combinator function would look like:
Assuming that
async/awaitis able to infer the bounds to set to itsExecutordepending on its contents, this sounds like a minor inconvenience when writing futures manually (a bit more boilerplate) for much more flexibility and portability (task locals, timeouts, and likely other things that will be implemented by lots of executors and that we've not yet thought about)I guess this has already been discussed somewhere… could someone point me to where, so I understand the discussion around it?
Ekleog commentedon Nov 15, 2018
Oh, forgot to mention: it would also allow experimenting with cross-executor task_locals / timeouts outside of
std, given thatstdwould only need to provide theExecutortrait, and libraries could defineTaskLocalExecutor,TimeoutExecutor, etc.Nemo157 commentedon Nov 15, 2018
@Ekleog rust-lang/futures-rs#1196 has some similar prior discussion. The place where I've always thought this would become far too tedious is in propagating the bounds everywhere it's needed (i.e. when you have a function generic over
F: Futureyou now need to haveE: Executor, F: Future<E>andf: impl Future<Output = Foo>becomesf: impl Future<impl Executor, Output = Foo>). Maybe that just won't be a common enough issue to really matter.Ekleog commentedon Nov 15, 2018
@Nemo157 Thank you for the pointer! I'll continue discussion there :)
Ralith commentedon Nov 16, 2018
It's worth noting that tokio constructs its executor(s) lazily, so there is adequate opportunity to hook into them during startup without forcing ugly global state into standard APIs.