Problem/Motivation
Coming from / related issue: #3207813: ModuleHandler skips all hook implementations when invoked before the module files have been loaded
During deployment, in the config:import phase, when there are modules to be uninstalled and installed at the same time, invoking hooks can lead to a fatal error if the file containing them have not been loaded yet.
This seems to be a regression from #3505049: Drupal 11.1.2 upgrade causes \Drupal::$container is not initialized yet error introduced in 11.2.3.
Steps to reproduce
On clean 11.x with demo_umami and devel
Steps:
- composer require --dev drupal/devel drush/drush
- drush si demo_umami -y
- drush cex -y
- Now go to sites/default/files/sync/core.extension.yml:40 and replace page_cache: 0 with devel: 0
- drush cim -y
// Import the listed configuration changes?: yes.
> [notice] Synchronized extensions: uninstalled page_cache.
> [error] Error: Call to undefined function \devel_entity_type_alter() in Drupal\Core\Extension\ModuleHandler->alter() (line 479 of /var/www/html/core/lib/Drupal/Core/Extension/ModuleHandler.php) #0 /var/www/html/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php(389): Drupal\Core\Extension\ModuleHandler->alter()
...stracktrace...
Stacktrace: https://gist.github.com/vever001/a80414b43d317d84e1d1aacc91cbb895
Proposed resolution
Make hook invocation resilient by ignoring registered hooks that aren’t actually callable at runtime.
See comments #16, #17, #20
Remaining tasks
None?
User interface changes
N/A
Introduced terminology
N/A
API changes
N/A
Data model changes
N/A
Release notes snippet
N/A
| Comment | File | Size | Author |
|---|---|---|---|
| #38 | 3549397-38.diff | 3.06 KB | herved |
Issue fork drupal-3549397
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
herved commentedObservations (so far):
- In that example, it seems to involve
\Drupal\language\EventSubscriber\LanguageRequestSubscriber::onContainerInitializeSubrequestFinished, which gets called when devel gets installed. The container gets -reinitialized and triggers all the language negociation handlers and eventually hook_entity_type_alter.This doesn't seem to be a problem when the deployment only contains a module install.
However when a module gets uninstalled (i.e.: page_cache), then
ModuleInstaller::uninstallclears cached (entity) definitions at the end (see "// Flush all persistent caches.")- Applying the patch from #3207813-95: ModuleHandler skips all hook implementations when invoked before the module files have been loaded solves it, but does it really make sense that
devel_entity_type_altergets called while devel is not actually installed yet?- Another possible fix might be to ensure the hook is actually callable before adding to the list in
\Drupal\Core\Extension\ModuleHandler::getFlatHookListenersWould that be a better solution?
Comment #3
herved commentedComment #4
berdirAn is callable is an option to avoid fatal errors, silently ignoring things is always a bit tricky if something unexpected happens, e.g. discovery of a callback that can't be loaded.
I think what should solve this problem in this case is moving the load() calls to the moduleHandler above updateKernel() in \Drupal\Core\Extension\ModuleInstaller::doInstall. I think that's consistent with how we register class namespaces as well, which also happens before the container is rebuilt.
Comment #5
herved commented#4, indeed that also seems to work.
OTOH it seems a bit strange to me that
devel_entity_type_altergets called while devel is not installed yet. Is that a concern?At this stage, it is still in the preparation phase calling
$this->updateKernelinModuleInstaller::doInstall.I'm not sure what is the safest option to apply until a proper fix is found. I may go with the is_callable option for now...
Comment #8
berdir> OTOH it seems a bit strange to me that devel_entity_type_alter gets called while devel is not installed yet. Is that a concern?
As far as HookCollectorPass is concerned, devel *is* installed at that point, because it exists in the modules list in the kernel. I agree it could be a problem if a hook is invoked that relied on something from the module, but invoking hooks while rebuilding the kernel is an extremely bad idea anyway. It's going through multiple layers, it's also relevant that you have admin language negotiation enabled (\Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUserAdmin), which is the thing that's doing a permission check, which in turn is what is causing the role config entities to be loaded, which is invoking those hooks.
Comment #9
herved commentedExactly, this is why I used demo_umami to reproduce the issue I have on my project.
I identified https://git.drupalcode.org/project/drupal/-/commit/78813e01c72ff50265df2... as the first failing commit which was introduced in 11.2.3. Issue: #3505049: Drupal 11.1.2 upgrade causes \Drupal::$container is not initialized yet error
Specifically, lines 406-411 removed in
core/lib/Drupal/Core/Hook/HookCollectorPass.phpSo this could be seen as regression.
Comment #10
herved commentedComment #11
herved commentedSo far 4 options/ideas:
1. Adding is_callable causes 1 test fail and seems to go against:
ModuleHandlerTest::testImplementsHookModuleEnabled2. Loading module files before updateKernel causes 1 test to fail:
ClassLoaderTest::testAutoloadFromModuleFileHere is the stacktrace:
3. Use loadAll hack from #3207813-95: ModuleHandler skips all hook implementations when invoked before the module files have been loaded
Also fails on
ModuleHandlerTest::testImplementsHookModuleEnabled4. Should the fix focus on the language module and LanguageRequestSubscriber::onContainerInitializeSubrequestFinished which is what is causing havoc here during container rebuild ?
That code originates from #2650434: Clearing cache via UI in translated language resets config translation of field labels to default language, interestingly I cannot reproduce that issue after commenting that event listener, and the corresponding test from ConfigTranslationCacheTest.php passes without it...
Comment #12
nicxvan commentedGreat detective work.
1. I think we should not do this, it's just covering the symptom.
2. I think we should explore this.
3. I think we should avoid this, I don't have a concrete reason, but it feels like a bandaid more than a fix.
4. We should probably explore this as well.
Comment #14
herved commentedI could use some help, but this language subscriber is very suspicious...
For now one workaround is to uninstall modules with a post_update hook instead of letting config:import do it.
Comment #15
nicxvan commentedComment #16
berdirFWIW, I've suggested that we do an is_callable() at runtime in #3495051: ModuleHandler::invokeAllWith() does not check listener is actually callable at the moment a while ago (forgot about that issue, it's partially a duplicate of this), we had that in the old hook system as well.
The specific issue here is limited to legacy hooks in a .module file, it would not happen with OOP hooks because those hooks too are then part of the container and should be load-able. That said, I still think a runtime check doesn't hurt, if only to make it easier to recover a possibly broken site when code has been removed (a module or even just a single method). That scenario will also apply to OOP hooks.
It also slightly overlaps with #3506930: Separate hooks from events because that will change it from a fatal error when trying to invoke that callback to an exception thrown by CallableResolver when trying to resolve that callback. If nothing else, it means thata we'll need a different MR/change if/when we backport this to 11.2
Comment #17
berdirI had a look at the is callable approach test fail. I think that's just an incomplete setup in the unit test, as unit tests typically just set up what they actually need. If you extend that method with an invokeAll() then it fails with a fatal error, because module_handler_test_added_hook is in a hook.inc file that isn't being loaded.
It's even weirder considering that it talks about runtime added module, but there's no runtime adding here, because that has been deprecated and removed in #3481778: Deprecate functions using ModuleHandler::add().
So IMHO, the line with module_handler_test_added is pretty bogus and we can just remove that. We could improve the mocking in that test and add the necessary include file to ProceduralCall, but we're really just adding more legacy stuff and would conflict more with #3506930: Separate hooks from events. It also uses legacy hooks, but it's not a legacy/deprecation test, because legacy hooks don't trigger deprecations yet and we'll need to convert it into OOP hooks anyway.
Comment #19
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #20
berdirThere are now two callable merge requests, one against 11.2 for backport, and a rebased but almost completely changed one against 11.x Things are quite different there. The previously failing test no longer fails, because I fixed the include bits already in the separate hooks issue. Also added an explicit test with an invalid definition.
Comment #21
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #22
berdirbot is confused about the 11.2 MR.
Comment #23
nicxvan commentedCan we clean this issue up a bit? Close the one that isn't relevant anymore and move the language fix to a separate issue?
Also can we update the IS?
Comment #24
smustgrave commentedWill agree with #23
Comment #27
herved commentedCreated #3558942: Investigate whether LanguageRequestSubscriber::onContainerInitializeSubrequestFinished() is still required and appropriate
Comment #28
herved commentedThank you @berdir!
I rebased both branches, it seems tests are passing.
Tried summarizing the solution in the IS, but feel free to adjust if needed, thanks.
Comment #29
smustgrave commentedAssuming the 11.2 branch will have to be 11.3 now?
Comment #30
berdirNo, it just means that it won't be backported anymore to that and that MR is no longer relevant.
Comment #32
john.oltman commentedThe new MR for 11.3 worked for me, thank you @herved
Comment #34
smustgrave commentedSorry this is one where the auto change to main may have hurt? Could 13355 be rebased. Can I assume the other hidden MRs can be closed?
Comment #37
herved commentedyes, MR rebased, closed the others.
Comment #38
herved commentedMR rebased, attaching snapshot for composer.
Comment #39
smustgrave commentedShould we log something during the exception?
Comment #40
oily commentedThe Test-only test shows failing test code:
https://git.drupalcode.org/issue/drupal-3549397/-/jobs/9255778
Comment #41
oily commentedRe: #39 I am not sure anything needs to be logged? Not sure an error status message would be appropriate? So something more informational? Not sure there is anything to report?
Comment #42
smustgrave commentedSince there's been no follow up going to mark and see what committers think.
Comment #43
alexpottThis fix does not feel correct. The order of uninstalls and installs means that this should not occur - see \Drupal\Core\Config\ConfigImporter::getNextExtensionOperation() - we process uninstalls and then installs - devel_entity_type_alter should not be called without devel being installed correctly.
Comment #44
berdir> devel_entity_type_alter should not be called without devel being installed correctly.
It's not about config import at all I think. It's ultimately about the disconnect between when we consider a module installed. HookCollectorPass looks at the list of modules in the kernel/container. When installing a module, this contains the new module when rebulding the container. However, we have not yet loaded the .module file for it. The collector still finds that, registers it and if then someone triggers the hook during the container build, which unfortunately happens for language stuff atm, it won't find it.
This is restricted to the BC layer, but the idea here is also to protect against code being removed, I think you can currently produce the same exception when you develop and remove a defined hook method, or rename or so. I'm unsure how much we want to handle that honestly. But we did protect against completely removing modules to a certain degree in the past and give you warnings on updating instead of hard errors.
Comment #45
alexpottWhat I don't understand at the moment is why we need to uninstall the page_cache module in the config import to trigger the error.
Like why if you install umami and then enable the devel module doesn't this always occur? I've tried to debug this but it's a lot of code step through and highly complex. I suspect this is happening because caches are protecting us when we're only doing the install but if you do the uninstall as part of the config import these caches are cleared and no longer protect us on install.
I'm concerned that the fix here will make it harder to debug some errors because we're just eating them.
Comment #46
quietone commentedChanging title per Special titles.
Comment #47
herved commentedSee #2
Yes, I think LanguageRequestSubscriber::onContainerInitializeSubrequestFinished is really what causes havoc in the first place, because it causes hooks to fire very early at a time when the container is still unstable - #3558942: Investigate whether LanguageRequestSubscriber::onContainerInitializeSubrequestFinished() is still required and appropriate.
Then the fix here adds an extra protection.