Problem/Motivation

While trying to move the task contrib module #2351313: Rerolling to latest core version to latest head there is little to search for when running

$ drush @drupal.d8 cache-rebuild
Recursive router rebuild detected.

The error is gone after

$ drush @drupal.d8 pm-uninstall rest
The following extensions will be uninstalled: rest, hal, restui
Do you really want to continue? (y/n): y
hal was uninstalled successfully.          [ok]
rest was uninstalled successfully.         [ok]
restui was uninstalled successfully.

Is this because ResourceRoutes::alterRoutes is in err?

For the code see https://github.com/clemens-tolboom/task/tree/feature/2351313-reroll

Proposed resolution

Remaining tasks

User interface changes

API changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

clemens.tolboom’s picture

Issue summary: View changes
clemens.tolboom’s picture

Installing mentioned module (using the github branch) next enable rest.module give

[Mon Oct 06 15:56:53 2014] [error] [client 127.0.0.1] PHP Fatal error:  Allowed memory size of 536870912 bytes exhausted (tried to allocate 8 bytes) in /Users/clemens/Sites/drupal/
d8/www/core/vendor/symfony/debug/Symfony/Component/Debug/Exception/FlattenException.php on line 228, referer: http://drupal.d8/index.php/admin/modules

git hash 5a85df4c194fed184b900f6b0d610b39c6fcdf08

Next exception build in #2230091: Route rebuilding is not guaranteed to finish in time for the next request triggers.

$ drush @drupal.d8 cache-rebuild
Recursive router rebuild detected.

Is module enable (exhausting memory) or the new exception the cause?

Berdir’s picture

Priority: Normal » Major

You need to figure out what is causing the recursion and then we need to fix it. Might be rest.module.

drush doesn't show backtrace, so add a debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) call above the place where the exception is thrown.

Could be the way it is called in EntityDerivate in rest.module:

              if (($collection = $this->routeBuilder->getCollectionDuringRebuild()) && $route = $collection->get($route_name)) {
              }
              else {
                $route = $this->routeProvider->getRouteByName($route_name);
              }

If $collection is returned, it should never try to call getRouteByName(), so the second part there should move in an inner if.

clemens.tolboom’s picture

Status: Active » Needs review
Related issues: +#2158571: Routes added in RouteSubscribers cannot be altered
FileSize
1.38 KB
$ drush @drupal.d8 cache-rebuild
#0  Drupal\Core\Routing\RouteBuilder->rebuild() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Routing/RouteBuilder.php:201]
#1  Drupal\Core\Routing\RouteBuilder->rebuildIfNeeded() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Routing/RouteProvider.php:175]
#2  Drupal\Core\Routing\RouteProvider->getRoutesByNames() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Routing/RouteProvider.php:145]
#3  Drupal\Core\Routing\RouteProvider->getRouteByName() called at [/Users/clemens/Sites/drupal/d8/www/core/modules/rest/src/Plugin/Derivative/EntityDerivative.php:121]
#4  Drupal\rest\Plugin\Derivative\EntityDerivative->getDerivativeDefinitions() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php:105]
#5  Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator->getDerivatives() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php:91]
#6  Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator->getDefinitions() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php:223]
#7  Drupal\Core\Plugin\DefaultPluginManager->findDefinitions() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php:151]
#8  Drupal\Core\Plugin\DefaultPluginManager->getDefinitions() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Component/Plugin/Discovery/DiscoveryCachedTrait.php:27]
#9  Drupal\Core\Plugin\DefaultPluginManager->getDefinition() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Plugin/Factory/ContainerFactory.php:20]
#10 Drupal\Core\Plugin\Factory\ContainerFactory->createInstance() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Component/Plugin/PluginManagerBase.php:71]
#11 Drupal\Component\Plugin\PluginManagerBase->createInstance() called at [/Users/clemens/Sites/drupal/d8/www/core/modules/rest/src/Plugin/Type/ResourcePluginManager.php:47]
#12 Drupal\rest\Plugin\Type\ResourcePluginManager->getInstance() called at [/Users/clemens/Sites/drupal/d8/www/core/modules/rest/src/Routing/ResourceRoutes.php:74]
#13 Drupal\rest\Routing\ResourceRoutes->alterRoutes() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Routing/RouteSubscriberBase.php:43]
#14 Drupal\Core\Routing\RouteSubscriberBase->onAlterRoutes()
#15 call_user_func() called at [/Users/clemens/Sites/drupal/d8/www/core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php:164]
#16 Symfony\Component\EventDispatcher\EventDispatcher->doDispatch() called at [/Users/clemens/Sites/drupal/d8/www/core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/EventDispatcher.php:53]
#17 Symfony\Component\EventDispatcher\EventDispatcher->dispatch() called at [/Users/clemens/Sites/drupal/d8/www/core/vendor/symfony/event-dispatcher/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php:167]
#18 Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch() called at [/Users/clemens/Sites/drupal/d8/www/core/lib/Drupal/Core/Routing/RouteBuilder.php:174]
#19 Drupal\Core\Routing\RouteBuilder->rebuild() called at [/Users/clemens/Sites/drupal/d8/www/core/includes/common.inc:3505]
#20 drupal_flush_all_caches() called at [/Users/clemens/Sites/drupal/d8/www/core/includes/utility.inc:68]
#21 drupal_rebuild() called at [/Users/clemens/lib/drush/commands/core/cache.drush.inc:261]
#22 drush_cache_rebuild()
#23 call_user_func_array() called at [/Users/clemens/lib/drush/includes/command.inc:359]
#24 _drush_invoke_hooks() called at [/Users/clemens/lib/drush/includes/command.inc:210]
#25 drush_command()
#26 call_user_func_array() called at [/Users/clemens/lib/drush/includes/command.inc:178]
#27 drush_dispatch() called at [/Users/clemens/lib/drush/lib/Drush/Boot/DrupalBoot.php:46]
#28 Drush\Boot\DrupalBoot->bootstrap_and_dispatch() called at [/Users/clemens/lib/drush/drush.php:76]
#29 drush_main() called at [/Users/clemens/lib/drush/drush.php:16]
Recursive router rebuild detected.

core/modules/rest/src/Plugin/Derivative/EntityDerivative.php has a @TODO refering to #2158571: Routes added in RouteSubscribers cannot be altered

              if (($collection = $this->routeBuilder->getCollectionDuringRebuild())) {
                $route = $collection->get($route_name);
              }
              else {
                $route = $this->routeProvider->getRouteByName($route_name);
              }
              $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $route->getPath();

gives Fatal error: Call to a member function getPath() on a non-object

Attached patch raises exception on missing $route_name which is unfortunately not logged somewhere.

clemens.tolboom’s picture

Added DX tag as the missing route should appear in the http://drupal.d8/admin/reports/dblog

Berdir’s picture

Status: Needs review » Needs work
+++ b/core/modules/rest/src/Plugin/Derivative/EntityDerivative.php
@@ -115,12 +115,18 @@ public function getDerivativeDefinitions($base_plugin_definition) {
+              else {
+                throw new RouteNotFoundException($route_name);
+              }

I think instead of this, we should just repeat the logic inside the catch block and continue. Throwing and immediately catching the exception again is just unecessary overhead.

clemens.tolboom’s picture

@Berdir I'm not sure what you mean. "repeat the logic" is not DRY. Throwing an exception is way more explicit.

dawehner’s picture

+++ b/core/modules/rest/src/Plugin/Derivative/EntityDerivative.php
@@ -115,12 +115,18 @@ public function getDerivativeDefinitions($base_plugin_definition) {
+                throw new RouteNotFoundException($route_name);

Beside the comment from berdir, in general at least let's add a message which is partly helpful.

clemens.tolboom’s picture

Trouble the message will end nowhere. What about adding a logger

try {
              if (($collection = $this->routeBuilder->getCollectionDuringRebuild())) {
                $route = $collection->get($route_name);
              }
              else {
                $route = $this->routeProvider->getRouteByName($route_name);
              }
              if ($route) {
                $this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $route->getPath();
              }
              else {
                $this->logger->error('Route name @route_name not found', array(':route_name' => $route_name));
                throw new RouteNotFoundException($route_name);
              }
            }
            catch (RouteNotFoundException $e) {

# what is different with the throw done above and the more general one?

              // If the route does not exist it means we are in a brittle state
              // of module enabling/disabling, so we simply exclude this entity
              // type.
              unset($this->derivatives[$entity_type_id]);
              // Continue with the next entity type;
              continue 2;
            }

Question remaining is : # what is different with the throw done above and the more general one?

Berdir’s picture

DRY is a nice principle, but not using exceptions for code flow is as well. And we're talking about two lines here.

If you want DRY, then move those two lines within the catch below it and check for if (!isset($route)).

clemens.tolboom’s picture

@Berdir please add your patch otherwise we're playing ping pong.

A misconfiguration of a route name is IMHO an exception of which the DX should be notified. Thus this needs a log message.

Berdir’s picture

Status: Needs work » Needs review
FileSize
2.77 KB

But it is not a misconfiguration, it is a known issue with the current routing/entity links systems and is being addressed in #2259445: Entity Resource unification.

This is what I was suggesting. Also had to harden ResourceRoutes in case you have default configuration for entity types during installation which are currently not there because the derivative class (which should be named Deriver) skipped them.

larowlan’s picture

I assume no way we can add a test here? Unless rebuild.php can be used?

jhedstrom’s picture

You can always test :)

The last submitted patch, 14: 2351253-test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 14: 2351253-14-rest-entity-routes-plugins.patch, failed testing.

jhedstrom’s picture

Reroll of #14. I've re-included the test-only patch to illustrate the fix.

Status: Needs review » Needs work

The last submitted patch, 18: 2351253-test-only.patch, failed testing.

jhedstrom’s picture

Status: Needs work » Needs review
klausi’s picture

Status: Needs review » Needs work

Looks almost ready!

+++ b/core/modules/rest/tests/src/Unit/Plugin/Derivative/EntityDerivativeTest.php
@@ -0,0 +1,134 @@
+    $this->assertSame('bar', $definition['foo'], 'The base plugin definition was not properly merged.');
+    $this->assertSame('entity:entity1', $definition['id'], 'The derivative ID was not properly set.');
+    $this->assertSame('/entity/entity1/{entity1}', $definition['uri_paths']['canonical'], 'The default canonical uri was not properly set.');

the assertion messages are confusing. I think the "not" is wrong. The assertion message should tell you what should happen. If it does not happen during the test then the assertion message will be red, so you don't specify the fail message here but the success message.

jhedstrom’s picture

The assertion message should tell you what should happen.

That is the case with SimpleTests, but with PHPUnit, my understanding was that we use a message to describe the failure since that is the only time it is displayed. I'm not finding this documented for Drupal though. Looking through our existing tests, I'm seeing that to be the case (see \Drupal\Tests\Core\Site\SettingsTest for instance).

klausi’s picture

Interesting, so this should not be blocked on the assertion messages.

If you look at https://phpunit.de/manual/current/en/appendixes.assertions.html#appendix... PHPUnit has very useful output in the generic assertion messages by printing the actual values, which is very helpful when looking at broken tests. Maybe we should move the message to a code comment and stick with the default assertion?

I noticed that there is no test coverage for the RouteNotFound exception in getDerivativeDefinitions(), correct?

jhedstrom’s picture

The RouteNotFound exception is tested in the last assert, it just isn't testing for a thrown exception (since the exception is caught).

jhedstrom’s picture

I could reroll to remove the custom assert messages--most core tests seem to not use custom messages.

jhedstrom’s picture

Status: Needs work » Needs review
FileSize
2.09 KB
7.76 KB

This moves the custom assert messages to code comments. I'm not re-attaching the failing test.

Status: Needs review » Needs work

The last submitted patch, 26: 2351253-26-rest-entity-routes-plugins.patch, failed testing.

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 26: 2351253-26-rest-entity-routes-plugins.patch, failed testing.

Status: Needs work » Needs review
tim.plunkett’s picture

  1. +++ b/core/modules/rest/src/Plugin/Derivative/EntityDerivative.php
    @@ -113,15 +113,22 @@ public function getDerivativeDefinitions($base_plugin_definition) {
    +            if (($collection = $this->routeBuilder->getCollectionDuringRebuild())) {
    

    You can remove a set of parens

  2. +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -71,7 +72,14 @@ protected function alterRoutes(RouteCollection $collection) {
    +      } catch (PluginNotFoundException $e) {
    

    Newline, please.

  3. +++ b/core/modules/rest/src/Routing/ResourceRoutes.php
    @@ -71,7 +72,14 @@ protected function alterRoutes(RouteCollection $collection) {
    +        // of module enabling/disabling, so we simply exclude this entity
    

    Not "enabling/disabling" anymore.

  4. +++ b/core/modules/rest/tests/src/Unit/Plugin/Derivative/EntityDerivativeTest.php
    @@ -0,0 +1,140 @@
    +  public function testGetDerivativeDefinitions() {
    

    Can this method be chopped up to test different parts of the code flow, please?

dawehner’s picture

+++ b/core/modules/rest/src/Routing/ResourceRoutes.php
@@ -71,7 +72,14 @@ protected function alterRoutes(RouteCollection $collection) {
+      try {
+        $plugin = $this->manager->getInstance(array('id' => $id));
+      } catch (PluginNotFoundException $e) {
+        // If the plugin type does not exist it means we are in a brittle state
+        // of module enabling/disabling, so we simply exclude this entity
+        // type.
+        continue;
+      }

It would be great to understand how this could happen, so we could check that as well, like with some kind of flag somewhere?

Berdir’s picture

Status: Needs review » Needs work

[#2382937] just landed, which is supposed to make this a lot less fragile.

We probably need to re-evaluate if we still need anything added here, but it is probably a good idea to still add the test scenario that resulted in this problem.

@dawehner: I had that too I believe in my project, it happens when the entity type is skipped in the deriver and try to access it (in my case, it was default configuration that triggered that).

Berdir’s picture

dawehner’s picture

When I debugged that some days ago the problem was the following:

Rest module ships with some rest.settings.yml which causes a $entity_manager->getDefinition()calls
which throws an exception during route rebuild time.

Due to our "excellent" idea to render exception pages via our normal rendering system, we get another router rebuild, boom.

dawehner’s picture

After #356399: Optimize the route rebuilding process to rebuild on write the chance of this happening is much lower, it just can happen if some code actually rebuilds the router on purpose, which most probably won't happen.

Wim Leers’s picture

+++ b/core/modules/rest/src/Routing/ResourceRoutes.php
@@ -71,7 +72,14 @@ protected function alterRoutes(RouteCollection $collection) {
-      $plugin = $this->manager->getInstance(array('id' => $id));
+      try {
+        $plugin = $this->manager->getInstance(array('id' => $id));
+      } catch (PluginNotFoundException $e) {
+        // If the plugin type does not exist it means we are in a brittle state
+        // of module enabling/disabling, so we simply exclude this entity
+        // type.
+        continue;
+      }

This is the key portion of this patch.

Which means it's solved by #2397271: REST configuration fails if it contains a plugin reference that does not exist, which itself is a temporary measure until #2308745: Remove rest.settings.yml, use rest_resource config entities is solved.