Problem/Motivation
Follow-up to #3553342: Race condition in LocalTaskManager::getLocalTasks() with fibers. That issue fixed the premature initialization of the task data in getLocalTasks(), and the same shape survives one method down, in getLocalTasksForRoute().
getLocalTasksForRoute() assigns an empty array to the instances property for the route and only then builds the tree. If a fiber suspends anywhere inside that build, the empty array is already visible to anyone else asking for the same route, and because the key exists the guard at the top of the method skips the build and returns it. The fiber handling added in #3553342: Race condition in LocalTaskManager::getLocalTasks() with fibers covers the task data and the suspension in getLocalTasks(), but nothing covers that placeholder.
The visible result is a tab bar that disappears rather than one that is short, because LocalTasksBlock drops the bar when fewer than two tabs are visible. That empty build is then cached with permanent max-age, so a single lost race hides the tabs of a page until the render cache is cleared or the local_task tag is invalidated. It reads as a per-language or per-page fault, since each request races on its own and only the loser keeps an empty bar.
On a real site the state was seen rather than the line: a probe reported that a request found the instances entry for the route already set to an empty array with no cache entry behind it, and that request rendered no tab bar while a concurrent one rendered all of its tabs. Where the other fiber was parked was not captured, only that it was somewhere inside the build.
Steps to reproduce
Reproduced on a clean 11.4.5 install with the minimal profile and no contrib modules:
- Construct a
LocalTaskManagerwith aNullBackend, so every call builds the tree instead of reading the cache. - Force one suspension inside the build: suspend the first
createInstance()call that runs inside a fiber. - Start two fibers that both call
getLocalTasksForRoute()for the same route, for exampleentity.user.canonical, and resume them in turn until both terminate. - The second fiber is handed an empty array. The first, once resumed, returns both tasks.
This is the scenario covered by testGetLocalTasksForRouteWithFiberSuspendedInBuild() in the merge request, which fails against unpatched code. It mirrors what testGetTasksBuildWithFibers() already does for the access check, one layer earlier, which may be why this half was not noticed.
Proposed resolution
Build the tree into a local variable and assign it to the instances property only once it is complete, so no caller is ever handed an empty or partial tree. A fiber arriving while another is building the route finds no key and builds it itself. This follows the existing comment in getLocalTasks(), which already accepts that a fiber may build the data twice rather than return something incomplete.
The alternative considered was recording which fiber is building a route, so that a caller arriving from a different one builds it itself. That needs bookkeeping the local variable does not, for the same outcome.
Remaining tasks
- Review.
- Backport to
11.xonce it is fixed onmain.
User interface changes
None.
Introduced terminology
None.
API changes
None.
Data model changes
None.
Release notes snippet
None.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the fix and its test case. I reviewed both, and the new test was confirmed to fail against unpatched core and to pass with the change.)
Issue fork drupal-3615746
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
Comment #2
mably commentedComment #4
mably commentedComment #5
quietone commentedThanks for the report and the fix and test. Changes are made on main first and then backported. This will need an MR on main.
I have restored the standard issue template which we use to track progress on an issue. It should be updated to include steps to reproduce and the proposed resolution.
The issue summary reads like an AI tool was used. If an AI agent was used to help with prose or the code, contributors are expected to disclose the use on the issue. The policy on the use of AI when contributing to Drupal includes that the use of AI must be disclosed. Thanks.
Comment #6
quietone commentedThe title doesn't read well to me. The phrase "the empty placeholder it left behind" seems not connected.
Comment #7
mably commentedThanks @quietone for the review, and for restoring the template.
EDIT: I removed my AI-assisted comment at @smustgrave's request below. Unfortunately there are only 24 hours in a day, English isn't my first language, and I no longer have the time to handcraft everything carefully. All I know is that this issue summary describes a real problem we have run into ourselves. Maybe someone will be able to report it better than I can. Happy to close it if that's preferred.
Comment #8
smustgrave commentedPer the policy AI summaries should be avoided
https://www.drupal.org/docs/develop/issues/issue-procedures-and-etiquett...