Re-allow zero blocking threads with warnings, panic in blocking spawner #3367
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Motivation
I'm of the development school favoring a fixed, small number of threads per process. Its the raison d'être for the additional effort of asynchronous code. Such applications are more predictable in terms of memory usage and performance. With tokio 0.2-0.3 I was able to configure a fixed number of "core" worker threads and, with some careful other choices, know that no (zero) "extra" threads were needed; in tokio terms, neither
spawn_blocking
norblock_in_place
were in use.Note that the existing blocking facilities are really just workarounds for OS gaps which we should expect to become less favored as alternatives become available, e.g. rust-native fully asynchronous DNS resolvers and Linux
io_uring
.As of tokio-1.0.0, #3287 disallowed configuring zero (0) "extra" threads (
max_blocking_threads
) on tokio's threaded runtime pool. Previously, if one configured zero extra threads, testing showed that any application tasks needing extra blocking threads would simply fail to complete. That was too subtle a failure, but on the other hand, #3287 may be too prescriptive as it stands, not matching other parts of tokio, which allows selecting only the features needed for the application.With #3287 as released, configuring just one "extra" thread (
max_blocking_threads(1)
) may still be a silent and hard to diagnose performance liability, for example serializing DNS lookups with file reads/writes. Runtime (as opposed to config time) panic on calls tospawn_blocking
may be more educational to the end user than simply forcing them to use a small positive number, which may still be inadequate for good performance.Solution
Introduce
spawn_core
to separate the call path for "core" worker thread launching.Make the pool aware of "core" and "extra" thread capacity. If there is zero "extra" capacity, panic with an informative message when
spawn_blocking
orblock_in_place
is called.Improve rustdoc in general for
max_blocking_threads
This logically preserves the "fix" for #2802 in that instead of never completing, tokio will panic with an informative runtime error message (and stack trace) as to the mis-configuration:
Alternatives
Configurable workaround
With tokio 1.0.0-1.0.1, we can effectively obtain the same panics and backtrace on the first use of a blocking "extra" thread:
This isn't terribly bad as a workaround, but consider that many users whom might have initially configured zero (0) blocking threads, thinking they don't need it, might next choose one (1) blocking thread, and not be aware of the significant performance implications to effectively serializing all blocking operations, e.g. DNS lookups and file operations, on one thread.
Feature gates
We could place
spawn_blocking
andblock_in_place
functions under spawn-blocking and block-in-place feature gates, respectively. (Previously there was theblocking
feature gate.) These would be included by full. Also tokio features like fs could depend on spawn-blocking to ensure continued compatibility.A start of a prototype for this is here:
master...dekellum:opt-in-blocking-features
An advantage for this alternative is that libraries (not using
full
) will need to explicitly opt-in to the features or will fail to compile (rather then deferring to a runtime panic).However, this alternative will frequently not be sufficient without associated down stream changes, and these may be an up hill battle for users. For example,
hyper
currently includes aclient::connect::dns::GaiResolver
which uses spawn_blocking. However, currently the only way to disable this resolver is to disable hyper's tcp feature, which disables too much: the entireHttpConnector
and all its tokio integration.Add
runtime::Builder::enable_blocking(false)
This adds an additional safety/hurdle to configuring no capacity for blocking, but would result in the same runtime panics as the preferred solution above. Here the user would additionally need to add the following to
runtime::Builder
configuration:Note that as a user, I would still want to explicitly set
max_blocking_threads(0)
.Prototype for this is as follows, with explicit warnings in rustdoc:
master...dekellum:allow-disable-blocking
IMO this alternative amounts to an unnecessary additional hurdle, but I'm willing to add this to the preferred solution of this PR, if it remains desired.
Or
disable_blocking
Another alternative interface is to add
disable_blocking
for the same purpose. In terms of consistency of the builder interface, there is alreadyenable_*
methods (e.g.enable_time
) so adisable_*
method, without a parameter, is more consistent in some sense.