Problem/Motivation

We currently try to rebuild the router in kernel.terminate so that it only gets rebuilt once per request. This is an optimization for when multiple modules (or other route rebuilding operations) are triggered in a single request.

However this makes module enabling non-transactional (see related issues) - the state of the installed modules and their routes gets out of sync, which can lead to race conditions and fatal errors.

Additionally, when enabling modules via the UI, it should move into a batch anyway (see related issues), so would get rebuilt every batch request anyway.

Proposed resolution

When enabling a module, just immediately rebuild routes instead of setting rebuildNeeded.

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#55 2589967-2-55.patch13 KBalexpott
#55 51-55-interdiff.txt3.78 KBalexpott
#52 2589967-2-51.patch12.1 KBalexpott
#52 50-51-interdiff.txt759 bytesalexpott
#50 2589967-2-50.patch12.2 KBalexpott
#50 49-50-interdiff.txt8.39 KBalexpott
#49 2589967-2-49.patch11.07 KBalexpott
#49 42-49-interdiff.txt7.17 KBalexpott
#43 39-42-interdiff.txt2.09 KBalexpott
#42 39-42-interdiff.txt1008 bytesalexpott
#42 2589967-2-42.patch9.22 KBalexpott
#39 2589967-2-39.patch8.28 KBalexpott
#39 34-39-interdiff.txt1.23 KBalexpott
#34 2589967-2-34.patch7.74 KBalexpott
#34 31-34-interdiff.txt1.59 KBalexpott
#31 interdiff.txt2.89 KBdawehner
#31 2589967-31.patch7.7 KBdawehner
#28 2589967-28.patch7.2 KBalexpott
#28 22-28-interdiff.txt1.57 KBalexpott
#22 2589967-22.patch5.63 KBalexpott
#22 9-22-interdiff.txt5.58 KBalexpott
#9 2589967-9.patch3.04 KBvalthebald
#2 2589967.patch1.12 KBdamiankloip
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch created an issue. See original summary.

damiankloip’s picture

Status: Active » Needs review
FileSize
1.12 KB

Let's start simple.

I still think we want #2572293: Race condition triggerable by a single user due to router rebuild in kernel.terminate and maybe some extension to destructable to allow similar things to happen in finish_request and not terminate.

Status: Needs review » Needs work

The last submitted patch, 2: 2589967.patch, failed testing.

dawehner’s picture

Oh yeah this basically now requires all kernel tests to have the table in place and would slow them down a bit :(

catch’s picture

It will slow them down a bit but I think we have to prioritize production site stability over test suite performance here.

Or possibly we could look at a null router storage by default for kernel tests.

damiankloip’s picture

Yeah having a properly working site is way more important than test speed :)

dawehner’s picture

Yeah sure, just saying. I think one approach would be to just add it to the base class.

Berdir’s picture

Agreed, either add it to be base class or introduce an in-memory (null might not work, tests possibly rely on it) router storage.

valthebald’s picture

Maybe different approach - get router builder as a parameter of module installer, and use it in install()?
Patch attached

Status: Needs review » Needs work

The last submitted patch, 9: 2589967-9.patch, failed testing.

dawehner’s picture

Is it just me that this issue is more like a routing issue?

catch’s picture

Component: base system » routing system
alexpott’s picture

I think we need to rebuild routing prior to running the install hook so that messages or anything done in there can rely on the module's routes see #2342015: Content Translation module still implements hook_enable

cilefen’s picture

snufkin’s picture

Status: Needs work » Needs review

I can confirm that this solves the issue of using routes in the hook_requirements (#2572459: Route "acquia_connector.setup" does not exist).

The previous test seems to have failed testing due to unrelated issues, so marking it needs review to kick tests off again.

Wim Leers’s picture

alexpott’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -294,8 +301,8 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
-      \Drupal::service('router.builder')->setRebuildNeeded();
       $this->moduleHandler->invokeAll('modules_installed', array($modules_installed));
+      $this->routeBuilder->rebuild();

I think the route rebuild should happen before hook_installed so at least one hook can rely on the routing system being right.

Wim Leers’s picture

cilefen’s picture

dawehner’s picture

Potentially ...

alexpott’s picture

We can't inject the route builder into the module installer because the container is rebuilt during module install so we'd be using the wrong route builder if we do that.

Status: Needs review » Needs work

The last submitted patch, 22: 2589967-22.patch, failed testing.

Berdir’s picture

alexpott’s picture

Version: 8.0.x-dev » 8.1.x-dev

Hmmm patch doesn't apply to 8.0.x so we need to make this an 8.1.x-dev issue at least.

alexpott’s picture

Status: Needs work » Needs review
alexpott’s picture

Issue tags: +Needs tests

We need explicit tests for install and uninstall..

alexpott’s picture

catch’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -294,7 +295,10 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
+      if (Database::getConnection()->schema()->tableExists('router')) {

This isn't compatible alternative routing implementations - we just moved the definition of that table to the implementation too. Can't the router rebuild do that check?

dawehner’s picture

This isn't compatible alternative routing implementations - we just moved the definition of that table to the implementation too. Can't the router rebuild do that check?

This if is entirely not needed anymore now that we lazy create the table.

dawehner’s picture

Expanded the documentation and removed the if.

catch’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleInstallerTest.php
@@ -35,6 +36,10 @@ class ModuleInstallerTest extends KernelTestBase {
+    //properly.

Missing space here, but this is much better thanks. Since that's fixable on commit, RTBC for me.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

So #31 is going to fail because of the rest module - it has an implicit dependency on the node module. Imo we need make that dependency concrete. Easiest way to see this is to run Drupal\KernelTests\Config\DefaultConfigTest with the patch in #31 applied.

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.59 KB
7.74 KB
catch’s picture

Could use a @todo for #2308745: Remove rest.settings.yml, use rest_resource config entities.

Also that means this will need an upgrade path to enable node module on any site using REST, otherwise the dependency won't be met.

In this case we're not adding a new dependency in the code, just making one that's already there explicit, so it's theoretically safe to newly add the requirement but still not fun.

The last submitted patch, 31: 2589967-31.patch, failed testing.

catch’s picture

Thinking about #35, if there's a workaround other than the explicit dependency on node, that'd save the update function and potential behaviour change for sites with REST but not node enabled. Neither is a good option, but nor is postponing this issue on the REST one.

dawehner’s picture

The workaround would be to skip hal/rest module from the DefaultConfigTest

alexpott’s picture

We could just suppress error during installation - that way we still get the default config testing but we not getting the side effect testing of the trigger error in \Drupal\rest\Routing\ResourceRoutes.

dawehner’s picture

Mh, I would honestly prefer to just skip rest/hal in the test. Hiding the error hides it for every module, not just rest.

alexpott’s picture

@dawehner I disagree. The point of this test is that after installing a module its active config matches the default config it provides. Using the @ symbol doesn't hide anything. All the tests that we expect to occur as part of this test occur.

I guess we could just install node if hal or rest is under test but to me skipping a module is worse than doing an @.

alexpott’s picture

So the last issue we have is that people using \Drupal\Core\Url in hook_install will still have potentially really inconsistent bugs. Because if they rely on a dependencies routes it'll work if the module was installed in a previous request but if it is installed in the same request it is going to break... it will depend on how boxes are ticked on the module install page.

Discuss a bit with @dawehner on IRC. The options seems to be:

  • Somehow decorate the route provider service during module install to one that rebuilds lazily.
  • Call the route rebuild during the install loop before hook_install() - which will potentially result in lots of unnecessary rebuilds.
alexpott’s picture

FileSize
2.09 KB

Oops... not quite the right interdiff...

dawehner’s picture

While the discussion in #42 is important it is not in scope of this issue to be honest. The problem already exists ...

catch’s picture

For #1 that'd be more or less what we used to have with Drupal 7-style rebuild needed right?

For #2, we could reduce the performance impact of that by combining it with #2501555: Move menu link rebuilding out of route rebuilding - i.e. only do the menu link rebuild at the end of the request, regardless of how many route rebuilds there are.

dawehner’s picture

For #1 that'd be more or less what we used to have with Drupal 7-style rebuild needed right?

Well, but just during the module installation process. Other processes would still hit the other router table.

catch’s picture

Hmm, so possibly even skip the database altogether then and have an in-memory implementation, closer to what Symfony does itself. Then we don't have to write anything out anywhere at all.

alexpott’s picture

re #44... from the issue summary:

However this makes module enabling non-transactional (see related issues) - the state of the installed modules and their routes gets out of sync, which can lead to race conditions and fatal errors.

I think the discussion in #42 is very much in scope.

alexpott’s picture

Here's a horrible implementation

alexpott’s picture

Something a little nicer...

The last submitted patch, 49: 2589967-2-49.patch, failed testing.

alexpott’s picture

Uninstall updates the container and removes the old service so the swap is not necessary... not sure we need to use the lazy building route provider during uninstall... leaving it in for discussion.

The last submitted patch, 50: 2589967-2-50.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 52: 2589967-2-51.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
FileSize
3.78 KB
13 KB

Fixing some docs and making sure we don't do unnecessary rebuilds.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I think this is as good as it can get now. In general its though quite sad that we need to do special things for routing, as similar problems could exist for all kind of other subsystems as well.

catch’s picture

Assigned: Unassigned » catch
Status: Reviewed & tested by the community » Needs review

I'm not sure about this yet at all.

The request that's installing the modules gets an up-to-date router when it needs it.

However I'm not at all sure what the state is when a new request comes in during or after the router has been lazy-rebuilt.

Too early in the morning for me to answer those questions myself, so assigning to me to look at later.

catch’s picture

Also note to self this looks a lot like the RouteProvider we already have in SimpleTest:

#2589967: Rebuild routes immediately when modules are installed reminded me.

dawehner’s picture

You probably talk about #2605956: Port #2605684 to the new KernelTest but yeah alex certainly was aware of that.

alexpott’s picture

re #57

+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -294,7 +298,15 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
+      if (!\Drupal::service('router.route_provider.lazy_builder')->hasRebuilt()) {
+        // Rebuild routes after installing module. This is done here on top of
+        // \Drupal\Core\Routing\RouteBuilder::destruct to not run into errors on
+        // fastCGI which executes ::destruct() after the module installation
+        // page was sent already.
+        \Drupal::service('router.builder')->rebuild();
+      }

This guarantees we've got an up-to-date router table after module installation regardless of whether it has been rebuilt by the lazy_builder service. So the rebuilt router is definitely available earlier than it is in HEAD and there is no danger that it is not available on the request subsequent to installing a module.

alexpott’s picture

Re the simpletest route provider - yes we can remove it and make it use this one - but I vote for doing that in (a repurposed) #2672762: Move core/modules/simpletest/src/RouteProvider.php to the Drupal\Tests namespace/folder

Novitsh’s picture

Tested #55 and still cleanly applies and fixes the issue. I would love to have this fix out fast. Many separate issues are created for this problem from different points of view and mostly in the issue queue of a contrib module. Some have the issue (like me: fastcgi) and others can't reproduce (mamp or so). See Google results also.

Should we start relate issues from contrib to this post, or?

catch’s picture

Assigned: catch » Unassigned
Status: Needs review » Reviewed & tested by the community
Issue tags: +rc target

I think #57 is better punted to #2572285: Module enabling and router rebuilding should be done in one transaction.

#61 makes sense.

Moving back to RTBC. Also tagging RC target, I think this is worth trying to get in before 8.1.0 if we can.

geerlingguy’s picture

I'm going to also test with a few modules (on PHP-FPM/mod_proxy_fcgi) to see if this patch fixes issues like #2701313: When installing via UI: RouteNotFoundException: Route "facets.overview" does not exist.; I've been running into this all over the place, and also get occasional bug reports from other people installing modules using the UI. Would be nice to get this fixed so the deluge of issues can stop.

+1 on getting this into 8.1.x if at all possible.

[Edit: Tested on Facets, Honeypot, and the rest of the modules I was getting the install WSOD with last Friday—+1 to the RTBC, it makes installing modules actually work.]

  • catch committed a97f7b7 on 8.2.x
    Issue #2589967 by alexpott, dawehner, damiankloip, valthebald: Rebuild...
geerlingguy’s picture

Status: Reviewed & tested by the community » Fixed
Novitsh’s picture

Thanks for the commit and the testing guys!

catch’s picture

Status: Fixed » Reviewed & tested by the community

Not in 8.1.x yet...

alexpott’s picture

I'm definitely +1 to this being an rc target - it solves a whole class of problems the hard to find the cause of.

  • catch committed 2ebaee3 on 8.1.x
    Issue #2589967 by alexpott, dawehner, damiankloip, valthebald: Rebuild...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Cherry-picked to 8.1.x too.

xjm’s picture

Status: Fixed » Closed (fixed)

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

cilefen’s picture

larowlan’s picture

Long time ago, but there may be a memory impact/leak from this change on large sites #3356739: Memory usage increases linearly when (un)installing modules via config import