Problem/Motivation

#3438424: [random test failures] Race condition in state when individual keys are set with an empty cache attempted to fix a race condition with the State usage of CacheCollector by setting a cache item immediately after setting a new state value, so that requests that started with an empty cache wouldn't write to cache at the end at the end of the request, potentially writing back a cache item for the old state value before it was set.

This worked most of the time, but not in the case that the request that was attempting to set the new value failed to acquire the cache write lock in ::updateCache() (e.g. when another request was just writing to cache).

For this case, we want to write to the cache even if the lock can't be acquired, since our new value takes precedence over whatever's being written in the other request.

Steps to reproduce

Proposed resolution

Directly attempt to acquire the lock in State::set(), and write the cache whether or not the lock is acquired.

Additionally, run the cache invaidation logic in CacheCollector::updateCache() even if the lock isn't acquired, only cache sets are constrained by acquiring the lock now.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3496257

Command icon 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:

Comments

catch created an issue. See original summary.

catch’s picture

Status: Active » Needs review

catch’s picture

Issue summary: View changes

luke.leber’s picture

Whenever I see a call to wait on a lock I get a chill up my spine.

From a cursory glance, this change could result on a ton of processes spinning/waiting to acquire a lock under a heavy load, couldn't it?

I think this one should be subject to some load testing on a larger site.

catch’s picture

Whenever I see a call to wait on a lock I get a chill up my spine.

I agree with this but I think we might not need the lock wait here, just to more aggressively clear/invalidate/set the cache even when we can't acquire the lock. Pushed a commit that tries this.

catch’s picture

Title: Race conditions/bad lock logic in CacheCollector/State » Race conditions in CacheCollector/State (again)
Issue summary: View changes

The lock name was a red herring but I discovered some real race conditions.

https://git.drupalcode.org/project/drupal/-/jobs/3827879 runs a frequently failing tests that depends on writing to state 2500 times and is green with the MR.

https://git.drupalcode.org/project/drupal/-/pipelines/380036 is a full test run with only nightwatch failures, which this won't help with.

catch’s picture

Issue summary: View changes
godotislate’s picture

Status: Needs review » Reviewed & tested by the community

lgtm

dww’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Bug Smash Initiative

Thanks for this! Great work tracking this down.

I added some suggestions to the MR, mostly for trivial nits and a typo. Hate to slow down progress on a critical, but I really think we need comments about why we're not releasing the locks we're acquiring. I don't think this is RTBC if all the MR threads aren't resolved.

catch’s picture

I'm going to spend a bit more time trying to figure out if we can do away with holding the lock, if we can't, it should result in a better comment, possibly add a @todo and issue to have another try in a follow-up, if we can release the lock, then great we don't need to explain why we can't.

catch’s picture

Status: Needs work » Needs review
Issue tags: -Bug Smash Initiative

Alright it looks like that theory was correct.

The remaining race condition was something like as follows:

We lock cache writes, but not cache reads. CacheCollector uses the cache created timestamp (which is stored with millisecond precision) to detect if the cache item it read at the beginning of the request is the same as the one it reads again at the end of the request.

Because we only lock writes, it's possible for two processes to lock acquire, cache write, lock release within the same millisecond. For the usual CacheCollector case this is a non-issue, because we are generally caching things which are expensive to build and don't get written to as such (like the runtime theme registry), so all these things happening in a single millisecond does not really come up. It's a problem here because state is cheap to build and also gets individual keys written to, especially with the database on a ramdisk on the same server etc. like gitlab pipelines.

If State::set() writes a cache item in the same millisecond as another cache item is set, this breaks the race condition detection in ::updateCache(). Another request can read the newly outdated cache item, get to the end of the request, compare with the State::set() one, see them as 'the same' and then write back stale data.

Holding the lock longer was preventing the situation where another process writes back to the cache item immediately after State::set() has written to it, having read a different cache item created in the same millisecond as the call to State::set().

Instead of that, we can usleep() for 10 milliseconds before and after writing, and try a bit harder to acquire the lock, this still results in 2500 passes for the test, but it doesn't involve either the full lock->wait() call or holding the lock that were necessary in earlier iterations.

There are still other theoretical race conditions here - e.g. if two servers have a time offset, and write cache items in the 'same' millisecond even though they are actually different due to the offset. We could potentially protect against this in CacheCollector by hashing the cache data every time it's read, and then comparing the hash when we write back to the cache - but this would introduce hashing overhead to every cache collector read which is not great. Or maybe we could store the hash with the cache item and compare that, which would mean changing the cache data structure a bit, but that might work and allow us to remove the usleep() calls then.

Going to try that approach in the sandbox MR and see if it's straightforward enough.

spokje’s picture

Status: Needs review » Needs work

*cough*
Testbot really doesn't like it...
*cough*

catch’s picture

OK trying to use a hash of the data doesn't work, or at least not yet, it also requires changing the structure of the cache item which means that even if did work it would probably be minor only.

I'll open a follow-up for that.

https://git.drupalcode.org/project/drupal/-/merge_requests/10704 is the MR and approach to review.

Restored the 2500 runs to its previous state before that diversion, to get a fresh green run on there.

https://git.drupalcode.org/project/drupal/-/jobs/3831420

catch changed the visibility of the branch 3496257-2500 to hidden.

catch’s picture

Status: Needs work » Needs review

catch changed the visibility of the branch 3496257-2500 to active.

catch’s picture

Just kicked off a third run of https://git.drupalcode.org/project/drupal/-/merge_requests/10705/pipelines which will give us 7500 passing tests in a row assuming it also comes back green.

spokje’s picture

Hating to be that guy, but hey, I _am_ that guy...

I'm very doubtful if 7500 runs will fit into the current cutoff time of 1 hour.

catch’s picture

@spokje it's 2500 runs * 3, should be OK.

catch’s picture

Well that's interesting, 2 runs out of 7500 failed https://git.drupalcode.org/project/drupal/-/jobs/3831692/viewer

Looks like this happened when the overall time for the test went up to 30-50+ seconds from 6-10s

Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes    9s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes    9s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes    9s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes    9s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes    9s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   18s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   23s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   24s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   26s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   26s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   30s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   36s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   35s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   35s                                      
\Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksT   0 passes   37s   1 fails                            
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   44s                                      
\Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksT   0 passes   48s   1 fails                            
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   62s                                      
Drupal\Tests\layout_builder\Functional\LayoutBuilderBlocksTe   1 passes   60s     

Might be a sign of the lock failing to be acquired or similar.

Don't think this necessarily invalidates the fix here since it's still a significant improvement, but might show we need to lock wait for longer and/or explore the hash idea further in the other issue.

catch’s picture

Pass rate of 12248/12500 now.

catch’s picture

Another 1/2500 fail on https://git.drupalcode.org/project/drupal/-/jobs/3832573/viewer so at 14997/15000 passes now.

catch changed the visibility of the branch 3496257-2500 to hidden.

catch changed the visibility of the branch 3496257-1750-again to hidden.

catch’s picture

catch’s picture

Famous last words because we're down to 4/15000 odds so false negatives are increasingly likely, but might have figured out the last race condition.

In earlier iterations of the MR, I was holding the lock acquired in State:set() and not releasing it, this was out of paranoia so changed it to release after it was noted in review. I now think there was a good reason for it after all.

In CacheCollector::updateCache(), we use a new cache item as a tombstone record for the following case:

1. Process A - cold cache

2. Process B - invalidates the cache (e.g. a state write).

3. Process A writes the cache back at end of request, with stale data from before process B wrote its data.

To avoid this, Process B writes a new cache item, and then process A sees that a brand new cache item has been written when none existed before, and throws its data away instead.

The remaining race condition looked like this:

1. Process A - cold cache
2. Process B - state write and cache write.
3. Process B - end of request - deletes cache item (fails to acquire a lock or any other reason
4. Process A - because the cache item is deleted, doesn't see it, so writes the stale data to the cache.

Holding the lock does the following:

1. Process A - cold cache
2. Process B - state write and cache write
3. Process B - end of request - still has the lock, so writes to cache again instead of deleting.
4. Process A - finds the new cache item so throws its data away

penyaskito’s picture

If this helps, as someone completely new to this issue, the code comments look as clear as I think they can be to understand the complex issue and the fix.

nicxvan’s picture

I think this is getting pretty close do we want a branch now with just the cache changes?

I'm trying to figure out how to check the probabilities, it's been a while since I took statistics.

nicxvan’s picture

Ok using Poisson's distribution, then the odds that something that happens 3/15000, having that same thing happen 0 times in 22500 is 1.1%
https://www.wolframalpha.com/input?i2d=true&i=Divide%5B%5C%2840%29Power%...

Now this isn't perfect because it assumes that the 3/15000 rate is constant and obviously we're affecting the thing driving it, but we can't really do better that I can see.

Just for others following along the formula is (e^(-λ) (λ^x))/x!
e is euler's number
λ is the rate 3 divided by 15000 multiplied by 22500 = 4.5
x is the number expected which in this case is 0

Plug those numbers in and you get 1.1%
I interpret that to mean if the rate was 3/15000 then we'd have to run a set of 22500 tests 100 times to get a run that had 0 failures.

That feels definitive to me.

spokje’s picture

Just my EUR0.02: I can see an, almost always considerable, rise in the failure rate when it's more busy on Drupal CI.
All runs here have taken place in the Christmas/New Years period, in which it's very quiet around here.

If possible, I would like to see a few runs from monday January 6th onward, when normal business is restored and see if that affects the numbers drastically.

If that would be too long of a wait, Im OK with this as-is, if stuff starts breaking down (more) on Monday we can act then as needed.

catch’s picture

Couple of general points, mostly answering questions I asked myself while working on this:

This problem is fairly unique to state's use of cache collector. Most cache collector implementations just build a 'runtime' cache from a discovery cache (smaller array from a massive array), so they are not changing values in the underlying data at all.

The most likely problem this change could cause we would be finding badly behaving contrib or custom code that is writing to state every request or something, but that would already cause problems with state caching, the lock wait would make those problems a bit worse, but writing to state every request would be pretty extreme.

With a normal/occasional state write, while the lock is being held, other processes will need to do some extra key/value queries, and won't be able to write back to the cache, but this is relatively cheap (about the same as before we had state caching in the first place). So that bit seems fine.

While adding all the logic, I wondered if state should instead re-implement the parts of cache collector it needs instead of making it more complicated, but:

- overall we'd probably end up with the same amount of complexity, but in two places instead of one
- the changes to cache collector make it more robust for contrib/custom use-cases doing similar things
- even if state moved to 'immediate write' with no end of request lazy-writing at all, it would still have to lock both state writes and cache writes.

There is one possible optimization we can do here which I realised writing the above:

When we write to the cache item in State::set(), if we've acquired a lock, include the other state keys that are already loaded too, that will make it more likely that subsequent requests get a cache hit (including while the lock is being held). Not going to have time to work on that for at least a few hours though.

No we can't do that, because we'd run into the same race condition again in another place - even if we acquire a lock, we don't know what happened immediately before we acquired the lock, so we'd need to do all the same checks on whether the cache item we had at the beginning of the request has changed in the meantime etc. in State::set() too. Maybe if we could factor out some of the logic in updateCache() to re-use, but not easy. Follow-up material, but not sure I want to open it even.

@nicxvan https://git.drupalcode.org/project/drupal/-/merge_requests/10704 is the committable/reviewable branch, it should be up-to-date.

catch’s picture

@nicxvan thanks for the stats research, that's pretty encouraging.

I've pushed a revert of #3477586: [random test failure] LayoutBuilderBlocksTest::testBlockPlaceholder failing to the 2500 test runs branch (https://git.drupalcode.org/project/drupal/-/merge_requests/10749) so that it's possible to keep hitting the 'run pipelines' button on there a bit more next week if this isn't committed before then. That will also give us another 2500 runs in the meantime.

catch’s picture

wim leers’s picture

Arrived here via #3496405: [random test failure] EditorSecurityTest::testEditorXssFilterOverride.

@catch's #13 + #30 + #36 + #38 are 🤯

@nicxvan: it's been >10 years since I saw somebody in a software engineering context mention Poisson's distribution and it reminds me of one of the very few books I still have lying around from studying Computer Science: https://www.taylorfrancis.com/books/mono/10.1201/9781420011425/probabili... 🤓😅

@spokje in #35: +1 for increased CI failure rates when CI infra is under load.

catch’s picture

Since it's Monday, started kicking off pipelines again. Will edit this comment to add them when I remember to kick another one off (insert xkcd compiling image).

https://git.drupalcode.org/project/drupal/-/pipelines/387144
https://git.drupalcode.org/project/drupal/-/pipelines/387190

catch’s picture

Status: Needs review » Needs work

I think we can skip holding the lock after all.

Short explanation of why:

Performance tests were failing in the latest MR, this is because the cli was holding the lock and preventing the site-under-test from acquiring it (annoying).

Trying to workaround this made me realise we're mainly holding the lock to avoid ::updateCache() throwing away the tombstone record when it can't acquire a lock.

Instead of that, if we've acquired a lock, we can completely disable the end of request logic in state, and release the lock. That means as soon as the state item is written other requests can start to build the cache again, without any interference from the ::set() logic.

Obviously, now need to check that this still fixes the race condition so another 10k+ test runs to kick off on the test-only MR - will start that once the main fix MR is green again.

catch’s picture

Status: Needs work » Reviewed & tested by the community
catch’s picture

Status: Reviewed & tested by the community » Needs review

Too much status change.

catch’s picture

15k runs without a failure so far, which is pretty encouraging, so I think this genuinely is ready for review again now. I'll kick off some more pipelines tomorrow.

spokje’s picture

Great work, I suppose this also takes care of random failures in tests that actually need to use State for testing like \Drupal\Tests\system\Functional\System\CronRunTest::testCronUI?

This is an example where we can't just switch to KeyValue, since State is an integral part of running cron.

catch’s picture

@Spokje yes it should fix cases like that as well.

catch’s picture

Several fails on: https://git.drupalcode.org/project/drupal/-/jobs/3923493

These all happened in a cluster of tests where the runtime doubled.

catch’s picture

Theory on the above - the runtime doubling could be not acquiring a lock, and having to wait for a second - the runtime doubled when the test itself was holding the lock for the duration of the test (although that prevented any new caching at all so fixed the race condition).

So... trying to be more aggressive when we fail to acquire the lock.

catch’s picture

The simplification led to a behaviour change for the path alias cache, but it's very small and still works.

I think it is probably possible to make updateCache() work for what State:::set() wants to do, but I've already spent a lot of time on this issue and need a break, so I think we should defer that to a follow-up - it might reduce some code and make things hopefully even more robust, but it will be as tricky as this issue to ensure we've caught all the cases. Would rather have things correct which hopefully the current state of the MR gets us to.

Because a few things have changed again, going to run more pipelines..

https://git.drupalcode.org/project/drupal/-/pipelines/390206
https://git.drupalcode.org/project/drupal/-/pipelines/390279
https://git.drupalcode.org/project/drupal/-/pipelines/390291
https://git.drupalcode.org/project/drupal/-/pipelines/390300
https://git.drupalcode.org/project/drupal/-/pipelines/391267 <- failures in this one

dww credited alexpott.

dww’s picture

Status: Needs review » Needs work
Issue tags: +Bug Smash Initiative

(Sorry for the assigned/unassigned noise -- accidentally clicked something in the MR review UI).

Resolved the opened threads, since @catch has already addressed all the feedback.

Opened a few trivial suggestions for some nits, and to rename a test method that no longer does what the old name says.

I would RTBC, but I think the suggestions are worth applying, first. 😅

Initial pass at saving credit:
@catch (duh)
@alexpott for MR reviews + suggestions
@Spokje, @nicxvan, @godotislate, @luke.leber, and myself for MR and issue reviews

Almost there!

Thanks,
-Derek

p.s. restoring the Bug Smash Initiative tag, which I think @catch accidentally clobbered in #13 with a stale form submit.

catch’s picture

Tracked down the very specific race condition with the test (well I think I did) which was still resulting in some failures occasionally - about 1/2500.

I think it is:

1.test requests a page
2. page writes to the state cache collector
3. test sets state
4. test requests a page.

Because the test doesn't use WaitTerminateTestTrait, if #2 is holding the lock while #3 happens, we go into the !$lock_acquired branch in state.

But the implementation so far was concentrating on the request that hits the $lock_acquired branch eventually winning out when it reaches the end of the request (more or less eventual consistency). In the test, there is no 'end of the request', there is just the end of the test, and the page we hit immediately after trying to set state can still get the wrong value in the meantime. The answer is to even more aggressively write to the cache when we can't acquire the lock. It still doesn't work if the underlying request takes longer than a second to complete, because then both the pre- and post- lock acquire attempt cache writes can still be overwritten. We could wait longer than a second, but on a real request the end of request logic is more likely to kick in later and clean everything up.

Bit annoyed that I didn't look at the test in more detail earlier because that's obvious in retrospect, but was trying to focus on the stampede condition that real sites will hit vs functional test weirdness.

Fresh set of runs:

https://git.drupalcode.org/project/drupal/-/pipelines/392873
https://git.drupalcode.org/project/drupal/-/pipelines/393001
https://git.drupalcode.org/project/drupal/-/jobs/3985642 <- 1 fail

godotislate’s picture

Is "Needs work" correct as current status, or is this ready to be reviewed again?

catch’s picture

@godotislate it's not actually fixing the race condition at the moment so very much needs work - although review of the approach would probably still be useful.

catch’s picture

Merged 11.x into the repeat test run MR. One fail in 5000 runs.

https://git.drupalcode.org/project/drupal/-/jobs/4331449

https://git.drupalcode.org/project/drupal/-/jobs/4331562 -> fail

I'm wondering if the remaining test failure is due to #2347867: Race conditions with lock/cache, session storage - add a non-transactional database connection (e.g. a deadlock on the cache/lock table), and if so, that will be unfixable with this MR. Given that, going to rebase the main MR here and move this back to needs review.

bgustafson’s picture

I have been having issues where state is being reset somehow/somewhere, which seems to be a caching issue. I'm planning to opt out of state cache (as per https://www.drupal.org/node/3443018) until this is resolved. Can someone clarify if this and/or #3496328: Use a hash of the cache data for CacheCollector race condition detection will be resolved before the Drupal 10 EOL? It seems like a major issue to me (I see this is already marked as Critical).

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

nicxvan’s picture

Just found this again, I think this would be a great one to get in.

A bunch of conflicts though, so I'm not sure it's worth reviewing in the current state (lol).

catch’s picture

Status: Needs work » Needs review

Rebased.

catch’s picture

Added some unit test coverage for both cache collector and state for everything I can think of.

To some extent this is 'testing the implementation' but to be entirely honest I don't have any other ideas there at the moment.

catch’s picture

JsonApiPerformanceTest had a new failure. Debugged it and it was due to the new stricter checks in CacheCollector as to whether we should write a cache item or not. Additional sleep() calls after the cache warming requests fixed it without changing any test assumptions.

nicxvan’s picture

I think this is pretty much ready.

There is a new conflict, and we should decide if 35 deserves another large run (is drupalcon usually busier for CI or lighter?)

catch’s picture

Rebased. The unit test now sets up stubs, whereas for the added method we actually want mocks, so I moved several things inline in the method.

If we want to do a large run for #35 we'll need to recreate (probably easier than rebase) https://git.drupalcode.org/project/drupal/-/merge_requests/10749 which restores a state usage in a test that was failing randomly then runs it 2500 times.

nicxvan’s picture

Status: Needs review » Reviewed & tested by the community

On second thought I don't think we really need the 2500 run, we've since fixed those issues and this already demonstrated it fixed one of those so this is just an outright improvement.

godotislate’s picture

Status: Reviewed & tested by the community » Needs review

Open comment on the MR.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new549 bytes

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Needs review

Don't think it's failing, rebased in case the bot is picking up a stale branch.

godotislate’s picture

Couple Qs on the MR about the comment content.

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

I think this lgtm now.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Left a couple of comments on the code that need addressing.

catch’s picture

Status: Needs work » Needs review

Removed the cruft, added an additional unit test method, and updated the usleep() bit (to also try to simplify it).

berdir’s picture

Status: Needs review » Reviewed & tested by the community

Went through this as well, looks good to me, my minor feedback was adressed and I confirmed that the review from @alexpott was adressed.

I may kind of regret pushing for the state collector cache after reading State::setMultiple(). It's useful, but that complexity is quite a steep price we have to pay for that.

catch’s picture

I may kind of regret pushing for the state collector cache after reading State::setMultiple(). It's useful, but that complexity is quite a steep price we have to pay for that.

If this one fixes it, or about 99.9% which seems to be the case, then I won't regret it. We're talking about billions, possibly trillions of database queries saved across all Drupal sites, state is right in the middle of the critical path. But yes it is gnarly in there.

alexpott’s picture

Version: main » 11.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

We should backport this to 11.x and 11.3.x right?

  • alexpott committed 8d660fef on main
    fix: #3496257 Race conditions in CacheCollector/State (again)
    
    By: catch...
alexpott’s picture

Committed 8d660fe and pushed to main. Thanks!

catch’s picture

Pushed an 11.x backport branch/MR.

If this is a clean cherry-pick to 11.3.x from 11.x we could do that, but if not I'd leave it in 11.4 - only known case of anyone running into this is core test runs, it should be extremely unlikely in the wild.

catch’s picture

Status: Patch (to be ported) » Needs review

Rebase is passing except for a parse error in AssetResolverTest on PHP 8.3 which I think is in HEAD, definitely not changed here.

godotislate’s picture

Think the 11.x issue with AssetResolverTest was reverted in #3366561: Support preloading of fonts in the libraries API.

  • catch committed 7a148275 on main
    Revert "fix: #3496257 Race conditions in CacheCollector/State (again)"...
catch’s picture

Version: 11.x-dev » main

Reverted the main commit - HelpTopicsSyntaxTest started taking 20 minutes, it sets state a lot.

catch’s picture

Status: Needs review » Reviewed & tested by the community

#3586606: Improve performance of HelpTopicsSyntaxTest is in main so this should be OK again.

godotislate’s picture

---- Drupal\Tests\help\Kernel\HelpTopicsSyntaxTest ----
Status      Duration Info                                                                               
--------------------------------------------------------------------------------------------------------
Pass         20.408s testHelpTopics

At least this one's not 20 minutes anymore.

Before we go ahead again, should we check to see if the MR build had any other noticeably slow tests?

alexpott’s picture

There is some indication that the MR is slower if we compare PHP Functional 6/8....

MR

https://git.drupalcode.org/project/drupal/-/jobs/9585741

real	4m12.229s
user	25m55.118s
sys	5m38.865s

Another MR on the same runner

https://git.drupalcode.org/project/drupal/-/jobs/9591086

real	2m28.912s
user	22m43.429s
sys	3m15.442s

HEAD

https://git.drupalcode.org/project/drupal/-/jobs/9589095

real	3m0.364s
user	30m24.841s
sys	4m4.009s

But I'm not really sure what this is telling us - there was no test that took 20 mins :)

catch’s picture

#3506148: [meta] Replace all uses of state in tests with direct key value should eventually remove all state usage for controlling, um, state in tests. That unfortunately has a high chance of hitting a lock wait because both the test and the site under test will end up trying to write to the cache, via an explicit state set, often multiple times per test method because different state values keep getting set. Exactly how widespread a problem this is likely to be I don't know though. We opened that issue and this one partly due to a high rate of random test failures due to this, not sure how many tests that we didn't already switch to key/value actually fail due to the race condition though at this point.

Kicked off another pipeline to another run to compare against.

catch’s picture

Functional 6/8 still a bit slower than the other examples but not as much, very hard to tell if it's the MR or just usual pipeline variation.

https://git.drupalcode.org/project/drupal/-/jobs/9593982

Wall time: 3 min 39 sec
Total time:  1 hour 9 min

real	3m41.728s
user	33m47.443s
sys	4m36.187s

However there's a new performance test fail - probably needs better setup

longwave’s picture

Status: Reviewed & tested by the community » Needs review

I found the $write_cache logic a bit hard to follow here so I've tried to simplify it, hopefully this makes more sense to others too?

catch’s picture

https://git.drupalcode.org/project/drupal/-/merge_requests/10704/diffs?c... looks good to me visually, however the performance fails suggest it's resulting in different behaviour.

catch’s picture

On the other hand there's a green run on #3496328: Use a hash of the cache data for CacheCollector race condition detection so maybe we should merge that issue back into this one? I originally spun it out to make this issue not too much change at once, but not sure it's actually any more or less change really given the complexity here.

longwave’s picture

I just rebased 3496257-race-conditionsbad-lock to see if that helps, because there were 71 commits here and it was quite far behind main, so possibly something odd going on with merging/CI.

longwave’s picture

Also wondering if those sleeps that you added here still aren't enough, maybe this is some non-determinism thing to do with the order things can happen in during tests...

longwave’s picture

Hm, so the results are exactly the same after rebase, investigating...

longwave’s picture

As far as I can tell: the numbers are now truly correct, the fix and reduction is real: in main we overwrite the cached data with possibly stale data, in this MR we notice that the item is newer and we don't overwrite it.

longwave’s picture

@catch MR!15935 proves it, that's a revert of my simplification work leaving only the previous MR behind, so something else has changed somewhere since this was previously green for this to now break HEAD!

longwave’s picture

Wondering if these changes are a side effect of #3503843: Remove automatic preloading of all "public" routes, cache routes in fast chained bin or something similar?

catch’s picture

OK interesting, well that proves that the behaviour is equivalent, although it doesn't explain why #3496328: Use a hash of the cache data for CacheCollector race condition detection doesn't require the same changes.

longwave’s picture

Oh, because we check the hash in #3496328: Use a hash of the cache data for CacheCollector race condition detection then we don't do any writes if the cache item didn't actually change, but here we blindly write anyway if the cache data is identical. So maybe we just land that...? However, the hashing isn't entirely free...

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new98 bytes

The Needs Review Queue Bot tested this issue. The merge request has merge conflicts and cannot be merged. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

Status: Needs work » Postponed

I think we should go ahead with #3496328: Use a hash of the cache data for CacheCollector race condition detection which includes all the changes here + additional refactoring on top, so marking this postponed - we can close it once that's in.