New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
rt: add rng_seed option to runtime::Builder
#4910
Conversation
The `tokio::select!` macro polls branches in a random order. While this is desirable in production, for testing purposes a more deterministic approach can be useul. This change adds an additional parameter to the runtime `Builder` to set the random number generator seed. This value is then used to reset the seed on all threads associated with the runtime being built. This guarantees that calls to the `tokio::select!` macro which are performed in the same order on the same thread will poll branches in the same order.
Thanks for taking this on! I will look shortly, but first I will answer some of the questions.
We probably don't want to do this as it will make the order of rands identical on each thread. What we can do is use the seed for a "seed random generator" and use that to generate a per-thread seed. Pseudo code: let seed_rng = Rng::new(seed_from_builder);
for _ in thread_to_spawn.iter() {
let thread_seed = seed.rng.next_seed();
spawn_thread_with_seed(thread_seed);
}
```
Hopefully, this makes some sense.
> Is there a way to make the thread that a task is run on deterministic?
Not entirely, but this is out of scope. For the use cases in question, they control enough to make sure it is deterministic. In theory, if you use the current_thread scheduler and strickly control I/O and other input, you can make it deterministic. Even better is to mock out I/O. |
Hmm, I don't think we want to expose |
Can you use the |
NOTE: This change doesn't correct the handling of the RngSeed on the thread the runtime is started from, so it's all likely to change. Just pushing for safety. Instead of exposing the width of the seed we're currently using, expose an opaque struct which can generate a seed from a byte slice. Additionally, each thread is given with a unique seed based on its `id` (`worker_thread_index`). This should enable more deterministic behavior, while ensuring that each thread does not begin with the same seed.
In order to properly clean up after ourselves, we set a specific seed into the thread local RNG when entering into a runtime context. The previous seed (RNG state) is stored in the `EnterGuard` together with the previous context (runtime handle). Upon dropping the guard, the previously stored seed is returned to the thread local RNG. To achieve this in a deterministic, but fair way, we now store a seed generator in the runtime handle, and another in the blocking thread spawner. These seed generators are thread safe (as the one in the handle may be passed across thread boundries) and will produce a deterministic series of seeds when the initial seed provided to the seed generator is the same.
OK, that was a fun rabbit hole I ended up running down following the We now have a public For the runtime, we do set the seed when entering the runtime and keep the old seed in the Some questions:
|
Extended the implementation to also seed the random number generator used by workers to pick the initial peer to attempt to steal work from. This change was included in the builder function docs.
@carllerche I think I've covered everything in your comments and added the necessary documentation, so this PR is ready for review again when you've got a moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I skimmed it, and it looks fine to me. I would suggest keeping the new APIs as unstable initially as we let consumers try out the API. To do this, I would just make the public APIs unstable, the implementation can stay as it is.
In order to test out the new API before fixing on it, make it unstable first.
I've made the new API unstable, now just waiting to see whether I've got the right visibility everywhere to satisfy clippy on stable and unstable builds. |
Is this ready to go? It looks like there are a few merge conflicts now. |
Yep, it's ready to go once approved (and now once I fix the merge conflicts). I'll try to merge master in it this afternoon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, a couple of suggestions that you can apply if you want.
Co-authored-by: Carl Lerche <me@carllerche.com>
Co-authored-by: Carl Lerche <me@carllerche.com>
…kio into hds/runtime-builder-rng-seed
Now that the runtime module is present even without the rt feature.
Also fixed a couple of internal comments.
The `tokio::select!` macro polls branches in a random order. While this is desirable in production, for testing purposes a more deterministic approach can be useul. This change adds an additional parameter `rng_seed` to the runtime `Builder` to set the random number generator seed. This value is then used to reset the seed on the current thread when the runtime is entered into (restoring the previous value when the thread leaves the runtime). All threads created explicitly by the runtime also have a seed set as the runtime is built. Each thread is set with a seed from a deterministic sequence. This guarantees that calls to the `tokio::select!` macro which are performed in the same order on the same thread will poll branches in the same order. Additionally, the peer chosen to attempt to steal work from also uses a deterministic sequence if `rng_seed` is set. Both the builder parameter as well as the `RngSeed` struct are marked unstable initially.
The original tests for the `Builder::rng_seed` added in #4910 were a bit fragile. There have already been a couple of instances where internal refactoring caused the tests to fail and need to be modified. While it is expected that internal refactoring may cause the random values to change, this shouldn't cause the tests to break. The tests should be more robust and not be affected by internal refactoring or changes in the Rust compiler version. The tests are changed to perform the same operation in 2 runtimes created with the same seed, the expectation is that the values that result from each runtime are the same.
The original tests for the `Builder::rng_seed` added in #4910 were a bit fragile. There have already been a couple of instances where internal refactoring caused the tests to fail and need to be modified. While it is expected that internal refactoring may cause the random values to change, this shouldn't cause the tests to break. The tests should be more robust and not be affected by internal refactoring or changes in the Rust compiler version. The tests are changed to perform the same operation in 2 runtimes created with the same seed, the expectation is that the values that result from each runtime are the same.
Motivation
In certain circumstances (e.g. running tests with loom), it may be desirable
to have deterministic behavior when running tokio.
Partly this may be achieved by seeding the random number generator used
by the
tokio::select!
macro.This PR is more a proof of concept that a real proposed change, it needs work
before it could be merged.
Refs: #4879
Open Questions
There are a number of open questions (or rather things that I need help with):
when using a multi-threaded runtime, the thread that a task will be scheduled
on is not deterministic. Is this acceptable?
This makes the values deterministic, but not the same.
u64
as a seed. This is not very flexible, but avoidsthe need to hash or otherwise reduce some other value to a
u64
to seed theactual RNG. This was done deliberately, because it's the easiest thing to change
in this PR before merging. Opinions?
RngSeed
type which can be constructedfrom a byte slice.
feels ugly to me, but I don't see a better way without sacrificing performance by,
e.g. making the RNG global. Suggestions?
Handle::enter
pattern is used to set the seed when a runtime is enteredfrom a thread, when the thread leaves the runtime, the previous thread local value is
restored.
Solution
The
tokio::select!
macro polls branches in a random order. While thisis desirable in production, for testing purposes a more deterministic
approach can be useul.
This change adds an additional parameter to the runtime
Builder
to setthe random number generator seed. This value is then used to reset the
seed on the current thread when the runtime is entered into (restoring the
previous value when the thread leaves the runtime). All threads created
explicitly by the runtime also have a seed set as the runtime is built. Each
thread is set with a seed from a deterministic sequence.
This guarantees that calls to the
tokio::select!
macro which areperformed in the same order on the same thread will poll branches in the
same order.
Both the builder parameter as well as the
RngSeed
struct are markedunstable initially.