Updated: Comment #N

Problem/Motivation

When some code needs to rebuild the router, it calls setRebuildNeeded() instead of calling rebuild() directly, in order to prevent multiple rebuilds in one request.
RouterRebuildSubscriber subscribes to KernelEvents::TERMINATE to check if it was set to rebuild, and it runs then.

However, in the case of a rebuild triggered right before a redirect, the redirect will happen *before* the router even starts rebuilding, and the route rebuilding only gets about 30% done before the next request starts.

In the case of #2229303: Enable/add of search pages leads to route not found exception, the redirected page tries to use a route that has not been built yet, and it fatals.

Proposed resolution

N/A

Remaining tasks

Find some way to fix this.

User interface changes

N/A

API changes

N/A

Comments

dawehner’s picture

Status: Active » Needs review
StatusFileSize
new709 bytes

This should be it.

jhodgdon’s picture

tim.plunkett’s picture

Status: Needs review » Needs work

Wow, very simple and elegant. Thanks @dawehner!

We also need to add this to getRoutesByPath() in the same class.

As I understand it, until #2205527: Move configuration import lock to lock.persistent service since a lock can not exist beyond a single request goes in, we'll actually be rebuilding the router twice.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new991 bytes
new527 bytes

Sure, let's do it.

The last submitted patch, 1: route_provider-2230091-1.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 4: route_provider-2230091-4.patch, failed testing.

dawehner’s picture

The reason for this failure is the following problem: People call getRouteByName during route rebuildling. (rest (yes rest!), config_translation),
which result in infinite recursion.
This is really fragile if you change anything. #2158571: Routes added in RouteSubscribers cannot be altered solves a couple of those problems,
by passing along the route collection during route rebuild time.

jhodgdon’s picture

There are good reasons for people calling getRouteByName (directly or indirectly) during dynamic route creation, for instance so that they can base their URLs on existing static routes. Presumably they wouldn't need the dynamic routes for this?

dawehner’s picture

I disagree that you should do that. It was indeed just implementation detail that this kinda worked in most cases currently.
In the linked patch you have events you can use which retrieve all the previous defined routes, just not accessible via the route provider interface.

jhodgdon’s picture

That sounds like a good idea. This should be documented on the getRouteByName and other methods that shouldn't be called during dynamic route creation.

tim.plunkett’s picture

4: route_provider-2230091-4.patch queued for re-testing.

The last submitted patch, 4: route_provider-2230091-4.patch, failed testing.

tim.plunkett’s picture

Posting the backtrace in 3 chunks, to call out the important part:

Drupal\Component\Plugin\Exception\PluginException: The plugin (entity:node) did not specify an instance class. in Drupal\Component\Plugin\Factory\DefaultFactory::getPluginClass() (line 64 of /Users/tim/www/d8/core/lib/Drupal/Component/Plugin/Factory/DefaultFactory.php).
Drupal\Component\Plugin\Factory\DefaultFactory::getPluginClass('entity:node', NULL)
Drupal\Core\Plugin\Factory\ContainerFactory->createInstance('entity:node', Array)
Drupal\Component\Plugin\PluginManagerBase->createInstance('entity:node')
Drupal\rest\Plugin\Type\ResourcePluginManager->getInstance(Array)
Drupal\rest\Routing\ResourceRoutes->alterRoutes(Object)
Drupal\Core\Routing\RouteSubscriberBase->onAlterRoutes(Object, 'routing.route_alter', Object)
call_user_func(Array, Object, 'routing.route_alter', Object)
Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(Array, 'routing.route_alter', Object)
Symfony\Component\EventDispatcher\EventDispatcher->dispatch('routing.route_alter', Object)
Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('routing.route_alter', Object)
Drupal\Core\Routing\RouteBuilder->rebuild()
Drupal\Core\Routing\RouteBuilder->rebuildIfNeeded()
Drupal\Core\Routing\RouteProvider->getRoutesByNames(Array, Array)
Drupal\Core\Routing\RouteProvider->getRouteByName('entity_test.render')
Drupal\rest\Plugin\Derivative\EntityDerivative->getDerivativeDefinitions(Array)
Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator->getDerivatives(Array)
Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator->getDefinitions()
Drupal\Core\Plugin\DefaultPluginManager->findDefinitions()
Drupal\Core\Plugin\DefaultPluginManager->getDefinitions()
Drupal\Core\Plugin\DefaultPluginManager->getDefinition('entity:node')
Drupal\Core\Plugin\Factory\ContainerFactory->createInstance('entity:node', Array)
Drupal\Component\Plugin\PluginManagerBase->createInstance('entity:node')
Drupal\rest\Plugin\Type\ResourcePluginManager->getInstance(Array)
Drupal\rest\Routing\ResourceRoutes->alterRoutes(Object)
Drupal\Core\Routing\RouteSubscriberBase->onAlterRoutes(Object, 'routing.route_alter', Object)
call_user_func(Array, Object, 'routing.route_alter', Object)
Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(Array, 'routing.route_alter', Object)
Symfony\Component\EventDispatcher\EventDispatcher->dispatch('routing.route_alter', Object)
Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('routing.route_alter', Object)
Drupal\Core\Routing\RouteBuilder->rebuild()
drupal_flush_all_caches()
Drupal\simpletest\WebTestBase->resetAll()
Drupal\simpletest\WebTestBase->setUp()
Drupal\views\Tests\ViewTestBase->setUp()
Drupal\rest\Tests\Views\StyleSerializerTest->setUp()
Drupal\simpletest\TestBase->run()
_simpletest_batch_operation(Array, '14', Array)
call_user_func_array('_simpletest_batch_operation', Array)
_batch_process()
_batch_do()
_batch_page(Object)
Drupal\system\Controller\BatchController->batchPage(Object)
call_user_func_array(Array, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\HttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)
drupal_handle_request()

So it is rebuilding, and runs the alter event.
The REST alter tries to find a definition of the entity:node resource
But the definitions were just cleared
So the REST EntityDerivative runs, and tries to find the route...

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new2.96 KB

If things would be always simple ...

Status: Needs review » Needs work

The last submitted patch, 14: 2230091-14.patch, failed testing.

catch’s picture

Priority: Major » Critical

Looks critical to me.

catch’s picture

MatcherDumper::dump() is supposed to do this in a transaction to avoid the empty router table issue. This issue proves that we can't rely on transactions for that - likely default transaction isolation level isn't enough to protect against this.

https://api.drupal.org/api/drupal/includes%21menu.inc/function/menu_get_... had a check for menu rebuild as well as an empty menu masks variable.

menu_rebuild() had a lock to prevent stampedes.

Looks like both of those need equivalents adding back.

dawehner’s picture

Well, the router rebuilder has a lock.

martin107’s picture

Status: Needs work » Needs review
StatusFileSize
new2.91 KB

Patch no longer applied, needed reroll. RouteProviderTests pass locally

Status: Needs review » Needs work

The last submitted patch, 19: 2230091-18.patch, failed testing.

martin107’s picture

This patch still applies, but much has changed in this area.. so I am about to retest.

Retest wipes away test data so I am providing a link to it here, currently 4 fails, 1 exception

https://qa.drupal.org/pifr/test/805923

Status: Needs work » Needs review

martin107 queued 19: 2230091-18.patch for re-testing.

dawehner’s picture

@martin107
If you re-test a patch you can't really access previous results. In case these errors are helpful consider to just reupload a patch.

Status: Needs review » Needs work

The last submitted patch, 19: 2230091-18.patch, failed testing.

dawehner’s picture

posting the backtrace which lead to the current exceptions:

There seems to be something going on wrong during rebuild of the routes.

Drupal\Core\Routing\RouteProvider->getRouteByName('system.admin_content')
Drupal\Core\Routing\UrlGenerator->getRoute('system.admin_content')
Drupal\Core\Routing\UrlGenerator->generateFromRoute('system.admin_content', Array, Array)
Drupal::url('system.admin_content')
node_permission()
call_user_func_array('node_permission', Array)
Drupal\Core\Extension\ModuleHandler->invokeAll('permission')
Drupal\simpletest\WebTestBase->checkPermissions(Array)
Drupal\simpletest\WebTestBase->drupalCreateRole(Array)
Drupal\simpletest\WebTestBase->drupalCreateUser(Array)
Drupal\comment\Tests\CommentStatisticsTest->setUp()
Drupal\simpletest\TestBase->run()
_simpletest_batch_operation(Array, '8', Array)
call_user_func_array('_simpletest_batch_operation', Array)
_batch_process()
_batch_do()
_batch_page(Object)
Drupal\system\Controller\BatchController->batchPage(Object)
call_user_func_array(Array, Array)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1)
Drupal\Core\DrupalKernel->handle(Object)
dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new3.05 KB
new4.6 KB
new2.67 KB

Sadly this is kind of similar as the failure in #2327965: drupal_flush_all_caches() doesn't clear all plugin caches

The last submitted patch, 26: routes_rebuild-2230091-26.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 26: routes_rebuild-2230091-26.patch, failed testing.

martin107’s picture

I won't trigger another retest ... but just to highlight this extract from the test errors.

Allowed memory size of 335544320 bytes exhausted (tried to allocate 130968 bytes)

dawehner’s picture

Yeah this is probably caused by some recursive call of the route rebuilding I can't really figure out yet.

mgifford’s picture

jhodgdon’s picture

mgifford - this issue was already marked "related" from that other one. Not really necessary to have it in the sidebar twice... :)

mgifford’s picture

Sorry, thanks for removing it. Most postponed issues I was looking at weren't properly related to why they were postponed. I definitely made a few mistakes.

marthinal’s picture

Status: Needs work » Needs review
StatusFileSize
new4.62 KB

It fixes #2229303. Rerolled. Let's take a look at the test results.

Status: Needs review » Needs work

The last submitted patch, 34: 2230091-34.patch, failed testing.

fabianx’s picture

Architectural question:

- Isn't it much simpler to decrease the priority of the rebuild to happen after the redirect.

Then check if a rebuild is needed on route request (like done in D7 with menu_reubild_needed()) - need to do that anyway as a process can die at any time ...

Status: Needs work » Needs review

Berdir queued 34: 2230091-34.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 34: 2230091-34.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new29.7 KB

Trying a combined patch of this an #2248297: Ensure routes are rebuilt when install modules, just to see what happens.

Status: Needs review » Needs work

The last submitted patch, 39: 2230091-37-combined.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new7.97 KB
new3.41 KB

Ok, back to the previous patch. Found the problem, it was a recursion in config_translation.module.

Fixed that, checked with @tstoeckler, we no longer need that. Also added a recursion protection so we have something more useful to look at.

fabianx’s picture

Status: Needs review » Needs work
Related issues: +#356399: Optimize the route rebuilding process to rebuild on write
+++ b/core/includes/common.inc
@@ -860,7 +860,7 @@ function drupal_set_time_limit($time_limit) {
     if ($current != 0) {
-      @set_time_limit($time_limit);
+      //@set_time_limit($time_limit);
     }

This is probably a debugging oversight?

Besides that: RTBC - Nice work!

This is touching this area of code, so adding related issue of #356399: Optimize the route rebuilding process to rebuild on write.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new7.53 KB
new316 bytes

Yep.

fabianx’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, then!

  • catch committed 5a85df4 on 8.0.x
    Issue #2230091 by dawehner, Berdir, marthinal, martin107: Fixed Route...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Looks great. Was wondering if we need to document more why we throw the exception, but can't really think of succint docs for that.

Committed/pushed to 8.0.x, thanks!

clemens.tolboom’s picture

I get the new exception triggered on module enable / drush cache-rebuild. See #2351353: What to do with "Recursive router rebuild detected."

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

leo liao’s picture

StatusFileSize
new726 bytes

PHP8.0, Drupal 9.3.16
- Install with standard profile.
- Go to Configuration > Search and Meta Data > Search pages (admin/config/search/pages)
- Add a new Content search page.

Symfony\Component\Routing\Exception\RouteNotFoundException: Route "search.view_test" does not exist. in Drupal\Core\Routing\RouteProvider->getRouteByName() (line 206 of docroot/core/lib/Drupal/Core/Routing/RouteProvider.php).