Problem/Motivation
TestTime->getRequestTime() can produce inconsistent results due to rounding in the calculation.
Steps to reproduce
We did the following in a kernel test:
public function testTimeRoundingError(): void {
\Drupal::time()->setTime('+100 seconds');
$timestamp = \Drupal::time()->getRequestTime();
for ($i = 1; $i <= 10; ++$i) {
time_nanosleep(0, 100000000);
$this->assertSame(0, $timestamp - \Drupal::time()->getRequestTime(), "after 0.$i seconds");
}
}
This fails for us with a random-ish value in the message, e.g. "after 0.3 seconds" then "after 0.7 seconds" etc.
I looked into the calculation, and it seems way too smart.
Proposed resolution
Add the test.
Fix the calculation.
Remaining tasks
User interface changes
API changes
Data model changes
Comments
Comment #2
donquixote commentedSome equation juggling which may or may not be helpful.
Note that `(int)` behaves like floor() for positive numbers and like ceil() for negative numbers, which makes it harder to reason about.
But the bug still happens if we replace (int) with floor().
Here we split float variables into varname_int + varname_fraction, so that we can move the integer part out of the floor() call.
This would not be possible with just (int) due to the asymmetry mentioned above.
The problem would go away if TIME_STARTED_FRACTION is zero.
Comment #3
ptmkenny commentedThanks for reporting this. As a maintainer, I'm happy to review an MR that contains a fix for this and a test, but I don't have the time to work on the code myself.
I also didn't start maintaining this module until a few years after that code was committed, so I'm not able to explain why it is written the way it is.
Comment #4
ptmkenny commentedDuplicate, please ignore.
Comment #7
jonathanshawThe problem is worse than one might realise from the IS. Because of the rounding fluctuation, the request time can go jump backwards as well as forwards. This can cause very obscure intermittent caching bugs, the kind that eat days of developer time.
I'm not completely sure the fix in the MR is the best solution - maybe deeper insight could see a better solution - but it works in practice and has a test so I suggest we commit it for now.
Comment #8
ptmkenny commentedThanks! I added an additional part to the test to make it clear that the caching persists even when resetTime() is called. (I think it makes sense to keep the cache even if resetTime() is called, but we may decide to change this later; the test makes it explicit.)
Comment #10
ptmkenny commentedComment #12
jonathanshawThanks @ptmkenny.
I agree it's probably safer to keep the cache, it prevents any risk of rounding errors creeping in and making the time go backwards if time gets frozen/unfrozen repeatedly in quick succession.