Problem/Motivation
Activating a module that uses a 'configure' route in its info file leads to this error page :
The website encountered an unexpected error. Please try again later.
Edit #38: Updated problem description, and possible solutions
Edit #5: it is related to fastcgi_finish_request() so it only concerns people running php through fastcgi.
Error in watchdog for Statistics module for example is:
Symfony\Component\Routing\Exception\RouteNotFoundException: Route "statistics.settings" does not exist. in Drupal\Core\Routing\RouteProvider->getRouteByName() (line 193 of /projects/d8/www/core/lib/Drupal/Core/Routing/RouteProvider.php)
Full stacktrace : https://gist.github.com/Haza/be9d14300ff8bd6e8870
Call backtrace: https://gist.github.com/valthebald/731b087d74a316f9f8cb
If you refresh the error page, site is loading again, and the module is marked as enabled (and it is functionnal)
No error when I try to activate the module via Drush.
How to reproduce :
- Install D8
- Activate the "statistics" module in the admin page
If you uninstall the module and re-enable it, you also get the error.
If you comment 'configure' line in module's .info.yml file, there is no error
If you force Drupal to go to another page after enabling the module, i.e. from admin/modules?destination=admin, there is no error
Tested with the following modules :
| Module | Machine name | Has configure? | Reproduced? |
|---|---|---|---|
| Action | action | yes | yes |
| Activity Tracker | tracker | no | no |
| Aggregator | aggregator | yes | yes |
| Ban | ban | yes | yes |
| Book | book | yes | no |
| Forum | forum | yes | yes |
| Responsive image | responsive_image | yes | yes |
| Syslog | syslog | yes | no |
Remaining tasks
Understand what happenssee below- Agree on resolution
- Fix it!
- Ensure it will not happen again (write a test)
What happens
The problem is caused by the fact that ModuleInstaller::install() does not perform router rebuild right after module is installed, but instead, schedules rebuild to happen during kernel->terminate().
In fact, there are 2 problems here:
1. When toolbar module is enabled. Enabling module that has configure route in its .info.yml, under PHP-FPM - fails with WSOD
2. When toolbar module is disabled. Enabling module that has configure route in its .info.yml, works, but module does not have configure link right after enabling: 
After refresh, configure link appears where it should be.
Problem 2 is suppressed by exception handling in AccessManager::checkNamedRoute() (core/lib/Drupal/Core/Access/AccessManager.php lines 87-110) - there is a call to routeProvider::getRouteByName(), which throws RouteNotFoundException.
Problem 1 (when toolbar is enabled), toolbar_toolbar() function renders possible admin toolbar links, and link generation in UrlGenerator::getRoute() (core/lib/Drupal/Core/Routing/UrlGenerator.php starting from line 533) has a call to routeProvider::getRouteByName(), and fails when there is exception there.
Proposed resolutions
There are several ways to address problem 1:
- add try/catch block in UrlGenerator::getRoute (implemented in patch#38)
- add try/catch block in UrlGenerator::getRoute without rebuilding the router. This will hide problem 1 in the same way, as problem 2 is hidden now
- implement try/catch block in RouteProvider::getRouteByName(). rethrow RouteNotFoundException after router is rebuilt
- rebuild router immediately after module is installed
- wait for router rebuild if it's in progress, and we need a new route. This will probably require some way to sync RouteProvider service with RouterBuilder service
- Something else?
Open questions
- What is the best way to address problem 1?
- If we are going to rebuild (or wait for rebuild in process) in exception handler, will that have performance effect?
- Is there a need to address problem 2?
User interface changes
None.
API changes
None.
Data model changes
None.
Beta phase evaluation
| Issue category | Bug because it causes a fatal error on usual UI usage. |
|---|---|
| Issue priority | Major because it's a Fatal error |
| Prioritized changes | Fatal error while enabling a core module on certain circumstances. |
| Comment | File | Size | Author |
|---|---|---|---|
| #120 | router_rebuild_fpm-2564921-120.patch | 5.28 KB | duaelfr |
| #85 | interdiff-2564921-78-85.txt | 1.37 KB | valthebald |
| #85 | 2564921-85.patch | 5.53 KB | valthebald |
| #78 | interdiff-66-78.txt | 3.16 KB | valthebald |
| #78 | 2564921-78-combined.patch | 4.59 KB | valthebald |
Comments
Comment #2
hazaComment #3
hazaComment #4
mr.baileysI'm unable to reproduce this against HEAD using the steps outlined in the IS. When enabling the statistics module immediately after installation, and after uninstalling/re-enabling the statistics module, everything appears to work as expected, no errors are thrown and I can access the modules settings page.
Comment #5
hazaSo, I made more tests.
It seems that I am only able to reproduce this using PHP-FPM.
What kind of PHP are you using mr.baileys ?
The main difference with php-fpm and our application is in
Response::Send().The function "
fastcgi_finish_request()" only exists in a php-fpm context.If I remove the call to
fastcgi_finish_request(), everything works fine.Can someone else confirm that ?
Comment #6
duaelfrI have PHP-FPM too and I can reproduce.
Comment #7
duaelfrUpdated IS with #5 informations.
Added beta eval.
Comment #8
duaelfrUpdated IS and title with more details.
Comment #9
duaelfrComment #10
hazaThanks for the IS update and the tests on the other core modules !
Comment #11
duaelfrBacktrace asked on IRC.
Comment #12
valthebaldChanged the title to point that this is relevant for PHP-FPM. Also, tagging, and changing subsystem to routing system (not only statistics module is affected)
Comment #13
dawehnerSo the fact why this just happens on PHP-FPM is that we reproduce the router now on kernel.terminate.
On FPM this happens after the response was sent out to the client, in which case the router is rebuilt afterwards. The browser in the meantime redirects to the page and at that page load the route is not there yet.
On the other hand I don't see why this should be a critical issue. If you hit f5 again everything will be alright.
Comment #14
dawehnerIts kinda by design how we rebuild the router at the moment.
Comment #15
dawehner@catch and @dawehner talked about that on IRC
The assumption first was that the configure links for the modules are broken but indeed the toolbar is causing problems.
Its not really clear how the menu rebuild can be done before the router rebuild is done, given that the menu rebuild is coming 100% after the router finished, see
\Drupal\Core\Routing\RouteBuilder::rebuild... this is confusing.While browsing through some code I found
\Drupal\page_manager\Plugin\PageManagerMenu\NormalMenu::invalidatedoing its own menu link rebuilding, but I cannot imagine how this could exactly cause the issues described here. Do you have page_manager installed on that particular site?
Comment #16
hazaNo page_manager here. Pure D8 without any additional stuff (contrib) inside.
Comment #17
duaelfrFollowing @dawehner's instructions I tried to put some traces at the beginning and the end of these functions:
The result follows:
Note: _toolbar_do_get_rendered_subtrees() crashes before it reaches its "END" trace.
Note 2: @Haza told us on IRC that adding a sleep(5) on the first line of MenuLinkManager::rebuild() "fixes" the issue.
Comment #18
duaelfrIf someone wants to continue here is the patch that adds the traces.
Comment #19
valthebaldUpdate: issue happens only when toolbar module is enabled
Comment #20
valthebaldComment #21
valthebaldFirst take on solution. But I have no idea how (and if) we can test the issue.
Comment #22
valthebaldComment #23
hazaJust doing some monkey-manual-testing with the patch in #21, and I don't have the error anymore.
I fear that the issue is deeper than that, and the patch seems just to be a workaround, not addressing the real issue behind that.
Comment #24
duaelfr@valthebald Thank you for your patch! Just one question:
Why don't you directly call $builder->rebuild() here?
Comment #27
valthebald@DuaelFr: no reason, I've left $builder variable just as a leftover from other things that I tried to do. Anyway, testbot didn't like this approach, as it breaks other things.
Another possible solution is to try and rebuild routes in RouteProvider, giving the final chance before throwing RouteNotFoundException.
Check may be placed in RouteProvider::getRouteByName():
or, as alternative, it is possible to catch RouteNotFoundException in ModulesListForm::buildForm():
I personally prefer the second method, since it's less disruptive. Let's see what testbot thinks
Comment #28
duaelfrGrrr, cross-posting made me lose all my comment :/
I would suggest, as it is a toolbar issue, to fix it in the toolbar module. The easiest would be to implement
hook_modules_installed()as it'd have the less disruptive impact. The cleanest would be to find somewhere in_toolbar_do_get_rendered_subtrees()to callRouteBuilder::rebuildIfNeeded().About #27: I think that you should use rebuildIfNeeded() in your two options because it spends less resources in case the route is not defined at all and it's not just a rebuild issue.
Comment #29
dawehnerThis is what I don't get, the backtrace has shown the toolbar
Comment #30
valthebaldI was able to reproduce the issue with toolbar module switched off, my comment #19 was wrong. More correctly, with toolbar module the issue is always reproducable, without it happens randomly.
Comment #31
dawehnerSo toolbar is actually a different problem we need to fix on top of that?
Comment #32
dawehnerMHH it seems to be that all of that are just workarounds, do we actually understand for toolbar how it could happen?
Comment #33
duaelfrI don't ;)
Comment #34
valthebaldI've updated issue description with what (IMO) leads to the issue - "What happens" section.
toolbar module, while not being direct cause, can make the issue more reproducible in 2 ways:
I cannot prove that theory though
Comment #35
valthebaldI was able to dig some further. I placed traces of current request time (float) in the following pieces of code:
Here are results of enabling Actions module under different conditions:
mod_php, toolbar enabled
1441880274.303 rebuild in process Drupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:195
1441880274.303 cache reset Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::reset:390
1441880274.303 rebuild finished Drupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:206
1441880276.556 Drupal\system\Form\ModulesListForm:Drupal\system\Form\ModulesListForm::buildRow:303
Process finished successfully, no errors. First request (1441880274.303) finished before form is finished to user
FPM, toolbar disabled
1441880731.3191 Drupal\system\Form\ModulesListForm:Drupal\system\Form\ModulesListForm::buildRow:303
1441880731.3191 Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::getRouteByName:194
1441880731.3191 exception caught Drupal\Core\Access\AccessManager:Drupal\Core\Access\AccessManager::checkNamedRoute:101
1441880729.849 rebuild in processDrupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:195
1441880729.849 cache reset Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::reset:387
1441880729.849 rebuild finished Drupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:206
Process finished successfully, no errors. Second request (1441880731.3191) throwed RouteNotFoundException, which was caught in AccessManager::checkNamedRoute(). Actions module line in modules list doesn't have configure link. After refresh, configure link is present
FPM, toolbar enabled
1441881096.3129 Drupal\system\Form\ModulesListForm:Drupal\system\Form\ModulesListForm::buildRow:303
1441881096.3129 Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::getRouteByName:194
1441881096.3129 exception caught Drupal\Core\Access\AccessManager:Drupal\Core\Access\AccessManager::checkNamedRoute:101
1441881094.5781 rebuild in processDrupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:195
1441881094.5781 cache reset Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::reset:387
1441881094.5781 rebuild finished Drupal\Core\Routing\RouteBuilder:Drupal\Core\Routing\RouteBuilder::rebuild:206
1441881096.3129 Drupal\Core\Routing\RouteProvider:Drupal\Core\Routing\RouteProvider::getRouteByName:194
Process crashed with unhandled exception. Second request (1441880731.3191) throwed RouteNotFoundException, which was caught in AccessManager::checkNamedRoute(). After that, first request finished rebuilding router, second request throwed RouteNotFoundException for the second time, which was not caught
Comment #36
valthebaldHere's the patch that shows the problem. Test should fail.
I am not sure where this test belongs. Since it needs to enable some module, I don't think it can be placed at Drupal/Tests/Core/Routing. My guess is Drupal\system\Tests\Routing\RouteBuilderTest.
Uncommenting line 28:
makes the test pass
Comment #38
valthebaldComment #42
valthebaldTestbot hiccup?
Comment #46
valthebaldMy bad, wrong annotation and class name for the test.
No other differences from #38
Comment #49
duaelfrFirst of all, the patch in #46 fixes the issue, thank you for that.
Now, I have a serious doubt about the way it's done.
With this patch, each time the UrlGenerator::getRoute() method is called on a non-existent route, it rebuilds the entire routes registry. In some cases that could be a huge performance problem (imagine a script that loops on expected routes to find a good one). I think we should only rebuild that registry once per request.
I tried to use rebuildIfNeeded() instead of rebuild() but it does not fix the bug anymore.
Comment #50
valthebald@DuaelFr: I have the same performance concern.
Unfortunately, we cannot use check inside rebuildIfNeeded() - it relies on the value of static variable $rebuildNeeded, which scope is only the current request. What we need is a state variable shared between route builder and route provider. Maybe we can use state service for that purpose?
Comment #51
duaelfrThat is what I was thinking too.
I think we should ask some specialist in the critical team to give us his/her opinion.
Comment #52
valthebaldOK, here's the change. I've added state service to RouteBuilder constructor, changed logic of RouteBuilder::rebuildIfNeeded() - to use state service instead of static variable.
It made sense to me to move logic of giving final chance to RouteBuilder from UrlGenerator to RouteProvider.
No interdiff since it's different logic compared to the last patch
Comment #57
damiankloip commentedMe and catch were going through the code and rather that try to just catch the exception, which could lead to problems discussed above. As well as coding around kind of specifically for a case like this.
We could consider just moving the rebuild event subscriber to use FINISH_REQUEST instead of TERMINATE. As this will always run before the response is sent (See HttpKernel::filterResponse()), terminate is called after the response is sent.
Comment #58
damiankloip commentedWhoops.
Comment #59
catchOpened #2572293: Race condition triggerable by a single user due to router rebuild in kernel.terminate #2572285: Module enabling and router rebuilding should be done in one transaction and #2572283: Neither REPEATABLE READ nor READ COMMITTED transaction isolation levels are always appropriate for the discussion, we could mark the kernel.terminate issue as duplicate of this one, couldn't find it at the time.
Comment #60
damiankloip commentedcatch, do you want me to move this patch into #2572293: Race condition triggerable by a single user due to router rebuild in kernel.terminate then?
Comment #61
damiankloip commentedComment #62
valthebaldUnfortunately, patch from #58 does not fix original problem
Comment #63
damiankloip commentedThat's why we split it into the child issue now. It feels like the patch in #52 is kind of wrong. Couldn't two processes be trying to rebuild the router in that case?
Comment #64
valthebald@damianklop: it looks like RouterRebuildSubsciber needs also this addition to core.services.yml (code was not executed until I added it)
Comment #65
damiankloip commentedYes, good spot. This is not even running. I am wondering what we are doing with this subscriber in that case!
Comment #66
valthebaldI've slightly modified the patch from #62:
Without the second change, issue is still there. ModuleInstaller calls \Drupal::service('router.builder')->setRebuildNeeded() *before* RouterRebuildSubscriber::onKernelFinishRequest(), but RouterRebuildSubscriber::routerBuilder->isRebuildNeeded is false during that call.
Comment #67
damiankloip commentedI think we need to work out why this subscriber is not used but still in core now.
Comment #68
valthebald@damiankloip: this code from KernelDestructionSubscriber:
destructs RouterBuilder service, which calls rebuildIfNeeded(). So yes, in the current HEAD RouterRebuildSubscriber is not called, so it's just a ballast code.
If we move RouterRebuildSubscriber to react to FinishRequest, KernelDestructionSubscriber still will work, but rebuild will not happen for the second time (because rebuildNeeded==FALSE)
Comment #69
mpotter commentedI found a way to reproduce this in page_manager (#2301485: Fatal error: Call to a member function getOption() after creating a new page). Using PHP-FPM on a fresh D8 site with page_manager, I would get an error when creating a new page because the page edit form was trying to load before the Route rebuild had finished saving to the DB. This is because RouteBuilder is doing the call to rebuild in destruct. Using the patch in #66 fixed the error. It causes the rebuild to happen earlier before the edit form is loaded and needs to access the routes.
Adding #66 fixed the race condition. Removing #66 brought it back again. So I'm fairly confident I had a good test system for reviewing this issue and feel pretty safe marking this RTBC.
Comment #70
valthebaldComment #71
duaelfrWell done mates, it's a really nice catch.
Thank you allowing me to discover that Kernel Terminate event and how symfony deal with php-fpm. I now understand one of the reasons why php-fpm has such good performances! :)
Comment #72
catchWe should have had some test coverage break due to this not getting run, let's add some here.
Also are there any places we're forcing a rebuild but could stop doing so now this is fixed?
Comment #73
dawehnerI'm also quite convinced that for drush we should keep kernel terminate, because this is executed there, but finish request just doesn't make sense, if you ask me.
Comment #74
valthebald@dawehner: kernel terminate cleanup task is run anyway, because rebuildIfNeeded() is part of RouterBuilder destructor.
Comment #75
valthebald@catch: what is the best way to test if event listeners is being called? I haven't done such a thing yet
Comment #76
damiankloip commentedI think we should have one or the other, as we need finish_request too, it makes sense for the RouteBuilder class not to implement destructable interface and just use one subscriber? HOWEVER, destruction will check if the service was used. So this change I guess could be problematic as it would always instantiate a RouteBuilder service regardless. Which is potentially not good.
Comment #77
valthebaldMaybe subscriber can check if the service was initialized
Comment #78
valthebaldAttached (new) test that checks that finish request listener is called, and finish request listener checks if router builder service was initialized
Comment #81
mpotter commentedOK, I can confirm the following:
1) With the patch from #66 NOT applied, but using the test from #78, the test correctly fails.
2) With the patch from #66 applied, the test in #78 passes.
So this looks good. The test reproduced the problem and correctly fails on a PHP-FPM system.
Comment #82
damiankloip commentedThanks for testing, that is some much needed validation. However, I think we still need to do a little bit of work. We currently implement DestructableInterface and have the subscriber, with the subscriber mimicking what destructable functionality provides (checking whether the service has be instantiated).
I think we need to either:
1. Remove DestructableInterface from RouteBuilder, and add kernel_events.terminate in the RouterRebuildSubscriber so both events are implemented there.
2. Either extend Destructable, or implement something similar that will provide this functionality but run in kernel_events.finish_request.
Comment #83
valthebald@damiankloip: shouldn't this be addressed in a separate issue #2572293: Race condition triggerable by a single user due to router rebuild in kernel.terminate?
Comment #84
damiankloip commentedIt could do, but it seems more sensible to just do it all here, and have everything verified as working together. Or there, I don't mind. The final solution here would affect that anyway. That issue was going to be for the actual fix, but you kept working in here, so we stayed in here :)
Comment #85
valthebaldLet's see what testbot says for removal of DestructableInterface
@damiankloip: why do you suggest to add kernel_events.terminate in the RouterRebuildSubscriber? I thought one of the goals is to *not* rebuild on kernel terminate
Comment #86
damiankloip commentedWell. Things like drush will still rely on terminate, as stated somewhere above or the referenced issue. I forget now. If it happens in finosh request terminate is a null op on a regular request so no harm done.
Also, I don't think the change in the subscriber to use \Drupal:: to get the route builder will fly...
Comment #87
valthebald@damiankloip: if we need both finish request and kernel terminate, what's the point to remove destructable interface?
Comment #88
valthebald@damiankloip: regarding your doubt about route builder - well, apparently it works, according to both test and the fact that the patch solves the problem :)
Comment #91
valthebaldApparently, removing DestructableInterface is not a small change... There are lot of tests relying on RouteBuilder::destroy()
Anyway, I'm still confused by (IMO controversial) needs to remove DestructableInterface and not rebuild router on kernel.terminate
Since we call rebuildIfNeeded() on the same object on both finish request and kernel terminate events, router is not rebuilt for the second time, so there is no performance impact here
Comment #92
damiankloip commentedIt's not about the performance impact. I understand how this is all working as I suggested the fix originally... and outlined the need for both. The issue I see is that the subscriber is being added back, made container aware, and replicating functionality of destructible interface. So having these things in two places makes things more confusing.
I would be very surprised if this patch got committed in its current state though.
Comment #93
valthebald@damiankloip: I still don't understand how do you suggest to address that. Can we discuss this briefly in IRC?
Comment #94
mpotter commentedThe problem with "doing it all here" is that we are risking this not getting fixed anytime soon. Can we work iteratively here with multiple issues instead of blocking this till it's perfect. I'm very concerned about problems people will have with the RC release on PHP-FPM. Doing it in two places seems a lot better than doing nothing at all and having people getting fatal error messages.
e.g., trying to work on Page Manager for D8. This issue causes creations of new page routes in the UI to fail on systems with PHP FPM "sometimes" which leads to a lot of head-scratching and time consuming debugging and confusion.
Comment #95
damiankloip commentedYes, appreciate the need to get something in. But there is a line to just getting whatever in. I'm afraid at this late stage in the cycle, that doesn't wash quite as easily. IMO this needs to be done in one go.
Comment #96
fabianx commentedUhm, this was a big performance improvement to lazy build the router we are removing here.
I think the biggest problem - besides the router being dirty - which is a totally different problem - is 302 responses for the day-to-day use-case.
We already use our own class for redirects, so we could be using a ResponseEmitterInterface (Something like #2577631: Allow HtmlResponse to use a flexible emitter) to control the send() function from a service.
We could then check if any dirty laundry operation is in progress for terminate and choose to not call fastcgi_finish_request() and instead wait().
On the other hand, then we could have waited all along ...
Comment #97
damiankloip commentedThis is not removing the laziness I don't think. It just replicates what DestructableInterface is doing. The response emitter idea is similar to something me and catch were talking about before, extending the response to not call fastcgi_finish_request(). This is definitely an interesting idea!
The thing that is potentially worrying is that another modules doing something similar will also need to do whatever we do here. Which may not be obvious at all.
The underlying issue is still here, that terminate is not meant to be used for this type of thing, finish_request is. I think.
Comment #98
fabianx commentedTalking with dawehner, the most easy change is to use a very late FinishResponseSubscriber and do:
That is a around 20 loc code change that even contrib could implement and would solve the problem for the 302 to the new page for views and page manager in the least invasive way for PHP-FPM.
Comment #99
valthebald2 notes:
something like this?
Comment #100
damiankloip commentedDo you know why that is the case? Why \Drupal:: has to be used? Is this because the container is being rebuilt? Because that certainly seems like a big red flag, as previously mentioned.
Comment #101
valthebaldLuckily (no big red flag for now), this is not related to container being rebuilt, rather to the fact that service is lazily loaded. What happens is:
IMO, execution flow makes no sense in having @router.builder as a parameter of RouterRebuildSubscriber constructor
Comment #102
valthebald$this->router->rebuildIfNeeded() or \Drupal::service('router.builder')->rebuildIfNeeded() will instantiate RouteBuilder. #78 uses $container->initialized() to do it only when necessary.
Comment #103
damiankloip commentedHmm, that seems to be quite a big problem generally. It definitely makes sense for the subscriber to inject the route builder service, as otherwise we are just falling back for Drupal globals again.
Comment #104
valthebaldRouterBuilder already treats $this->rebuildNeeded as a global. This issue just makes it more visible.
OK, everybody agrees using globals is bad. Should this meta be fixed in *this* issue? I doubt so.
Why can't we fix this (very major - good definition) issue, and move forward? Creating follow up issues (or linking existing) is fine, but I don't see why it should stop this issue from being fixed.
Comment #105
valthebaldFor the record: making rebuildNeeded static variable of RouterBuilder class makes usage of $this->router possible in RouterRebuildSubscriber. Of course, it still leaves the problem of loading of RouterBuilder class only when it's necessary
Comment #106
fabianx commentedI agree with #104: I think there is no way around using the container aware approach that DestructableKernelInterface uses.
Sorry for the slight distraction.
Comment #107
damiankloip commentedI don't think there is an issue making this container aware, I think this outlines a bigger issue that only \Drupal::service('route.builder') works within the subscriber as the container is changed mid request!
$rebuildNeeded is not global, it's just a property on the RouteBuilder instance.
Comment #108
catchOpened #2589967: Rebuild routes immediately when modules are installed.
Comment #109
valthebald@catch: as far as I see, there is a bunch of stuff happening before RouterBuilder::setRebuildNeeded() and actual rebuild. I also reckon that I when tried to call RouterBuilder::rebuild() right after module is enabled, this broke quite many tests. Do you see any flows in approach of #78? The patch solved the problem, worked, and passed all the tests
Comment #110
valthebaldwhat's important, #78 is less disruptive than initial patch of #2589967: Rebuild routes immediately when modules are installed
Comment #111
damiankloip commentedWhat's worrying about that is it only works with \Drupal::service () because it uses a different container to the one the event was dispatched from. This makes things very weird.
Comment #112
valthebald@damiankloip: is it the same question that you asked in #100? If so, I've answered in #101 - no, it's not because different container is used, it's because RouterBuilder service uses non-static variable rebuildNeeded, which is always set to FALSE in constructor.
EventSubscriber gets proxy $router_builder in constructor => ModuleInstaller initializes router.builder service => ModuleInstaller sets rebuildNeeded in RouterBuilder instance that is stored in container => EventSubscriber reacts on finish request event => EventSubscriber gets new instance of RouterBuilder => new instance's rebuildNeeded is FALSE => new instance's rebuildIfNeeded does nothing.
This flow pretty much explains why \Drupal::service solves the problem - because it doesn't create another instance of RouterBuilder.
What's weird in this explanation?
Comment #113
catch@valthebald the patch here solves the specific bug with php-fpm that we do things in terminate thst must complete before a redirect is finished.
It does not solve the issue that we have a race condition between module install and router rebuild - a second tab hitting the modules page could triggered the same error here even with this patch applied.
Comment #114
damiankloip commentedAnd why do you think you get a new instance of the route builder the second time?! Because you have a new container. If it was the same container why would you get a new instance? The proxy would give you the same instance as the CONTAINER is injected in there.
So what you are explaining is the exact symptom of there being two containers, hence the need for you to use \Drupal::service(). This will use the router builder from the new container, where are injecting it AND the event subscribers are attached and invoked from the old container.
So it's not your explanation that's weird, it's the conclusion.
Comment #115
duaelfrHi there, I'm sorry to have left this thread without love for such a long time.
I suggest that everyone take a deep breath and calm down. You are all doing great job here and on Drupal in general so let's keep the head out of water and smile :)
So, can we focus on an acceptable fix and let a todo (an open a follow-up) to fix the deeper problem later?
As a lot of hosters are running on php-fpm, we need that immediate problem to be fixed quickly. I agree that the container should not be rebuilt but no one seems to yell for this particular issue so that can wait a bit.
I think our best option for now is to use a static variable to mark the router builder as needing a rebuild AND move this call to the finish request event instead of terminate. Let's do this and move on!
Comment #116
damiankloip commentedThere is no issue with using finish_request here, I was the one that suggested that originally instead of the original locking fix.
The trouble I see is that we have to use \Drupal::service('route.builder') from the event subscriber because the event is invoked from container A but module handler uses container B. So this fix to use the global method to get the current container just seems like a hack.
And as catch says, it only really fixes the problem for one case, enabling a module via the UI with FPM. Yes, quite a big problem but this is a general problem we have. Rebuilding the container during the cycle of a request is bad™
Comment #117
valthebaldFirst, I need to apologize if my comments hurt anyone. This issue is stuck with no visible progress for quite some time, so I maybe gave unnecessary emotions outbreak.
Second, is #2589967: Rebuild routes immediately when modules are installed is committable any time soon? @catch mentions race condition in #113, but was it ever spotted in the wild? When I was thinking about initial test, I performed multiple debug sessions, stopping execution of module install at different stages, and tried to reproduce WSOD by opening module list in the second browser tab - and module list in the second tab always showed up correctly. So the patch can solve bigger problem, than described in this issue. This is true. But it also for sure makes tests slower, and break other things. All in change for what it seems like theoretical problem solution. Do we want to do it before 8.0.0?
Third
nobody argues with that. But how is that related to the issue?
Fourth, to sum up:
this issue (and last known-to-fix-the-issue patch #78) itself is not about container rebuild, or router rebuild. It's about very specific fact that FPM request continues execution after data is sent to client (all heil FPM power). Of course, if #2589967: Rebuild routes immediately when modules are installed was already committed, this issue would be irrelevant. If container was not rebuilt during request life cycle, there would be no need in using \Drupal::service().
I would be happy (for the sake of solving this specific issue) if router was rebuilt immediately. I would be happy if container was not rebuilt during one request cycle. None of the last things is true. So what do we do?
Comment #118
duaelfrMy suggestion is the following :
- move the call to rebuilIfNeeded in the finish request event
- use a getter and change the setter to rely on Drupal_static in the router builder and ensure the rebuildNeeded flag is consistent across multiple instances
- add a todo comment to remove that trick once we are sure the container is not rebuilt during the request
Sadly, I don't think I'll have the time to propose the associated patch.
Comment #119
catchComment #120
duaelfrHere is an half-working patch following my suggestion in #118.
It fixes the issue in my fpm environment.
I need some help with the following :
- add the todo comment and improve comments to explain why we use drupal_static (as a non-native speaker I have some difficulties to write that king of thing)
- fix the unit tests (the calls to drupal_static throw errors and I don't know how to bypass them)
Comment #122
aspilicious commentedIs this DS issue related? https://www.drupal.org/node/2594983
Comment #123
valthebald@aspilicious: it looks so!
Comment #124
fizk commentedI just wanted to double check if anything has been committed for this. I hope 8.0 isn't released with this bug?
Comment #125
duaelfrI just manually tested on HEAD and the answer in "no".
Comment #126
valthebaldThe issue is too big to find its way into 8.0.0, given known release date. But we should try and solve the problem of late router rebuild in 8.0.1 (no need to wait until 8.1 or 9.x)
Comment #127
fizk commentedI know you guys do amazing work, and I always appreciate it, but I have to say, this is going to be such a common scenario, people are going to be running into this bug a lot when 8.0 is released.
IMHO, a WSOD in a common use case should have been marked critical, and a 8.0 release blocker.
Comment #128
catchAt this point this is a duplicate of #2572293: Race condition triggerable by a single user due to router rebuild in kernel.terminate and #2589967: Rebuild routes immediately when modules are installed. The first issue is a quick fix (similar to the last patch here) that could probably land before 8.0.0 if it's ready, but it needs test coverage and probably manual testing on php-fpm.
In general, issues tend to get fixed faster when patches get reviewed and test coverage, rather than by asking when they're going to be fixed.