Problem/Motivation
Quoting @lendude from #12
If you get into a state where lastWriteTimestamp is equal to the create timestamp in the entry, the ChainedFastBackend no longer knows what the sequence of events was, so it should default to the consistent cache, not the fast cache entry as it currently does.
In #2907728-37: Installer: Convert system functional tests to phpunit we are faced with random fail. Looks like that occurs due to incorrect works of ChainedFastBackend::markAsOutdated(). This is the code:
/**
* Marks the fast cache bin as outdated because of a write.
*/
protected function markAsOutdated() {
// Clocks on a single server can drift. Multiple servers may have slightly
// differing opinions about the current time. Given that, do not assume
// 'now' on this server is always later than our stored timestamp.
// Also add 1 millisecond, to ensure that caches written earlier in the same
// millisecond are invalidated. It is possible that caches will be later in
// the same millisecond and are then incorrectly invalidated, but that only
// costs one additional roundtrip to the persistent cache.
$now = round(microtime(TRUE) + .001, 3);
if ($now > $this->getLastWriteTimestamp()) {
$this->lastWriteTimestamp = $now;
$this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp);
}
}
Here are some examples of last_write_timestamp_BIN_NAME values, when random fail appears.
d:1509618497.0820000171661376953125;
d:1509618497.131000041961669921875;
Modeling a possible situation:
- server1 wrote
last_write_timestamp_BIN_NAMEin 1500000000.08200001 - After 0.00001 seconds server2 call markAsOutdated()
- If the server2 is behind by 0.00056789 seconds from server1, we get the following:
$now = round(microtime(TRUE) + .001, 3);
microtime(TRUE)= 1500000000.08200001 - 0.00056789 + 0.00001= 1500000000.08144212
microtime(TRUE) + .001 = 1500000000.08244212
round(microtime(TRUE) + .001, 3) = 1500000000.082As result:
(server2) 1500000000.082 < 1500000000.08200001 (server1)
Despite the fact, that the code on the server2 was executed later.
Proposed resolution
Reject cache items from the fast backend when the created timestamp is exactly the same as the last_write_timestamp, this should almost never happen in production due to microsecond precision but it is not impossible - as demonstrated by the test coverage.
User interface changes
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #23 | x1-2920590-23-test-0.000001.patch | 2.75 KB | Anonymous (not verified) |
| #23 | x1-2920590-23-test-0.00001.patch | 2.75 KB | Anonymous (not verified) |
| #23 | x1-2920590-23-test-0.0001.patch | 2.75 KB | Anonymous (not verified) |
| #23 | x1-2920590-23-test-0.001.patch | 2.75 KB | Anonymous (not verified) |
| #22 | x1-2920590-22-passes.patch | 2.75 KB | Anonymous (not verified) |
Issue fork drupal-2920590
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 2920590-last-write
changes, plain diff MR !11643
Comments
Comment #1
Anonymous (not verified) commentedvaplas created an issue. See original summary.
Comment #2
Anonymous (not verified) commentedRaw patch from #2907728: Installer: Convert system functional tests to phpunit just for demonstration
Comment #3
Anonymous (not verified) commentedComment #5
Anonymous (not verified) commentedAnd here is the pure patch.
Comment #6
Anonymous (not verified) commentedComment #7
Anonymous (not verified) commentedI just found out that
round(4.444499999, 3);is4.444(I thought earlier that4.445, my bad). So you can even increase the numbers in the examples, but it's not important :)Refreshed title, because:
It will be vexing if I sit down with this issue in a puddle, but I'm ready for it. Therefore, feel free to express criticism to clarify the situation.
Comment #8
lendudeThis is some really impressive debugging. Nice work as always.
I think the fail/pass with the installer test is quite convincing, but.....is there any way we can write a less elaborate test for this? And something a little (ok, a lot) more explicit?
And I'm also wondering why this doesn't occur in WebTestBase install tests, are they just slower? Or are we doing something funky in BrowserTestBase?
Having an explicit test might help answer those questions too.
Comment #9
Anonymous (not verified) commented@Lendude thanks for the support and great questions!
I completely agree with the fact, that while everything is rather turbid, and this needs to be clarified. If I have a clean test, I will definitely share it :)
I just ran a series of tests with a specially downed markAsOutdated() calculation 0s, 0.002s, 0.01s, 0.1s, 1s, 10s and NULL (marks only if we haven't any values). See from #2920081-58: Ignore: patch testing issue .
I watched the tests burn in the fire and cried. But what's interesting, among all the chaos, the WTB:Installer tests passed as if nothing had happened!
Only 3 tests failed (with 10 and NULL variants), and only after the installation was completed, due to other reasons. See https://www.drupal.org/pift-ci-job/802608 (Installer.Installer section).
So, perhaps during installation WTB just ignores or permanently resets the bins.
Comment #10
Anonymous (not verified) commentedWow! @Lendude/@mixologic made a sharp turn of the plot!
Repost @Lendude's comment:
And the proposal instead replaced one conditional in ChainedFastBackend::getMultiple():
Comment #12
lendudeOk so if you set things up like this, you have a failing test.
If you get into a state where lastWriteTimestamp is equal to the create timestamp in the entry, the
ChainedFastBackendno longer knows what the sequence of events was, so it should default to the consistent cache, not the fast cache entry as it currently does.But.....it's all well and good in a test scenario, but I don't actually see how you can end up in such a state in a real environment.
Comment #15
lendude#12 is failing randomly.... even locally it turns out. That doesn't make sense to me, but that's what happening.
Here is a version of the patch that I can consistently get to fail randomly (interesting sentence :-))
Running this locally gets me a fail rate of 25% to 50% fails.
Comment #18
Anonymous (not verified) commentedSteep hit as always!
Yep!)) Me too, failed/all: 12/20, 9/20, 9/20, 10/20, 11/20.
Comment #20
berdir\Drupal\Core\Cache\ChainedFastBackendFactory::__construct():
So yes, we do not use the chained backend during the installer.
Comment #21
Anonymous (not verified) commented#20: Thanks for the tip, @Berdir! Perhaps this is the wrong moment, because of which our converted tests work slower than WTB versiion (#2907728-76: Installer: Convert system functional tests to phpunit)?
However, #12 and #15 patches fall not on installation phase.
And if we add a bit of time, it works stably.
Here are the patches for testings, without fix from #12.
Comment #22
Anonymous (not verified) commented#21 with run-tests.sh tweak for a quick check.
Comment #23
Anonymous (not verified) commentedComment #33
catchIt looks like there's a couple of approaches here, using <= seems like a good plan though. Tagging for issue summary update.
Comment #35
mstrelan commentedIs this still relevant since #3498193: Optimize last_write_timestamp writes in ChainedFastBackend?
Comment #36
catchI converted the test-only patch to an MR, (edit: unsuccessfully...).
The
<check is still in core and I don't think it would hurt at all to change that to<=#3498193: Optimize last_write_timestamp writes in ChainedFastBackend means that when there is one cache write on a page, the two servers would have to be more out of sync for there to be a problem, but it's only 50ms which is within NTP drift expectations, so it only makes the problem less likely and doesn't eliminate it. I think therre might be an issue around to try to use something else (like a special cache tag) that doesn't rely on clock time. The random failure discussed here appears to be something that isn't actually due to NTP clock sync at all but more rounding errors, with the 50ms ahead that's almost certainly solved now because it's further ahead, but still think changing to <= is more correct.
Comment #38
catchThe test only job didn't fail, but reverting the fix causes the new test to fail - here's the failing job:
https://git.drupalcode.org/project/drupal/-/jobs/4790389
Comment #39
borisson_There are more tests that are failing on the last test run. All in ChainedFastBackend.
The change however makes sense and it has good test coverage.
Comment #40
catchhmm the new test that fails is the one that's being added so something is still wrong here.
Comment #41
catchForgot about this issue but I think that #3526080: Reduce write contention to the fast and consistent backend in ChainedFastBackend makes this outdated now.
Comment #43
catchGoing to close as outdated since #3526080: Reduce write contention to the fast and consistent backend in ChainedFastBackend landed.