Problem/Motivation
There are typically two blocks that display local tasks, primary and secondary. They essentially rely on the same static cache and access different data within.
The return value for a miss in the static cache is immediately initialized. If the access checks for those local tasks result in a fiber suspension, then the other blocks is built, which requests the local tasks for the same route and then immediately gets an empty list back.
Steps to reproduce
Enable webform and entity_usage on 11.x, enable entity usage for webforms. Secondary local tasks are somewhat uncommon, this specific combination loads a webform for the usage local task (and because it doesn't load it override free, it doesn't hit the existing static cache)
Either the primary or secondary local tasks will now be missing.
Proposed resolution
Add a fiber suspend if we're in a fiber and already calculating local tasks? Fairly straightforward fix, but I am concerned that we're going to hit more cases like this with static caches.
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3553342
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:
- 3553342-race-condition-in
changes, plain diff MR !13539
Comments
Comment #3
berdirProposed a fix that seems to be fixing this for our case.
Comment #4
kristiaanvandeneyndeHmm, in this case the fix is simple and you only needed to move an array initialization into an else-block to avoid tripping up the initial isset() check. But not every system will be so easily adjusted to avoid fibers-induced race conditions like these.
However, by moving the array initialization, doesn't that solve the issue we had in the first place? I.e.: Do we still need to suspend the fiber then? I don't think the issue persists if you no longer initialize $this->taskData[$route_name] outside of the route attribute check. Right?
Edit: Suspending the fiber might still have some performance benefits because we don't run the calculations twice.
Comment #5
smustgrave commentedPossible to add a test case for this?
Comment #6
clayfreemanShould there be a meta issue to add critical sections for all other static caches? This would seem to be a sensible requirement that should block progress on additional concurrency efforts. (I presume out-of-process caches won't have similar concerns.)
Comment #7
berdirI managed to write a unit test that simulates a fiber suspend in a callback. The test fails hard and not quite like real-world use cases do because I added an explicit assert on the fiber state. Alternatively I'd need to conditionally resume the fiber to simulate the early return that happens in D10 so that I could then assert the actual response. I think this is clearer and easier to understand.
Comment #8
kristiaanvandeneyndeI think a rogue commit snuck in there?
Ideally we stop using properties as caches altogether. As demonstrated here, they lead to issues. So maybe a meta issue that tracks all the places where we use properties as caches is warranted.
The test approach seems fine, but given how the issue was resolved by moving the variable initialization around, I wonder what we're testing here. If we're only testing whether the fiber is suspended, maybe we could extend the LocalTaskManager for this test and add a public isLoadingLocalTasks() method.
Then we could drop the result checks altogether and merely verify that when isLoadingLocalTasks() returns TRUE, the next fiber gets suspended? We don't really care about the return value of getLocalTasks() here, do we?
Comment #9
berdirOops, fixed the extra commit that snuck in.
The test fails first on the explicit asserts on the fiber, but it also explicitly asserts the response value. On HEAD, this would also fail because the return value of the second call would be an empty array.
I could make it more agnostic to the exact fiber behavior, I kind of did that on purpose as I wrote it to assert that the mock does suspend at least once, because if I'm just doing a general loop to resume the fibers until they're done then it might also pass if for some reason the fiber was never suspended. But I just had an idea to do the loop but also ensure that it was suspended at least once.
The test fail is clearer then:
So the result of the second fiber/level 1 is now empty without the fix. This doesn't explicitly test that we suspend to avoid calculating twice, but that's the optimization on top of the fix. The most important thing is IMHO the correct response.
On properties vs cache, I think that would only indirectly help in that using a cache would make it less likely to initialize to an empty result too early, as is the original problem here. We wouldn't think of the extra optimization either, but I still have that idea of a cache wrapper/helper that allows to use that neat Symfony DX of a cache get with a compute callback and optional locking (per preprocess for a memory bin or global) to standardize this. One day I'll manage to flesh that out and create an issue.
Comment #10
kristiaanvandeneyndeEh, good enough for me.
My point exactly.
Yes, ideally we hit two birds with one stone by combining the effort of replacing property caches with the approach you suggested.
Comment #11
catchWe could add a sibling issue for #2218651: [meta] Make Drupal compatible with persistent app servers like ReactPHP, PHP-PM, PHPFastCGI, FrankenPHP, Swoole and #1577902: [META] Remove all usages of drupal_static() & drupal_static_reset() for the class property caches, we have quite a lot of ::reset() methods that can eventually be removed if we clean all of that up. Would also be a follow-up from #3047289: Standardize how we implement in-memory caches.
I think this solution looks good but need to look at it one more time when I have a bit longer in one go.
Comment #12
catchMoving to needs work for the expanded comment.
Comment #13
berdirUpdated the comment.
Comment #14
catchThat looks great. This is something that will eventually need updating once we move to revolt, so good to have the background in there.
Comment #16
catchTagging needs follow-up for #10 / #11.
Committed/pushed to 11.x and cherry-picked to 11.3.x, thanks!
Comment #20
greatmatter commentedDo we know when this will make it into a release?
Comment #21
nicxvan commentedIt will be in 11.3.2