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:

  1. Construct a LocalTaskManager with a NullBackend, so every call builds the tree instead of reading the cache.
  2. Force one suspension inside the build: suspend the first createInstance() call that runs inside a fiber.
  3. Start two fibers that both call getLocalTasksForRoute() for the same route, for example entity.user.canonical, and resume them in turn until both terminate.
  4. 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.x once it is fixed on main.

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

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

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review
quietone’s picture

Version: 11.x-dev » main
Issue summary: View changes
Issue tags: +Needs issue summary update

Thanks 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.

quietone’s picture

Issue tags: +Needs title update

The title doesn't read well to me. The phrase "the empty placeholder it left behind" seems not connected.

mably’s picture

Title: getLocalTasksForRoute() can hand a second fiber the empty placeholder it left behind » Tab bar disappears when a fiber suspends while getLocalTasksForRoute() is building the local tasks
Issue summary: View changes
Issue tags: -Needs issue summary update, -Needs title update

Thanks @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.

smustgrave’s picture