Issue #3615746: Do not publish the local task instances before they are built

Follow-up to #3553342, which fixed the premature initialization of the task data in getLocalTasks(). The same shape survives one method down.

getLocalTasksForRoute() assigned an empty array to the instances for a route and only then built the tree. A fiber suspended anywhere inside that build left the empty array visible, and because the key existed the next caller skipped the build and took it for the route's tasks. LocalTasksBlock drops a bar with fewer than two visible tabs, and the empty build is cached with permanent max-age, so one lost race hid a page's tabs until the render cache was cleared or the local_task tag was invalidated.

The change

The tree is built into a local variable and assigned to $this->instances only once complete, which is the same move #3553342 made for the task data. A caller arriving during a suspended build now builds the tree itself, which the existing comment in getLocalTasks() already accepts: it "may build that information twice but will not return incomplete local task data". Nothing is added to the per-request path.

The test

testGetLocalTasksForRouteWithFiberSuspendedInBuild() follows the existing testGetTasksBuildWithFibers() closely, but forces the suspension during instance creation rather than in the access check that runs afterwards, which may be why this half went unnoticed. It fails without the change, on the assertion that the second fiber was handed an empty array.

Also reproduced outside the test suite on a clean 11.4.5 install with the minimal profile and no contrib modules: two fibers asking for entity.user.canonical, the second receiving an empty array while the first returns both tasks once resumed. I am happy to attach that script if useful.

An alternative would be to have a caller wait on the fiber doing the build, as getLocalTasks() does with its suspend-once guard, rather than building twice. I went with the smaller change and will gladly switch if maintainers prefer the symmetry.

See also #3615745, where a contrib module worked around this by subclassing the manager.

Merge request reports

Loading
Loading