-
Notifications
You must be signed in to change notification settings - Fork 636
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
Mocking the system clock #5123
Mocking the system clock #5123
Conversation
🤔 I have mixed feelings here. On the positive side, yess, mockable/virtualized time is an essential feature of the system, and we absolutely must have it. That being said, I have a bunch of design-level doubts regarding this specific implementation. In an ideal world, for virtualizing time I would love us to have just a single call to "get current time" at the start of the event loop, and then to pass the current time as a parameter to any function that needs it. That way, we don't even need to mock the time, we just pass it into the system as a parameter. Obviously, in our code base we are calling Things like timer: DoomslugTimer {
started: Instant::now(),
last_endorsement_sent: Instant::now(), look quite suboptimal to me -- logically, this is a single point in time. Specific design issues with the current solution:
|
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.
Please change the title of your PR to a descriptive one. Thanks!
@matklad
|
@matklad |
I like the idea of adding mocking to time. While writing tests I had to add a bit of code to support testing code, which gets executed rarely, having mocks for time would be useful. However:
Everytime you start new actor, yo would make it point to the same Arc<RefCell>>` as it parent.
function:
This would allow us to use single source of time for all code, and also it would be easier to mock. |
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.
@yordanmadzhunkov after a discussion with @matklad we agree that the design in this implementation is not optimal. However, to not get blocked on this for fuzzing, I suggest that you figure out the minimal amount of change we need to make the fuzzing possible (deterministic). There are likely a few places where we can refactor the functions to have time passed in as a parameter so that we can avoid doing a big change like this at once.
@@ -4,6 +4,7 @@ use crate::testbed::RuntimeTestbed; | |||
use indicatif::{ProgressBar, ProgressStyle}; | |||
use near_crypto::{InMemorySigner, KeyType}; | |||
use near_primitives::hash::CryptoHash; | |||
use near_primitives::time::Instant; |
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.
ditto here: basically, all the code in runtime-params-estimator
should be exempt from time mocking. This is essentially benchmarking code, and it has its' own requrenments for time, unrelated to the rest of the code.
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.
Approving to unblock, but there are three fixes we need to do here:
- remove unneded cargo dependency
- remove changes to the estimator
- make sure that
is_mock
can't leak between tests
The thread_local
suggestion is optional -- I do like to minimize the amount of machinery we use, but as this is impl details, this doesnt' really matter.
Two other nice-to-haves:
- are we sure that this change is enough to enable the fuzzing we want? Given that we are not mocking tokio's time or
thread::sleep
s I wouldn't be surprised if this isn't actually enough. - are we sure that this is the minimal change? From my conversation with @bowenwang1996, I recall the idea was to just refactor some specific part of the code-base to take time as an argument, without introducing thread-locals, but also without fixing every usage of time. If that turns out to be infeasible, I think it's OK to do what we are doing here, but, still, let's try to minimize the blast radius of the design which we agree is not the long-term one.
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.
Approve to unblock, but please address Aleksey's comments.
I created my own implementation Clock for purpose of mocking time. Please, consider an alternative: |
core/primitives/src/time.rs
Outdated
}); | ||
} | ||
|
||
pub fn utc(&mut self) -> DateTime<chrono::Utc> { |
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.
How about we change signature to pub fn utc_now() -> DateTime<chrono::Utc>
- self is not used - This will allow us to use
Clock::utc_now()
without having to createClock {}
- changing
utc
toutc_now
should make code more readable. Without prior knowledge we will know that we are getting current time.
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. The current design will work fine to mock time for tests executing on the same thread. Though, it's easy for someone to start using Instant::now()
or Utc::now()
by accident, that's why it may be a good idea to introduce one wrapper.
Though, this is not a blocking issue.
core/primitives/src/time.rs
Outdated
}) | ||
} | ||
|
||
pub fn instant(&mut self) -> Instant { |
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.
How about we change signature to pub fn instant_now() -> DateTime<chrono::Utc>
@yordanmadzhunkov I pushed a few huge changes to Good job on the PR. |
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.
👍
Couple of more nitpicly coments:
- some
pub
can still be removed - Rather than making the user import
MockClockGuard
, the API could look like this:
let _guard: MockClockGuard = Clock::mock();
🤔 thinking more about this, I think it even makes sense to make add_utc
and such methods of the guard object, rather than of the Clock
itself.
Anyway, I don't think we should try to polish Rust API in this PR (we must polish it sometime later though, time mocking is a fundamental fascility used thourgouht the codebase, its importatn that the API is just right, otherwise changing it later would be a pain).
0afbdb2
to
343f155
Compare
I accidentally invoke everybody on the review. Feel free to leave, if you feel there are enough colleagues already invoked. For those who want to participate, I will explain the motivation:
We want to test our code using fuzz tests, because it provides benefits, using like random input
In order to get those benefits, we need repeatable tests with require high level of determinism. Putting the
Utc::now
in blockchain header and computing hash is not deterministic. There are other source of not-deterministic behaviour like threads, random seed which will not be addressed in this review.The goal of this PR is to make it possible to inject values into system clock before invoking the production code. The actual code in production should have no idea that we are mocking the system clock.
Mocking the system clock.